Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ff56aeb1c1 | |||
| c5d080d60d | |||
| d5c4844583 | |||
| 2a971c6f16 | |||
| 67d44ba23f | |||
| 7fdec8e7fc | |||
| 5493d6f271 | |||
| 775a02139f | |||
| e68d24406e | |||
| e87630d731 | |||
| 5c55ca7ea8 | |||
| 970e0f066b | |||
| df92d9c409 | |||
| f2adde5085 | |||
| 8ea1a1bc8e | |||
| a13b1e49e5 | |||
| 6b66e1479d | |||
| ade5dbf5cd | |||
| 0a39db82dc | |||
| be8fb87593 | |||
| e4a948b322 | |||
| 5eefa5a400 | |||
| 374d08671f | |||
| 45de2b5144 | |||
| 94cd180e64 | |||
| 0e00242543 | |||
| 9463df7516 | |||
| 45fab3f079 | |||
| d597b772e2 | |||
| 7abc703dbb | |||
| f9f94ecffd | |||
| c5002c79a5 | |||
| 1c720bfd18 | |||
| e55790a197 | |||
| a46e606ed0 | |||
| b6f5d3ea9f | |||
| d909647537 | |||
| a13fa85dcd | |||
| 7fbd2d120a | |||
| 9ac8c2a48e | |||
| e0fc982ae5 | |||
| c3736a0955 | |||
| 05d621a883 | |||
| 7927a80e63 | |||
| 6d02be04f7 | |||
| e27eec824f | |||
| 01def5d074 | |||
| 2f4e3cf8e3 | |||
| 2cf7d93b74 | |||
| bcc6ed5ca1 | |||
| 0c51cac769 | |||
| 47b660068e | |||
| 342c0fb72d |
@@ -7,10 +7,10 @@ on: # rebuild any PRs and main branch changes
|
||||
- 'releases/*'
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
pre-commit-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
|
||||
@@ -19,6 +19,10 @@ jobs:
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18.17.1'
|
||||
cache: 'yarn'
|
||||
- name: Yarn Install
|
||||
run: yarn install
|
||||
- uses: actions/cache/save@v3
|
||||
@@ -27,3 +31,6 @@ jobs:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
- uses: pre-commit/action@v3.0.0
|
||||
- name: Tests
|
||||
run: yarn test
|
||||
continue-on-error: true # ignore test failures for now
|
||||
|
||||
@@ -13,7 +13,6 @@ It is a GitHub Action that publishes a Speckle Automate Function definition to S
|
||||
> This is a low level building block action.
|
||||
> As a Speckle Automate Function developer you probably want to use our [composite action](https://github.com/specklesystems/speckle-automate-github-composite-action)
|
||||
|
||||
|
||||
### Inputs
|
||||
|
||||
#### `speckle_automate_url`
|
||||
@@ -32,19 +31,41 @@ If you believe your token has been compromised, please revoke it immediately on
|
||||
|
||||
Please note that this is not a Speckle Account token, but a **Speckle Automate API** token. You can create one by logging into the [Speckle Automate Server](https://automate.speckle.dev) and going to the [API Tokens](https://automate.speckle.dev/tokens) page.
|
||||
|
||||
#### `speckle_function_path`
|
||||
|
||||
The path to the Speckle Automate Function to publish. This path is relative to the root of the repository. If you provide a path to a directory, your Speckle Automate Function must be in a file named `specklefunction.yaml` within that directory.
|
||||
|
||||
#### `speckle_function_id`
|
||||
|
||||
*Optional.* If you have already registered a Speckle Function, you can use the ID of that Speckle Function to ensure that any changes are associated with it.
|
||||
If you do not provide a Function Id, we will attempt to determine the Function ID based on the GitHub server, GitHub repository, Reference (branch), and the Speckle Function Path.
|
||||
|
||||
Providing a Speckle Function ID allows you to change one of those values, and update the original Function instead of creating a new one.
|
||||
Associates this new version with the given ID of a Speckle Function.
|
||||
|
||||
Your Speckle Token must have write permissions for the Speckle Function with this ID, otherwise the publish will fail.
|
||||
|
||||
#### `speckle_function_input_schema_file_path`
|
||||
|
||||
*Optional.* The path to the JSON Schema file that describes the input schema for this version of the Speckle Function. This file is used to define the input form that will be presented to users when they compose an Automation based on this Function. If not provided, no input form will be presented to users.
|
||||
|
||||
#### `speckle_function_command`
|
||||
|
||||
The command to run when this version of the Speckle Function is invoked. This command must be a valid command for the Docker image that contains the Speckle Function. This command must be a single string.
|
||||
|
||||
#### `speckle_function_release_tag`
|
||||
|
||||
The release tag for this version of the Speckle Function. This is intended to provide a more human understandable name for this version, and we recommend using the Git SHA of the commit used to generate this function version. The name must conform to the following:
|
||||
|
||||
- A minimum of 1 character is required.
|
||||
- A maximum of 128 characters are permitted.
|
||||
- The first character must be alphanumeric (of lower or upper case) or an underscor.
|
||||
- Subsequent characters, if any, must be either alphanumeric (lower or upper case), underscore, hyphen, or a period.
|
||||
|
||||
#### `speckle_function_recommended_cpu_m`
|
||||
|
||||
*Optional.* The recommended maximum CPU in millicores for the function. If the Function exceeds this limit, it will be throttled to run within the limit.
|
||||
|
||||
1000 millicores = 1 CPU core. Defaults to 1000 millicores (1 CPU core).
|
||||
|
||||
#### `speckle_function_recommended_memory_mi`
|
||||
|
||||
*Optional.* The recommended maximum memory in mebibytes for the function. If the Function exceeds this limit, it will be **terminated**.
|
||||
|
||||
1024 mebibytes = 1 gibibyte. Defaults to 100 mebibytes.
|
||||
|
||||
### Outputs
|
||||
|
||||
#### `version_id`
|
||||
@@ -67,9 +88,9 @@ with:
|
||||
# speckle_automate_url is optional and defaults to https://automate.speckle.dev
|
||||
# The speckle_token is a secret and must be stored in GitHub as an encrypted secret
|
||||
# https://docs.github.com/en/actions/security-guides/encrypted-secrets#using-encrypted-secrets-in-a-workflow
|
||||
speckle_token: ${{ secrets.SPECKLE_TOKEN }}
|
||||
# speckle_function_path is optional and defaults to ./specklefunction.yaml
|
||||
# speckle_function_id is optional and will be auto-generated if not provided
|
||||
speckle_token: "${{ secrets.SPECKLE_TOKEN }}"
|
||||
speckle_function_id: "abcdefghij"
|
||||
speckle_function_release_tag: "1.0.0"
|
||||
```
|
||||
|
||||
#### Publish a function to a self-hosted server
|
||||
@@ -81,8 +102,8 @@ with:
|
||||
speckle_automate_url: https://example.org
|
||||
# https://docs.github.com/en/actions/security-guides/encrypted-secrets#using-encrypted-secrets-in-a-workflow
|
||||
speckle_token: ${{ secrets.SPECKLE_TOKEN }}
|
||||
# speckle_function_path is optional and defaults to ./specklefunction.yaml
|
||||
# speckle_function_id is optional and will be auto-generated if not provided
|
||||
speckle_function_id: "abcdefghij"
|
||||
speckle_function_release_tag: "1.0.0"
|
||||
```
|
||||
|
||||
### Example usage within an entire GitHub Actions Workflow
|
||||
@@ -113,12 +134,6 @@ jobs:
|
||||
# but Speckle Automate does
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
id: speckle
|
||||
name: Register Speckle Function
|
||||
uses: actions/speckle-automate-github-action@0.1.0
|
||||
with:
|
||||
speckle_token: ${{ secrets.SPECKLE_TOKEN }}
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
@@ -143,7 +158,17 @@ jobs:
|
||||
# ## platforms must match the platforms that you have registered with Speckle Automate, which also defaults to linux/amd64.
|
||||
# platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.speckle.outputs.image_name }}
|
||||
# The name of the image to build and push.
|
||||
# This must be the automate url host, followed by the function id, followed by the release tag.
|
||||
tags: automate.speckle.dev/abcdefghij:1.0.0
|
||||
-
|
||||
id: speckle
|
||||
name: Register Speckle Function
|
||||
uses: actions/speckle-automate-github-action@0.1.0
|
||||
with:
|
||||
speckle_token: ${{ secrets.SPECKLE_TOKEN }}
|
||||
speckle_function_id: "abcdefghij"
|
||||
speckle_function_release_tag: "1.0.0"
|
||||
```
|
||||
|
||||
## Developing & Debugging
|
||||
|
||||
+11
-4
@@ -21,11 +21,18 @@ inputs:
|
||||
speckle_function_command:
|
||||
description: 'The command to run to execute the function in a runtime environment.'
|
||||
required: true
|
||||
speckle_function_release_tag:
|
||||
description: 'User defined tag for the function release'
|
||||
required: true
|
||||
speckle_function_recommended_cpu_m:
|
||||
description: 'The recommended maximum CPU in millicores for the function. 1000 millicores = 1 CPU core. Defaults to 1000 millicores (1 CPU core). If the Function exceeds this limit, it will be throttled to run within the limit.'
|
||||
required: false
|
||||
speckle_function_recommended_memory_mi:
|
||||
description: 'The recommended maximum memory in mebibytes for the function. 1024 mebibytes = 1 gibibyte. Defaults to 100 mebibytes. If the Function exceeds this limit, it will be terminated.'
|
||||
required: false
|
||||
outputs:
|
||||
version_id:
|
||||
description: 'The unique identifier of the function version.'
|
||||
speckle_automate_host:
|
||||
description: 'The host component of the Speckle Automate Server URL.'
|
||||
speckle_automate_function_release_id:
|
||||
description: 'The unique identifier of the function release.'
|
||||
runs:
|
||||
using: 'node16' #FIXME bump to node18 when available
|
||||
main: 'dist/action/index.js'
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+24600
-322
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+20
-19
@@ -16,38 +16,39 @@
|
||||
"precommit": "pre-commit run --all-files",
|
||||
"prettier:check": "prettier --check '**/*.ts'",
|
||||
"prettier:fix": "prettier --write '**/*.ts'",
|
||||
"test": "vitest --run --coverage",
|
||||
"test": "vitest --run --coverage",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.19.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.0",
|
||||
"@actions/core": "^1.10.1",
|
||||
"@lifeomic/attempt": "^3.0.3",
|
||||
"node-fetch": "^3.3.2",
|
||||
"zod": "^3.21.4"
|
||||
"zod": "^3.22.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/node": "^18.17.4",
|
||||
"@typescript-eslint/eslint-plugin": "^6.3.0",
|
||||
"@typescript-eslint/parser": "^6.3.0",
|
||||
"@vercel/ncc": "^0.36.1",
|
||||
"@vitest/coverage-istanbul": "^0.34.1",
|
||||
"eslint": "^8.46.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.5",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^18.19.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
||||
"@typescript-eslint/parser": "^6.14.0",
|
||||
"@vercel/ncc": "^0.38.1",
|
||||
"@vitest/coverage-istanbul": "^1.0.4",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||
"eslint-plugin-filenames": "latest",
|
||||
"eslint-plugin-github": "^4.9.2",
|
||||
"eslint-plugin-github": "^4.10.1",
|
||||
"eslint-plugin-i18n-text": "^1.0.1",
|
||||
"eslint-plugin-import": "^2.28.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-no-only-tests": "^3.1.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-vitest": "^0.2.8",
|
||||
"prettier": "^3.0.1",
|
||||
"typescript": "^5.1.6",
|
||||
"vite": "^4.4.9",
|
||||
"vitest": "^0.34.1"
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"eslint-plugin-vitest": "^0.3.20",
|
||||
"msw": "^2.0.11",
|
||||
"prettier": "^3.1.1",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.11",
|
||||
"vitest": "^1.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
+106
-39
@@ -1,17 +1,36 @@
|
||||
import * as core from '@actions/core'
|
||||
import { z } from 'zod'
|
||||
import { ZodError, z } from 'zod'
|
||||
import fetch from 'node-fetch'
|
||||
import { retry } from '@lifeomic/attempt'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
const InputVariablesSchema = z.object({
|
||||
speckleAutomateUrl: z.string().url().nonempty(),
|
||||
speckleToken: z.string().nonempty(),
|
||||
speckleFunctionId: z.string().nonempty(),
|
||||
speckleFunctionInputSchema: z.record(z.string().nonempty(), z.unknown()).nullable(),
|
||||
speckleFunctionCommand: z.string().nonempty().array(),
|
||||
speckleFunctionReleaseTag: z.string().max(10).nonempty()
|
||||
speckleAutomateUrl: z.string().url().min(1),
|
||||
speckleToken: z.string().min(1),
|
||||
speckleFunctionId: z.string().min(1),
|
||||
speckleFunctionInputSchema: z.record(z.string().min(1), z.unknown()).nullable(),
|
||||
speckleFunctionCommand: z.array(z.string().min(1)),
|
||||
speckleFunctionReleaseTag: z
|
||||
.string()
|
||||
.regex(
|
||||
new RegExp('^[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}$'),
|
||||
'A maximum of 128 characters are permitted. The first character must be alphanumeric (of lower or upper case) or an underscore, the subsequent characters may be alphanumeric (or lower or upper case), underscore, hyphen, or period.'
|
||||
),
|
||||
speckleFunctionRecommendedCPUm: z
|
||||
.number()
|
||||
.int()
|
||||
.finite()
|
||||
.gte(100)
|
||||
.lte(16000)
|
||||
.optional(),
|
||||
speckleFunctionRecommendedMemoryMi: z
|
||||
.number()
|
||||
.int()
|
||||
.finite()
|
||||
.gte(1)
|
||||
.lte(8000)
|
||||
.optional()
|
||||
})
|
||||
|
||||
type InputVariables = z.infer<typeof InputVariablesSchema>
|
||||
@@ -20,20 +39,16 @@ const parseInputs = (): InputVariables => {
|
||||
const speckleTokenRaw = core.getInput('speckle_token', { required: true })
|
||||
core.setSecret(speckleTokenRaw)
|
||||
|
||||
const rawInputSchemaPath = core.getInput('speckle_function_input_schema_file_path')
|
||||
const homeDir = process.env['HOME']
|
||||
if (!homeDir)
|
||||
throw new Error('The home directory is not defined, cannot load inputSchema')
|
||||
let speckleFunctionInputSchema: Record<string, unknown> | null = null
|
||||
try {
|
||||
const rawInputSchemaPath = core.getInput('speckle_function_input_schema_file_path')
|
||||
const homeDir = process.env['HOME']
|
||||
if (!homeDir)
|
||||
throw new Error('The home directory is not defined, cannot load inputSchema')
|
||||
if (rawInputSchemaPath) {
|
||||
const rawInputSchema = readFileSync(join(homeDir, rawInputSchemaPath), 'utf-8')
|
||||
speckleFunctionInputSchema = JSON.parse(rawInputSchema)
|
||||
}
|
||||
} catch (err) {
|
||||
core.setFailed(`Parsing the function input schema failed with: ${err}`)
|
||||
throw err
|
||||
if (rawInputSchemaPath) {
|
||||
const rawInputSchema = readFileSync(join(homeDir, rawInputSchemaPath), 'utf-8')
|
||||
speckleFunctionInputSchema = JSON.parse(rawInputSchema)
|
||||
}
|
||||
|
||||
const rawInputs: InputVariables = {
|
||||
speckleAutomateUrl: core.getInput('speckle_automate_url', { required: true }),
|
||||
speckleToken: speckleTokenRaw,
|
||||
@@ -44,13 +59,20 @@ const parseInputs = (): InputVariables => {
|
||||
.split(' '),
|
||||
speckleFunctionReleaseTag: core.getInput('speckle_function_release_tag', {
|
||||
required: true
|
||||
})
|
||||
}),
|
||||
speckleFunctionRecommendedCPUm:
|
||||
parseInt(
|
||||
core.getInput('speckle_function_recommended_cpu_m', {
|
||||
required: false
|
||||
})
|
||||
) || undefined,
|
||||
speckleFunctionRecommendedMemoryMi:
|
||||
parseInt(
|
||||
core.getInput('speckle_function_recommended_memory_mi', { required: false })
|
||||
) || undefined
|
||||
}
|
||||
const inputParseResult = InputVariablesSchema.safeParse(rawInputs)
|
||||
if (inputParseResult.success) return inputParseResult.data
|
||||
core.setFailed(
|
||||
`The provided inputs do not match the required schema, ${inputParseResult.error.message}`
|
||||
)
|
||||
throw inputParseResult.error
|
||||
}
|
||||
|
||||
@@ -65,9 +87,6 @@ const parseEnvVars = (): RequiredEnvVars => {
|
||||
gitCommitSha: process.env.GITHUB_SHA
|
||||
} as RequiredEnvVars)
|
||||
if (parseResult.success) return parseResult.data
|
||||
core.setFailed(
|
||||
`The current execution environment does not have the required variables: ${parseResult.error.message}`
|
||||
)
|
||||
throw parseResult.error
|
||||
}
|
||||
|
||||
@@ -76,6 +95,8 @@ type FunctionVersionRequestBody = {
|
||||
versionTag: string
|
||||
command: string[]
|
||||
inputSchema: Record<string, unknown> | null
|
||||
recommendedCPUm?: number
|
||||
recommendedMemoryMi?: number
|
||||
}
|
||||
|
||||
const FunctionVersionResponseBodySchema = z.object({
|
||||
@@ -91,7 +112,9 @@ const registerNewVersionForTheSpeckleAutomateFunction = async (
|
||||
speckleFunctionId,
|
||||
speckleFunctionInputSchema,
|
||||
speckleToken,
|
||||
speckleFunctionReleaseTag
|
||||
speckleFunctionReleaseTag,
|
||||
speckleFunctionRecommendedCPUm,
|
||||
speckleFunctionRecommendedMemoryMi
|
||||
}: InputVariables,
|
||||
commitId: string
|
||||
// { gitCommitSha, gitRefName, gitRefType }: RequiredEnvVars
|
||||
@@ -101,7 +124,9 @@ const registerNewVersionForTheSpeckleAutomateFunction = async (
|
||||
commitId,
|
||||
versionTag: speckleFunctionReleaseTag,
|
||||
command: speckleFunctionCommand,
|
||||
inputSchema: speckleFunctionInputSchema
|
||||
inputSchema: speckleFunctionInputSchema,
|
||||
recommendedCPUm: speckleFunctionRecommendedCPUm,
|
||||
recommendedMemoryMi: speckleFunctionRecommendedMemoryMi
|
||||
}
|
||||
const versionRegisterUrl = new URL(
|
||||
`/api/v1/functions/${speckleFunctionId}/versions`,
|
||||
@@ -144,20 +169,55 @@ const registerNewVersionForTheSpeckleAutomateFunction = async (
|
||||
}
|
||||
}
|
||||
)
|
||||
return FunctionVersionResponseBodySchema.parse(response)
|
||||
const parsedResult = FunctionVersionResponseBodySchema.safeParse(response)
|
||||
if (parsedResult.success) return parsedResult.data
|
||||
throw parsedResult.error
|
||||
} catch (err) {
|
||||
core.setFailed(
|
||||
`Failed to register new function version to the automate server: ${err}`
|
||||
if (err instanceof Error) {
|
||||
throw Error(
|
||||
`Failed to register new function version to the automate server. ${err.message}`,
|
||||
{
|
||||
cause: err
|
||||
}
|
||||
)
|
||||
}
|
||||
throw Error(
|
||||
`Failed to register new function version to the automate server. ${err}`,
|
||||
{
|
||||
cause: err
|
||||
}
|
||||
)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
const failAndReject = async (
|
||||
e: unknown,
|
||||
errorMessageForUnknownObjectType: string
|
||||
): Promise<never> => {
|
||||
if (e instanceof ZodError || e instanceof Error) {
|
||||
core.setFailed(e.message)
|
||||
return Promise.reject(e.message)
|
||||
}
|
||||
core.setFailed(errorMessageForUnknownObjectType)
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
export async function run(): Promise<void> {
|
||||
core.info('Start registering a new version on the automate instance')
|
||||
const inputVariables = parseInputs()
|
||||
let inputVariables: InputVariables = {} as InputVariables
|
||||
try {
|
||||
inputVariables = parseInputs()
|
||||
} catch (e: unknown) {
|
||||
return failAndReject(e, 'Failed to parse the input variables')
|
||||
}
|
||||
core.info(`Parsed input variables to: ${JSON.stringify(inputVariables)}`)
|
||||
const requiredEnvVars = parseEnvVars()
|
||||
let requiredEnvVars: RequiredEnvVars = {} as RequiredEnvVars
|
||||
try {
|
||||
requiredEnvVars = parseEnvVars()
|
||||
} catch (e: unknown) {
|
||||
return failAndReject(e, 'Failed to parse the required environment variables')
|
||||
}
|
||||
|
||||
const { gitCommitSha } = requiredEnvVars
|
||||
core.info(
|
||||
`Parsed required environment variables to: ${JSON.stringify(requiredEnvVars)}`
|
||||
@@ -172,13 +232,20 @@ export async function run(): Promise<void> {
|
||||
// github uses 7 chars to identify commits
|
||||
const commitId = gitCommitSha.substring(0, 7)
|
||||
|
||||
const { versionId } = await registerNewVersionForTheSpeckleAutomateFunction(
|
||||
inputVariables,
|
||||
commitId
|
||||
)
|
||||
let versionId = ''
|
||||
try {
|
||||
const registrationResponse = await registerNewVersionForTheSpeckleAutomateFunction(
|
||||
inputVariables,
|
||||
commitId
|
||||
)
|
||||
versionId = registrationResponse.versionId
|
||||
} catch (e: unknown) {
|
||||
return failAndReject(e, 'Failed to register the new function version')
|
||||
}
|
||||
core.info(
|
||||
`Registered function version tagged as ${inputVariables.speckleFunctionReleaseTag} with new id: ${versionId}`
|
||||
`Registered function version tagged as ${inputVariables.speckleFunctionReleaseTag} with new id: ${versionId}. Recommended CPU: ${inputVariables.speckleFunctionRecommendedCPUm}m, recommended memory: ${inputVariables.speckleFunctionRecommendedMemoryMi}Mi.`
|
||||
)
|
||||
core.setOutput('speckle_automate_function_release_id', versionId)
|
||||
}
|
||||
|
||||
run()
|
||||
|
||||
+282
-6
@@ -1,16 +1,292 @@
|
||||
import { run } from '../src/main.js'
|
||||
import { describe, it, vi } from 'vitest'
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
vi,
|
||||
afterEach,
|
||||
beforeEach,
|
||||
expect,
|
||||
beforeAll,
|
||||
afterAll
|
||||
} from 'vitest'
|
||||
import { mkdtempSync, writeFileSync, rmdirSync, rmSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { setupServer } from 'msw/node'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { z } from 'zod'
|
||||
|
||||
describe('Register new version', () => {
|
||||
it('send the request', async () => {
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_ID', '{fake}')
|
||||
let tmpDir: string
|
||||
let countHappyPath = 0
|
||||
let count500Errors = 0
|
||||
let count422Errors = 0
|
||||
|
||||
const error422 = {
|
||||
type: 'H3Error',
|
||||
message: 'Body parsing failed',
|
||||
stack: `Error: Body parsing failed
|
||||
at createError...`,
|
||||
statusCode: 422,
|
||||
fatal: false,
|
||||
unhandled: false,
|
||||
statusMessage: 'Body parsing failed',
|
||||
data: {
|
||||
type: 'ZodError',
|
||||
message:
|
||||
'[\n {\n "code": "custom",\n "message": "Invalid JSON schema: strict mode: unknown keyword: \\"IAmInvalid\\"",\n "path": [\n "inputSchema"\n ]\n }\n]',
|
||||
stack: {
|
||||
ZodError: [
|
||||
{
|
||||
code: 'custom',
|
||||
message: 'Invalid JSON schema: strict mode: unknown keyword: "IAmInvalid"',
|
||||
path: ['inputSchema']
|
||||
}
|
||||
]
|
||||
},
|
||||
aggregateErrors: [
|
||||
{
|
||||
type: 'Object',
|
||||
message: 'Invalid JSON schema: strict mode: unknown keyword: "IAmInvalid"',
|
||||
stack: {},
|
||||
code: 'custom',
|
||||
path: ['inputSchema']
|
||||
}
|
||||
],
|
||||
issues: [
|
||||
{
|
||||
code: 'custom',
|
||||
message: 'Invalid JSON schema: strict mode: unknown keyword: "IAmInvalid"',
|
||||
path: ['inputSchema']
|
||||
}
|
||||
],
|
||||
name: 'ZodError'
|
||||
}
|
||||
}
|
||||
|
||||
const server = setupServer(
|
||||
http.post(
|
||||
'http://myfakeautomate.speckle.internal/api/v1/functions/fake_function_id/versions',
|
||||
async ({ request }) => {
|
||||
const parseResult = FunctionVersionRequestSchema.safeParse(await request.json())
|
||||
expect(parseResult.success).to.be.true
|
||||
countHappyPath++
|
||||
return new HttpResponse(JSON.stringify({ versionId: 'fake_version_id' }), {
|
||||
status: 201,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
}
|
||||
),
|
||||
http.post(
|
||||
'http://myfakeautomate.speckle.internal/api/v1/functions/network_error/versions',
|
||||
async ({ request }) => {
|
||||
const parseResult = FunctionVersionRequestSchema.safeParse(await request.json())
|
||||
expect(parseResult.success).to.be.true
|
||||
return HttpResponse.error() // simulates a network error
|
||||
}
|
||||
),
|
||||
http.post(
|
||||
'http://myfakeautomate.speckle.internal/api/v1/functions/422_response/versions',
|
||||
async ({ request }) => {
|
||||
const parseResult = FunctionVersionRequestSchema.safeParse(await request.json())
|
||||
expect(parseResult.success).to.be.true
|
||||
count422Errors++
|
||||
return HttpResponse.json(error422, {
|
||||
status: 422
|
||||
})
|
||||
}
|
||||
),
|
||||
http.post(
|
||||
'http://myfakeautomate.speckle.internal/api/v1/functions/500_response/versions',
|
||||
async ({ request }) => {
|
||||
const parseResult = FunctionVersionRequestSchema.safeParse(await request.json())
|
||||
expect(parseResult.success).to.be.true
|
||||
count500Errors++
|
||||
return HttpResponse.json(
|
||||
{},
|
||||
{
|
||||
status: 500
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
|
||||
|
||||
afterAll(() => server.close())
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = mkdtempSync(join(tmpdir(), 'speckle-automate-github-action-test-'))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(tmpDir, { recursive: true })
|
||||
vi.unstubAllEnvs()
|
||||
server.resetHandlers()
|
||||
})
|
||||
|
||||
it('sends the request', async () => {
|
||||
writeFileSync(join(tmpDir, 'schema.json'), '{}')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_ID', 'fake_function_id')
|
||||
vi.stubEnv('INPUT_SPECKLE_TOKEN', '{token}')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_COMMAND', 'echo "hello automate"')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_INPUT_SCHEMA', '{}')
|
||||
vi.stubEnv('INPUT_SPECKLE_AUTOMATE_URL', 'http://automate.speckle.internal:3030')
|
||||
vi.stubEnv('HOME', tmpDir) // the input schema file path is assumed to be relative to the home directory
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_INPUT_SCHEMA_FILE_PATH', './schema.json')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_RELEASE_TAG', 'v1.0.0')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_RECOMMENDED_CPU_M', '1000')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_RECOMMENDED_MEMORY_MI', '500')
|
||||
vi.stubEnv('INPUT_SPECKLE_AUTOMATE_URL', 'http://myfakeautomate.speckle.internal')
|
||||
vi.stubEnv('GITHUB_SHA', 'commitSha')
|
||||
vi.stubEnv('GITHUB_REF_TYPE', 'commit')
|
||||
vi.stubEnv('GITHUB_REF_NAME', 'version')
|
||||
await run()
|
||||
await expect(run()).resolves.not.toThrow()
|
||||
expect(countHappyPath).to.equal(1)
|
||||
countHappyPath = 0
|
||||
})
|
||||
it('handles network errors', async () => {
|
||||
writeFileSync(join(tmpDir, 'schema.json'), '{}')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_ID', 'network_error')
|
||||
vi.stubEnv('INPUT_SPECKLE_TOKEN', '{token}')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_COMMAND', 'echo "hello automate"')
|
||||
vi.stubEnv('HOME', tmpDir) // the input schema file path is assumed to be relative to the home directory
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_INPUT_SCHEMA_FILE_PATH', './schema.json')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_RELEASE_TAG', 'v1.0.0')
|
||||
vi.stubEnv('INPUT_SPECKLE_AUTOMATE_URL', 'http://myfakeautomate.speckle.internal')
|
||||
vi.stubEnv('GITHUB_SHA', 'commitSha')
|
||||
vi.stubEnv('GITHUB_REF_TYPE', 'commit')
|
||||
vi.stubEnv('GITHUB_REF_NAME', 'version')
|
||||
await expect(run()).rejects.toThrow(
|
||||
'Failed to register new function version to the automate server'
|
||||
)
|
||||
})
|
||||
it('handles 500 responses', async () => {
|
||||
writeFileSync(join(tmpDir, 'schema.json'), '{}')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_ID', '500_response')
|
||||
vi.stubEnv('INPUT_SPECKLE_TOKEN', '{token}')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_COMMAND', 'echo "hello automate"')
|
||||
vi.stubEnv('HOME', tmpDir) // the input schema file path is assumed to be relative to the home directory
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_INPUT_SCHEMA_FILE_PATH', './schema.json')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_RELEASE_TAG', 'v1.0.0')
|
||||
vi.stubEnv('INPUT_SPECKLE_AUTOMATE_URL', 'http://myfakeautomate.speckle.internal')
|
||||
vi.stubEnv('GITHUB_SHA', 'commitSha')
|
||||
vi.stubEnv('GITHUB_REF_TYPE', 'commit')
|
||||
vi.stubEnv('GITHUB_REF_NAME', 'version')
|
||||
await expect(run()).rejects.toThrow(
|
||||
'Failed to register new function version to the automate server'
|
||||
)
|
||||
expect(count500Errors).to.toBeGreaterThan(1) // we expect the action to retry the request
|
||||
count500Errors = 0
|
||||
})
|
||||
it('handles 422 responses', async () => {
|
||||
writeFileSync(join(tmpDir, 'schema.json'), '{}')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_ID', '422_response')
|
||||
vi.stubEnv('INPUT_SPECKLE_TOKEN', '{token}')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_COMMAND', 'echo "hello automate"')
|
||||
vi.stubEnv('HOME', tmpDir) // the input schema file path is assumed to be relative to the home directory
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_INPUT_SCHEMA_FILE_PATH', './schema.json')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_RELEASE_TAG', 'v1.0.0')
|
||||
vi.stubEnv('INPUT_SPECKLE_AUTOMATE_URL', 'http://myfakeautomate.speckle.internal')
|
||||
vi.stubEnv('GITHUB_SHA', 'commitSha')
|
||||
vi.stubEnv('GITHUB_REF_TYPE', 'commit')
|
||||
vi.stubEnv('GITHUB_REF_NAME', 'version')
|
||||
await expect(run()).rejects.toThrow(
|
||||
'Failed to register new function version to the automate server'
|
||||
)
|
||||
expect(count422Errors).to.eq(1) // we expect the action not to retry the request
|
||||
count422Errors = 0 // reset the count after the test
|
||||
})
|
||||
it('errors if the token is empty', async () => {
|
||||
writeFileSync(join(tmpDir, 'schema.json'), '{}')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_ID', 'fake_function_id')
|
||||
vi.stubEnv('INPUT_SPECKLE_TOKEN', '')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_COMMAND', 'echo "hello automate"')
|
||||
vi.stubEnv('HOME', tmpDir) // the input schema file path is assumed to be relative to the home directory
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_INPUT_SCHEMA_FILE_PATH', './schema.json')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_RELEASE_TAG', 'v1.0.0')
|
||||
vi.stubEnv('INPUT_SPECKLE_AUTOMATE_URL', 'http://myfakeautomate.speckle.internal')
|
||||
vi.stubEnv('GITHUB_SHA', 'commitSha')
|
||||
vi.stubEnv('GITHUB_REF_TYPE', 'commit')
|
||||
vi.stubEnv('GITHUB_REF_NAME', 'version')
|
||||
await expect(run()).rejects.toThrow(
|
||||
'Input required and not supplied: speckle_token'
|
||||
)
|
||||
})
|
||||
it('errors if the environment variable is empty', async () => {
|
||||
writeFileSync(join(tmpDir, 'schema.json'), '{}')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_ID', 'fake_function_id')
|
||||
vi.stubEnv('INPUT_SPECKLE_TOKEN', '{token}')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_COMMAND', 'echo "hello automate"')
|
||||
vi.stubEnv('HOME', tmpDir) // the input schema file path is assumed to be relative to the home directory
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_INPUT_SCHEMA_FILE_PATH', './schema.json')
|
||||
vi.stubEnv('INPUT_SPECKLE_FUNCTION_RELEASE_TAG', 'v1.0.0')
|
||||
vi.stubEnv('INPUT_SPECKLE_AUTOMATE_URL', 'http://myfakeautomate.speckle.internal')
|
||||
vi.stubEnv('GITHUB_SHA', '')
|
||||
vi.stubEnv('GITHUB_REF_TYPE', 'commit')
|
||||
vi.stubEnv('GITHUB_REF_NAME', 'version')
|
||||
await expect(run()).rejects.toThrow('gitCommitSha')
|
||||
})
|
||||
})
|
||||
|
||||
//This must be updated to align with the schema in speckle automate
|
||||
const FunctionVersionRequestSchema = z.object({
|
||||
commitId: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(6)
|
||||
.transform((arg: string) => arg.substring(0, 10)),
|
||||
versionTag: z
|
||||
.string()
|
||||
.regex(
|
||||
new RegExp('^[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}$'),
|
||||
'A maximum of 128 characters are permitted. The first character must be alphanumeric (of lower or upper case) or an underscore, the subsequent characters may be alphanumeric (or lower or upper case), underscore, hyphen, or period.'
|
||||
), // regex as per OCI distribution spec https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests
|
||||
inputSchema: z.record(z.string(), z.unknown()).nullable(), // TODO: we need to validate the jsonschema somehow
|
||||
command: z.array(z.string().nonempty()),
|
||||
annotations: z
|
||||
.object({
|
||||
'speckle.systems/v1alpha1/publishing/status': z
|
||||
.enum(['publish', 'draft', 'archive'], {
|
||||
description:
|
||||
'Whether this Function is published (and should appear in the library), a draft, or archived.'
|
||||
})
|
||||
.default('draft'),
|
||||
'speckle.systems/v1alpha1/author': z
|
||||
.string({
|
||||
description:
|
||||
'The name of the authoring organization or individual of this Function.'
|
||||
})
|
||||
.optional(),
|
||||
'speckle.systems/v1alpha1/license': z
|
||||
.enum(['MIT', 'BSD', 'Apache-2.0', 'MPL', 'CC0', 'Unlicense'], {
|
||||
description:
|
||||
'The license under under which this Function is made available. This must match the license in the source code repository.'
|
||||
})
|
||||
.optional(), //TODO match the specification for license names
|
||||
'speckle.systems/v1alpha1/website': z
|
||||
.string({
|
||||
description: 'The marketing website for this Function or its authors.'
|
||||
})
|
||||
.url()
|
||||
.optional(),
|
||||
'speckle.systems/v1alpha1/documentation': z
|
||||
.string({
|
||||
description:
|
||||
'The documentation website for this function. For example, this could be a url to the README in the source code repository.'
|
||||
})
|
||||
.url()
|
||||
.optional(),
|
||||
'speckle.systems/v1alpha1/keywords': z
|
||||
.string({
|
||||
description:
|
||||
'Comma separated list of keywords used for categorizing this function.'
|
||||
})
|
||||
.optional(),
|
||||
'speckle.systems/v1alpha1/description': z.string().optional()
|
||||
})
|
||||
.optional(),
|
||||
recommendedCPUm: z.number().gte(100).lte(16000).finite().optional().default(1000),
|
||||
recommendedMemoryMi: z.number().gte(1).lte(8000).finite().optional().default(100)
|
||||
})
|
||||
|
||||
+4
-4
@@ -20,10 +20,10 @@ export default defineConfig({
|
||||
'**/*.mjs',
|
||||
'**/*.js'
|
||||
],
|
||||
lines: 95,
|
||||
functions: 95,
|
||||
branches: 95,
|
||||
statements: 95,
|
||||
lines: 90,
|
||||
functions: 90,
|
||||
branches: 70,
|
||||
statements: 90,
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src/')
|
||||
|
||||
Reference in New Issue
Block a user