diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 363bb15..f97637f 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -44,4 +44,5 @@ jobs: JAVASCRIPT_DEFAULT_STYLE: prettier VALIDATE_ALL_CODEBASE: true VALIDATE_JSCPD: false + VALIDATE_JAVASCRIPT_STANDARD: false VALIDATE_MARKDOWN: false diff --git a/.github/workflows/registered_runner.yml b/.github/workflows/registered_runner.yml index 5d1bb10..47b00a7 100644 --- a/.github/workflows/registered_runner.yml +++ b/.github/workflows/registered_runner.yml @@ -8,11 +8,14 @@ on: required: true type: string +permissions: + contents: read + jobs: test-runner: name: Test Runner runs-on: ${{ inputs.runner-name }} - timeout-minutes: 5 # Job should be picked very quickly + timeout-minutes: 5 # Job should be picked very quickly steps: - name: Runner registered run: | diff --git a/.github/workflows/removed_runner.yml b/.github/workflows/removed_runner.yml index 07d9de5..37918c5 100644 --- a/.github/workflows/removed_runner.yml +++ b/.github/workflows/removed_runner.yml @@ -14,6 +14,9 @@ on: READ_REPO_TOKEN: required: true +permissions: + contents: read + jobs: test-removed-runner: name: Test Removed Runner diff --git a/.github/workflows/test_spawn_terminate.yml b/.github/workflows/test_spawn_terminate.yml index 0a4c7bc..caae9a1 100644 --- a/.github/workflows/test_spawn_terminate.yml +++ b/.github/workflows/test_spawn_terminate.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - provider: [ aws, hyperstack ] + provider: [aws, hyperstack] fail-fast: false outputs: runner-aws: ${{ steps.gen-output.outputs.runner_aws }} @@ -45,14 +45,14 @@ jobs: test-runner-alive-aws: name: Test runner is alive (AWS) - needs: [ action-start ] + needs: [action-start] uses: ./.github/workflows/registered_runner.yml with: runner-name: ${{ needs.action-start.outputs.runner-aws }} test-runner-alive-hyperstack: name: Test runner is alive (Hyperstack) - needs: [ action-start ] + needs: [action-start] uses: ./.github/workflows/registered_runner.yml with: runner-name: ${{ needs.action-start.outputs.runner-hyperstack }} @@ -60,12 +60,15 @@ jobs: action-stop: name: GitHub Actions Test (terminate) runs-on: ubuntu-latest - needs: [ action-start, test-runner-alive-aws, test-runner-alive-hyperstack ] + needs: [action-start, test-runner-alive-aws, test-runner-alive-hyperstack] if: ${{ always() && needs.action-start.result != 'skipped' }} strategy: matrix: - runner: [ "${{ needs.action-start.outputs.runner-aws }}", - "${{ needs.action-start.outputs.runner-hyperstack }}" ] + runner: + [ + '${{ needs.action-start.outputs.runner-aws }}', + '${{ needs.action-start.outputs.runner-hyperstack }}' + ] fail-fast: false steps: - name: Checkout @@ -84,7 +87,7 @@ jobs: test-runner-removed-aws: name: Test runner is removed (AWS) - needs: [ action-start, action-stop ] + needs: [action-start, action-stop] uses: ./.github/workflows/removed_runner.yml with: runner-name: ${{ needs.action-start.outputs.runner-aws }} @@ -94,7 +97,7 @@ jobs: test-runner-removed-hyperstack: name: Test runner is removed (Hyperstack) - needs: [ action-start, action-stop ] + needs: [action-start, action-stop] uses: ./.github/workflows/removed_runner.yml with: runner-name: ${{ needs.action-start.outputs.runner-hyperstack }} diff --git a/.github/workflows/test_start_stop.yml b/.github/workflows/test_start_stop.yml index 91acf6a..f10168b 100644 --- a/.github/workflows/test_start_stop.yml +++ b/.github/workflows/test_start_stop.yml @@ -24,7 +24,7 @@ jobs: action-start: name: GitHub Actions Test (start) runs-on: ubuntu-latest - needs: [ test-runner-exist ] + needs: [test-runner-exist] steps: - name: Checkout id: checkout @@ -43,7 +43,7 @@ jobs: test-runner-alive: name: Test runner is alive - needs: [ action-start ] + needs: [action-start] uses: ./.github/workflows/registered_runner.yml with: runner-name: ci-persistent-runner @@ -51,7 +51,7 @@ jobs: action-stop: name: GitHub Actions Test (stop) runs-on: ubuntu-latest - needs: [ action-start, test-runner-alive ] + needs: [action-start, test-runner-alive] if: ${{ always() && needs.action-start.result != 'skipped' }} steps: - name: Checkout @@ -70,7 +70,7 @@ jobs: test-runner-persist: name: Test runner is still registered - needs: [ action-stop ] + needs: [action-stop] uses: ./.github/workflows/removed_runner.yml with: runner-name: ci-persistent-runner diff --git a/README.md b/README.md index b60db35..113a939 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # On-demand self-hosted runner for GitHub Actions -Start your EC2 [self-hosted runner](https://docs.github.com/en/free-pro-team@latest/actions/hosting-your-own-runners) -right before you need it. -Run the job on it. Finally, stop it when you finish. +Start your EC2 +[self-hosted runner](https://docs.github.com/en/free-pro-team@latest/actions/hosting-your-own-runners) +right before you need it. Run the job on it. Finally, stop it when you finish. And all this automatically as a part of your GitHub Actions workflow. It relies on Slab CI bot to do all the heavy-lifting. @@ -22,7 +22,7 @@ See [below](#example) the YAML code of the depicted workflow. ### Inputs | Name | Required | Description | -|----------------|-----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| -------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `mode` | Always required. | Specify here which mode you want to use: `start` to start a new runner, `stop` to stop the previously created runner. | | `github-token` | Always required. | GitHub Personal Access Token with the `repo` scope assigned. | | `slab-url` | Always required. | URL to Slab CI server. | @@ -33,13 +33,14 @@ See [below](#example) the YAML code of the depicted workflow. ### Outputs -| Name | Description | -|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `label` | Name of the unique label assigned to the runner. The label is used in two cases: to use as the input of `runs-on` property for the following jobs and to remove the runner from GitHub when it is not needed anymore. | +| Name | Description | +| ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `label` | Name of the unique label assigned to the runner. The label is used in two cases: to use as the input of `runs-on` property for the following jobs and to remove the runner from GitHub when it is not needed anymore. | ### Examples -Here's an example workflow. It uses a backend profile declared in `ci/slab.toml` within the calling repository +Here's an example workflow. It uses a backend profile declared in `ci/slab.toml` +within the calling repository ```yml name: do-the-job @@ -62,7 +63,7 @@ jobs: profile: cpu-test do-the-job: - # ... # + # ... # stop-runner: name: Stop self-hosted EC2 runner diff --git a/action.yaml b/action.yaml index 041f8d3..7b5e41f 100644 --- a/action.yaml +++ b/action.yaml @@ -1,5 +1,7 @@ name: On-demand self-hosted runner for GitHub Actions -description: GitHub Action for automatic AWS EC2 instance provisioning as a GitHub Actions self-hosted runner. +description: + GitHub Action for automatic AWS EC2 instance provisioning as a GitHub Actions + self-hosted runner. author: zama-ai branding: icon: 'box' @@ -25,27 +27,26 @@ inputs: required: true backend: description: >- - Backend provider name to look for in slab.toml file in repository that uses the action. - This input is required if you use the 'start' mode. + Backend provider name to look for in slab.toml file in repository that + uses the action. This input is required if you use the 'start' mode. required: false profile: description: >- - Profile to use as described slab.toml file in repository that uses the action. - This input is required if you use the 'start' mode. + Profile to use as described slab.toml file in repository that uses the + action. This input is required if you use the 'start' mode. required: false label: description: >- - Name of the unique label assigned to the runner. - The label is used to remove the runner from GitHub when the runner is not needed anymore. - This input is required if you use the 'stop' mode. + Name of the unique label assigned to the runner. The label is used to + remove the runner from GitHub when the runner is not needed anymore. This + input is required if you use the 'stop' mode. required: false outputs: label: description: >- - Name of the unique label assigned to the runner. - The label is used in two cases: - - to use as the input of 'runs-on' property for the following jobs; + Name of the unique label assigned to the runner. The label is used in two + cases: - to use as the input of 'runs-on' property for the following jobs; - to remove the runner from GitHub when it is not needed anymore. runs: using: node20 diff --git a/dist/index.js b/dist/index.js index a8f12e7..23828ef 100644 --- a/dist/index.js +++ b/dist/index.js @@ -49806,31 +49806,31 @@ class Config { // if (!this.input.mode) { - throw new Error(`The 'mode' input is not specified`) + throw new Error("The 'mode' input is not specified") } if (!this.input.githubToken) { - throw new Error(`The 'github-token' input is not specified`) + throw new Error("The 'github-token' input is not specified") } if (!this.input.slabUrl) { - throw new Error(`The 'slab-url' input is not specified`) + throw new Error("The 'slab-url' input is not specified") } if (!this.input.jobSecret) { - throw new Error(`The 'job-secret' input is not specified`) + throw new Error("The 'job-secret' input is not specified") } if (this.input.mode === 'start') { if (!this.input.backend || !this.input.profile) { throw new Error( - `Not all the required inputs are provided for the 'start' mode` + "Not all the required inputs are provided for the 'start' mode" ) } } else if (this.input.mode === 'stop') { if (!this.input.label) { throw new Error( - `Not all the required inputs are provided for the 'stop' mode` + "Not all the required inputs are provided for the 'stop' mode" ) } } else { @@ -49935,7 +49935,7 @@ function getSignature(content) { return hmac.digest('hex') } -function concat_path(url, path) { +function concatPath(url, path) { if (url.endsWith('/')) { // Multiple '/' char at the end of URL is fine. return url.concat(path) @@ -49969,7 +49969,7 @@ async function startInstanceRequest() { core.info(`Request ${provider} instance start`) try { - response = await fetch(concat_path(url, 'job'), { + response = await fetch(concatPath(url, 'job'), { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -49980,7 +49980,7 @@ async function startInstanceRequest() { body: body.toString() }) } catch (error) { - core.error(`Fetch call has failed`) + core.error('Fetch call has failed') throw error } @@ -49988,9 +49988,9 @@ async function startInstanceRequest() { core.info(`${provider} instance start successfully requested`) return await response.json() } else { - const resp_body = await response.text() + const respBody = await response.text() core.error( - `${provider} instance start request has failed (HTTP status code: ${response.status}, body: ${resp_body})` + `${provider} instance start request has failed (HTTP status code: ${response.status}, body: ${respBody})` ) throw new Error('instance start request failed') } @@ -50012,7 +50012,7 @@ async function stopInstanceRequest(runnerName) { core.info(`Request instance stop (runner: ${runnerName})`) try { - response = await fetch(concat_path(url, 'job'), { + response = await fetch(concatPath(url, 'job'), { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -50031,9 +50031,9 @@ async function stopInstanceRequest(runnerName) { core.info('Instance stop successfully requested') return response.json() } else { - const resp_body = await response.text() + const respBody = await response.text() core.error( - `Instance stop request has failed (HTTP status code: ${response.status}, body: ${resp_body})` + `Instance stop request has failed (HTTP status code: ${response.status}, body: ${respBody})` ) throw new Error('instance stop request failed') } @@ -50051,15 +50051,15 @@ async function waitForInstance(taskId, taskName) { if (response.ok) { const body = await response.json() - const task_status = body[taskName].status.toLowerCase() + const taskStatus = body[taskName].status.toLowerCase() - if (task_status === 'done') { + if (taskStatus === 'done') { if (taskName === 'start') { await acknowledgeTaskDone(taskId) } await removeTask(taskId) return body - } else if (task_status === 'failed') { + } else if (taskStatus === 'failed') { core.error(`Instance task failed (details: ${body[taskName].details})`) core.error('Failure occurred while waiting for instance.') await removeTask(taskId) @@ -50080,7 +50080,7 @@ async function getTask(taskId) { let response try { - response = await fetch(concat_path(url, route)) + response = await fetch(concatPath(url, route)) } catch (error) { core.error(`Failed to fetch task status with ID: ${taskId}`) throw error @@ -50103,7 +50103,7 @@ async function removeTask(taskId) { let response try { - response = await fetch(concat_path(url, route), { + response = await fetch(concatPath(url, route), { method: 'DELETE' }) } catch (error) { @@ -50128,7 +50128,7 @@ async function acknowledgeTaskDone(taskId) { let response try { - response = await fetch(concat_path(url, route), { + response = await fetch(concatPath(url, route), { method: 'POST' }) } catch (error) { @@ -52102,19 +52102,18 @@ const slab = __nccwpck_require__(4156) const config = __nccwpck_require__(4570) const core = __nccwpck_require__(2186) const { waitForRunnerRegistered } = __nccwpck_require__(6989) -const utils = __nccwpck_require__(1608) function setOutput(label) { core.setOutput('label', label) } // This variable should only be defined for cleanup purpose. -let runner_name +let runnerName async function cleanup() { - if (runner_name) { + if (runnerName) { core.info('Stop instance after cancellation') - await slab.stopInstanceRequest(runner_name) + await slab.stopInstanceRequest(runnerName) } } @@ -52126,12 +52125,12 @@ process.on('SIGINT', async function () { async function start() { const provider = config.input.backend - let start_instance_response + let startInstanceResponse for (let i = 1; i <= 3; i++) { try { - start_instance_response = await slab.startInstanceRequest() - runner_name = start_instance_response.runner_name + startInstanceResponse = await slab.startInstanceRequest() + runnerName = startInstanceResponse.runner_name break } catch (error) { core.info('Retrying request now...') @@ -52144,49 +52143,47 @@ async function start() { } } - setOutput(start_instance_response.runner_name) + setOutput(startInstanceResponse.runner_name) core.info( `${provider} instance details: ${JSON.stringify( - start_instance_response.details + startInstanceResponse.details )}` ) try { - const wait_instance_response = await slab.waitForInstance( - start_instance_response.task_id, + const waitInstanceResponse = await slab.waitForInstance( + startInstanceResponse.task_id, 'start' ) - const instance_id = wait_instance_response.start.instance_id - core.info(`${provider} instance started with ID: ${instance_id}`) + const instanceId = waitInstanceResponse.start.instance_id + core.info(`${provider} instance started with ID: ${instanceId}`) - await waitForRunnerRegistered(start_instance_response.runner_name) + await waitForRunnerRegistered(startInstanceResponse.runner_name) } catch (error) { core.info(`Clean up after error, stop ${provider} instance`) - await slab.stopInstanceRequest(start_instance_response.runner_name) + await slab.stopInstanceRequest(startInstanceResponse.runner_name) } } async function stop() { - let stop_instance_response + let stopInstanceResponse for (let i = 1; i <= 3; i++) { try { - stop_instance_response = await slab.stopInstanceRequest( - config.input.label - ) + stopInstanceResponse = await slab.stopInstanceRequest(config.input.label) break } catch (error) { core.info('Retrying request now...') } if (i === 3) { - core.setFailed(`Instance stop request has failed after 3 attempts`) + core.setFailed('Instance stop request has failed after 3 attempts') } } - await slab.waitForInstance(stop_instance_response.task_id, 'stop') + await slab.waitForInstance(stopInstanceResponse.task_id, 'stop') core.info('Instance successfully stopped') } diff --git a/package.json b/package.json index 2025f12..aafd5a0 100644 --- a/package.json +++ b/package.json @@ -28,11 +28,13 @@ "ci-test": "jest --passWithNoTests", "format:write": "prettier --write **/*.js", "format:check": "prettier --check **/*.js", + "format:codebase": "prettier --write **/*js **/*.yml **/*.yaml **/*.md", + "format:standardjs": "standard --fix", "lint": "npx eslint . -c ./.github/linters/.eslintrc.yml", "package": "ncc build src/index.js --license licenses.txt", "package:watch": "npm run package -- --watch", "test": "jest --passWithNoTests", - "all": "npm run format:write && npm run lint && npm run test && npm run package" + "all": "npm run format:standardjs && npm run format:write && npm run lint && npm run test && npm run package" }, "eslintConfig": { "extends": "./.github/linters/.eslintrc.yml" diff --git a/src/config.js b/src/config.js index 6518b4a..0cf74f7 100644 --- a/src/config.js +++ b/src/config.js @@ -28,31 +28,31 @@ class Config { // if (!this.input.mode) { - throw new Error(`The 'mode' input is not specified`) + throw new Error("The 'mode' input is not specified") } if (!this.input.githubToken) { - throw new Error(`The 'github-token' input is not specified`) + throw new Error("The 'github-token' input is not specified") } if (!this.input.slabUrl) { - throw new Error(`The 'slab-url' input is not specified`) + throw new Error("The 'slab-url' input is not specified") } if (!this.input.jobSecret) { - throw new Error(`The 'job-secret' input is not specified`) + throw new Error("The 'job-secret' input is not specified") } if (this.input.mode === 'start') { if (!this.input.backend || !this.input.profile) { throw new Error( - `Not all the required inputs are provided for the 'start' mode` + "Not all the required inputs are provided for the 'start' mode" ) } } else if (this.input.mode === 'stop') { if (!this.input.label) { throw new Error( - `Not all the required inputs are provided for the 'stop' mode` + "Not all the required inputs are provided for the 'stop' mode" ) } } else { diff --git a/src/index.js b/src/index.js index b3daf6a..d5e5c38 100644 --- a/src/index.js +++ b/src/index.js @@ -2,19 +2,18 @@ const slab = require('./slab') const config = require('./config') const core = require('@actions/core') const { waitForRunnerRegistered } = require('./gh') -const utils = require('./utils') function setOutput(label) { core.setOutput('label', label) } // This variable should only be defined for cleanup purpose. -let runner_name +let runnerName async function cleanup() { - if (runner_name) { + if (runnerName) { core.info('Stop instance after cancellation') - await slab.stopInstanceRequest(runner_name) + await slab.stopInstanceRequest(runnerName) } } @@ -26,12 +25,12 @@ process.on('SIGINT', async function () { async function start() { const provider = config.input.backend - let start_instance_response + let startInstanceResponse for (let i = 1; i <= 3; i++) { try { - start_instance_response = await slab.startInstanceRequest() - runner_name = start_instance_response.runner_name + startInstanceResponse = await slab.startInstanceRequest() + runnerName = startInstanceResponse.runner_name break } catch (error) { core.info('Retrying request now...') @@ -44,49 +43,47 @@ async function start() { } } - setOutput(start_instance_response.runner_name) + setOutput(startInstanceResponse.runner_name) core.info( `${provider} instance details: ${JSON.stringify( - start_instance_response.details + startInstanceResponse.details )}` ) try { - const wait_instance_response = await slab.waitForInstance( - start_instance_response.task_id, + const waitInstanceResponse = await slab.waitForInstance( + startInstanceResponse.task_id, 'start' ) - const instance_id = wait_instance_response.start.instance_id - core.info(`${provider} instance started with ID: ${instance_id}`) + const instanceId = waitInstanceResponse.start.instance_id + core.info(`${provider} instance started with ID: ${instanceId}`) - await waitForRunnerRegistered(start_instance_response.runner_name) + await waitForRunnerRegistered(startInstanceResponse.runner_name) } catch (error) { core.info(`Clean up after error, stop ${provider} instance`) - await slab.stopInstanceRequest(start_instance_response.runner_name) + await slab.stopInstanceRequest(startInstanceResponse.runner_name) } } async function stop() { - let stop_instance_response + let stopInstanceResponse for (let i = 1; i <= 3; i++) { try { - stop_instance_response = await slab.stopInstanceRequest( - config.input.label - ) + stopInstanceResponse = await slab.stopInstanceRequest(config.input.label) break } catch (error) { core.info('Retrying request now...') } if (i === 3) { - core.setFailed(`Instance stop request has failed after 3 attempts`) + core.setFailed('Instance stop request has failed after 3 attempts') } } - await slab.waitForInstance(stop_instance_response.task_id, 'stop') + await slab.waitForInstance(stopInstanceResponse.task_id, 'stop') core.info('Instance successfully stopped') } diff --git a/src/slab.js b/src/slab.js index b8e84bd..ca44854 100644 --- a/src/slab.js +++ b/src/slab.js @@ -9,7 +9,7 @@ function getSignature(content) { return hmac.digest('hex') } -function concat_path(url, path) { +function concatPath(url, path) { if (url.endsWith('/')) { // Multiple '/' char at the end of URL is fine. return url.concat(path) @@ -43,7 +43,7 @@ async function startInstanceRequest() { core.info(`Request ${provider} instance start`) try { - response = await fetch(concat_path(url, 'job'), { + response = await fetch(concatPath(url, 'job'), { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -54,7 +54,7 @@ async function startInstanceRequest() { body: body.toString() }) } catch (error) { - core.error(`Fetch call has failed`) + core.error('Fetch call has failed') throw error } @@ -62,9 +62,9 @@ async function startInstanceRequest() { core.info(`${provider} instance start successfully requested`) return await response.json() } else { - const resp_body = await response.text() + const respBody = await response.text() core.error( - `${provider} instance start request has failed (HTTP status code: ${response.status}, body: ${resp_body})` + `${provider} instance start request has failed (HTTP status code: ${response.status}, body: ${respBody})` ) throw new Error('instance start request failed') } @@ -86,7 +86,7 @@ async function stopInstanceRequest(runnerName) { core.info(`Request instance stop (runner: ${runnerName})`) try { - response = await fetch(concat_path(url, 'job'), { + response = await fetch(concatPath(url, 'job'), { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -105,9 +105,9 @@ async function stopInstanceRequest(runnerName) { core.info('Instance stop successfully requested') return response.json() } else { - const resp_body = await response.text() + const respBody = await response.text() core.error( - `Instance stop request has failed (HTTP status code: ${response.status}, body: ${resp_body})` + `Instance stop request has failed (HTTP status code: ${response.status}, body: ${respBody})` ) throw new Error('instance stop request failed') } @@ -125,15 +125,15 @@ async function waitForInstance(taskId, taskName) { if (response.ok) { const body = await response.json() - const task_status = body[taskName].status.toLowerCase() + const taskStatus = body[taskName].status.toLowerCase() - if (task_status === 'done') { + if (taskStatus === 'done') { if (taskName === 'start') { await acknowledgeTaskDone(taskId) } await removeTask(taskId) return body - } else if (task_status === 'failed') { + } else if (taskStatus === 'failed') { core.error(`Instance task failed (details: ${body[taskName].details})`) core.error('Failure occurred while waiting for instance.') await removeTask(taskId) @@ -154,7 +154,7 @@ async function getTask(taskId) { let response try { - response = await fetch(concat_path(url, route)) + response = await fetch(concatPath(url, route)) } catch (error) { core.error(`Failed to fetch task status with ID: ${taskId}`) throw error @@ -177,7 +177,7 @@ async function removeTask(taskId) { let response try { - response = await fetch(concat_path(url, route), { + response = await fetch(concatPath(url, route), { method: 'DELETE' }) } catch (error) { @@ -202,7 +202,7 @@ async function acknowledgeTaskDone(taskId) { let response try { - response = await fetch(concat_path(url, route), { + response = await fetch(concatPath(url, route), { method: 'POST' }) } catch (error) {