diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 930905f..ba9ba7a 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -1,38 +1,286 @@ -name: Build & Test +name: 'CI: Lint, Build & Test' on: push: branches: [main] + paths: + - 'src/**' + - 'package.json' + - 'package-lock.json' + - 'tsconfig.json' + - 'eslint.config.mjs' + - 'action.yaml' + - 'dist/**' + - '.github/workflows/**' + pull_request: + branches: [main] + paths: + - 'src/**' + - 'package.json' + - 'package-lock.json' + - 'tsconfig.json' + - 'eslint.config.mjs' + - 'action.yaml' + - 'dist/**' + - '.github/workflows/**' workflow_dispatch: -# permissions: -# contents: write - jobs: + # =========================================================================== + # Build & lint — all test jobs depend on this + # =========================================================================== build: + name: Build & Lint runs-on: ubuntu-latest steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 with: egress-policy: audit - - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24.x' + - run: npm ci + - run: npm run lint + - run: npm run build - - name: Build with esbuild - run: | - npm install - npm run build + # =========================================================================== + # Test: Dispatch a workflow by its display name, with inputs + # =========================================================================== + test-dispatch-by-name: + name: 'Test: Dispatch by name' + needs: build + runs-on: ubuntu-latest + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 + with: + egress-policy: audit - - name: Invoke echo 1 workflow using this action + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Dispatch echo-1 by name + id: dispatch uses: ./ with: workflow: Message Echo 1 - inputs: '{"message": "blah blah"}' + inputs: '{"message": "hello from name test"}' + ref: ${{ github.event.pull_request.head.ref || github.ref_name }} + + - name: Assert outputs are set + env: + RUN_ID: ${{ steps.dispatch.outputs.runId }} + WORKFLOW_ID: ${{ steps.dispatch.outputs.workflowId }} + RUN_URL: ${{ steps.dispatch.outputs.runUrl }} + RUN_URL_HTML: ${{ steps.dispatch.outputs.runUrlHtml }} + run: | + echo "runId=$RUN_ID workflowId=$WORKFLOW_ID" + echo "runUrl=$RUN_URL" + echo "runUrlHtml=$RUN_URL_HTML" + [[ -n "$RUN_ID" ]] || { echo "FAIL: runId empty"; exit 1; } + [[ -n "$WORKFLOW_ID" ]] || { echo "FAIL: workflowId empty"; exit 1; } + [[ -n "$RUN_URL" ]] || { echo "FAIL: runUrl empty"; exit 1; } + [[ -n "$RUN_URL_HTML" ]] || { echo "FAIL: runUrlHtml empty"; exit 1; } - - name: Invoke echo 2 workflow using this action + # =========================================================================== + # Test: Dispatch a workflow by its filename (short form) + # =========================================================================== + test-dispatch-by-filename: + name: 'Test: Dispatch by filename' + needs: build + runs-on: ubuntu-latest + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Dispatch echo-2 by filename uses: ./ with: workflow: echo-2.yaml + ref: ${{ github.event.pull_request.head.ref || github.ref_name }} + + # =========================================================================== + # Test: Dispatch a workflow by its full path + # =========================================================================== + test-dispatch-by-path: + name: 'Test: Dispatch by full path' + needs: build + runs-on: ubuntu-latest + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Dispatch echo-2 by full path + uses: ./ + with: + workflow: .github/workflows/echo-2.yaml + ref: ${{ github.event.pull_request.head.ref || github.ref_name }} + + # =========================================================================== + # Test: Dispatch a workflow by its numeric ID + # =========================================================================== + test-dispatch-by-id: + name: 'Test: Dispatch by ID' + needs: build + runs-on: ubuntu-latest + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + # NOTE: This ID is specific to this repo's echo-1 workflow. + # If the workflow file is deleted & recreated, this ID will change. + - name: Dispatch echo-1 by numeric ID + uses: ./ + with: + workflow: '78181717' + inputs: '{"message": "dispatched by id"}' + ref: ${{ github.event.pull_request.head.ref || github.ref_name }} + + # =========================================================================== + # Test: Wait for completion on a slow workflow (success case) + # =========================================================================== + test-wait-completion: + name: 'Test: Wait for completion' + needs: build + runs-on: ubuntu-latest + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Dispatch echo-2 and wait for it to finish + uses: ./ + with: + workflow: echo-2.yaml + wait-for-completion: true + ref: ${{ github.event.pull_request.head.ref || github.ref_name }} + + # =========================================================================== + # Test: sync-status mirrors a successful workflow conclusion + # =========================================================================== + test-sync-status-success: + name: 'Test: Sync status (success)' + needs: build + runs-on: ubuntu-latest + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Dispatch echo-1 with wait + sync-status + uses: ./ + with: + workflow: Message Echo 1 + inputs: '{"message": "sync status success test"}' + wait-for-completion: true + sync-status: true + ref: ${{ github.event.pull_request.head.ref || github.ref_name }} + + # =========================================================================== + # Test: sync-status propagates a failing workflow as a failure + # =========================================================================== + test-sync-status-failure: + name: 'Test: Sync status (failure)' + needs: build + runs-on: ubuntu-latest + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Dispatch echo-3 (always fails) with sync-status + id: dispatch-fail + continue-on-error: true + uses: ./ + with: + workflow: echo-3.yaml + wait-for-completion: true + sync-status: true + ref: ${{ github.event.pull_request.head.ref || github.ref_name }} + + - name: Assert action reported failure + run: | + if [[ "${{ steps.dispatch-fail.outcome }}" != "failure" ]]; then + echo "FAIL: Expected action to fail when sync-status mirrors a failing workflow" + exit 1 + fi + echo "PASS: Action correctly propagated failure" + + # =========================================================================== + # Test: Invalid workflow reference produces an error + # =========================================================================== + test-invalid-workflow: + name: 'Test: Invalid workflow name' + needs: build + runs-on: ubuntu-latest + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Dispatch a workflow that does not exist + id: bad-workflow + continue-on-error: true + uses: ./ + with: + workflow: this-workflow-does-not-exist.yaml + ref: ${{ github.event.pull_request.head.ref || github.ref_name }} + + - name: Assert action failed + run: | + if [[ "${{ steps.bad-workflow.outcome }}" != "failure" ]]; then + echo "FAIL: Expected action to fail for non-existent workflow" + exit 1 + fi + echo "PASS: Action correctly failed for invalid workflow reference" + + # =========================================================================== + # Test: Wait timeout fires before a slow workflow finishes + # =========================================================================== + test-wait-timeout: + name: 'Test: Wait timeout' + needs: build + runs-on: ubuntu-latest + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + # echo-2 sleeps for 11s — a 1-second timeout will trigger after the + # first poll cycle (~5s). The action warns but does NOT fail on timeout. + - name: Dispatch echo-2 with 1-second timeout + uses: ./ + with: + workflow: echo-2.yaml + wait-for-completion: true + wait-timeout-seconds: 1 + ref: ${{ github.event.pull_request.head.ref || github.ref_name }} diff --git a/.github/workflows/echo-2.yaml b/.github/workflows/echo-2.yaml index 0969420..0df036e 100644 --- a/.github/workflows/echo-2.yaml +++ b/.github/workflows/echo-2.yaml @@ -20,7 +20,7 @@ jobs: with: egress-policy: audit - - name: Echo message slowly with a wait + - name: Echo message with a wait run: | - sleep 125 + sleep 11 echo '${{ inputs.message }}' diff --git a/.github/workflows/echo-3.yaml b/.github/workflows/echo-3.yaml index fd1db3d..d600dcb 100644 --- a/.github/workflows/echo-3.yaml +++ b/.github/workflows/echo-3.yaml @@ -1,16 +1,12 @@ name: Message Echo 3 -# A version using workflow_call for investigation purposes - -on: - workflow_call: +on: + workflow_dispatch: inputs: message: required: false - default: "this is echo 3" - type: string - description: "Message to echo" - + default: 'this is echo 3' + description: 'Message to echo' permissions: contents: read @@ -23,5 +19,5 @@ jobs: with: egress-policy: audit - - name: Echo message - run: echo '${{ inputs.message }}' \ No newline at end of file + - name: Intentional failure + run: exit 1 diff --git a/README.md b/README.md index f73b903..6a04d68 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,17 @@ The workflow must be configured for this event type e.g. `on: [workflow_dispatch This allows you to chain workflows, the classic use case is have a CI build workflow, trigger a CD release/deploy workflow when it completes. Allowing you to maintain separate workflows for CI and CD, and pass data between them as required. -**2026 Update**: We finally have a way to get the details of the triggered workflow run, including the run ID and URL, which means we can now poll for the run status and wait for it to complete if required. This is a common ask and I'm glad to have added this feature after nearly 6 years! - ![Screenshot the logs of a workflow run](./etc/screen.png) For details of the `workflow_dispatch` even see [this blog post introducing this type of trigger](https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch/) -_Note 1._ GitHub now has a native way to chain workflows called "reusable workflows". See the docs on [reusing workflows](https://docs.github.com/en/actions/using-workflows/reusing-workflows). This approach is somewhat different from workflow_dispatch but it's worth keeping in mind. +Note, GitHub now has a native way to chain workflows called "reusable workflows". See the docs on [reusing workflows](https://docs.github.com/en/actions/using-workflows/reusing-workflows). This approach is somewhat different from workflow_dispatch but it's worth keeping in mind. + +The GitHub UI will report flows triggered by this action as "manually triggered" this might seem confusing at first, but it makes sense when you remember that the workflow_dispatch event is designed to be triggered manually by a user, and this action is simulating that manual trigger. -_Note 2._ The GitHub UI will report flows triggered by this action as "manually triggered" even though they have been run programmatically via another workflow and the API. +## 2026 Update -_Note 3._ If you want to reference the target workflow by ID, you will need to list them with the following REST API call `curl https://api.github.com/repos/{{owner}}/{{repo}}/actions/workflows -H "Authorization: token {{pat-token}}"` +We finally have a way to get the details of the triggered workflow run, including the run ID and URL, which means we can now poll for the run status and wait for it to complete if required. This is a common ask and I'm glad to have added this feature after nearly 6 years! ## Action Inputs @@ -33,6 +33,8 @@ workflow: my-workflow.yaml workflow: 1218419 ``` +> _Note:_ If you want to reference the target workflow by ID, you will need to list the IDs with a REST API call `curl https://api.github.com/repos/{{owner}}/{{repo}}/actions/workflows -H "Authorization: token {{pat-token}}"` + ### `inputs` **Optional.** The inputs to pass to the workflow (if any are configured), this must be a JSON encoded string, e.g. `{ "myInput": "foobar" }` diff --git a/action.yaml b/action.yaml index 166ae00..c767892 100644 --- a/action.yaml +++ b/action.yaml @@ -15,6 +15,7 @@ inputs: ref: description: 'The reference can be a branch, tag, or a commit SHA' required: false + default: ${{ github.head_ref || github.ref }} repo: description: 'Repo owner & name, slash separated, only set if invoking a workflow in a different repo' required: false @@ -26,6 +27,10 @@ inputs: description: 'Maximum time in seconds to wait for the workflow run to complete before timing out (only applies if wait-for-completion is true)' required: false default: 900 + sync-status: + description: 'Whether to set the status of this action to failed if the triggered workflow run fails, or is cancelled. Only applies if wait-for-completion is true.' + required: false + default: false outputs: runId: diff --git a/dist/index.js b/dist/index.js index 4423d7d..9edc82d 100644 --- a/dist/index.js +++ b/dist/index.js @@ -39288,7 +39288,7 @@ function getOctokit(token, options, ...additionalPlugins) { var fs3 = __toESM(require("fs")); // package.json -var version = "1.3.0"; +var version = "1.3.1"; // src/main.ts async function run() { @@ -39297,20 +39297,22 @@ async function run() { await validateSubscription(); const workflowRef = getInput("workflow"); const token = getInput("token"); - const ref = getInput("ref") || context2.ref; + const ref = getInput("ref"); const [owner, repo] = getInput("repo") ? getInput("repo").split("/") : [context2.repo.owner, context2.repo.repo]; let inputs = {}; const inputsJson = getInput("inputs"); if (inputsJson) { - inputs = JSON.parse(inputsJson); + try { + inputs = JSON.parse(inputsJson); + } catch (e) { + error(`Failed to parse 'inputs' JSON string: ${e instanceof Error ? e.message : String(e)}`); + } } const octokit = getOctokit(token); const workflows = await octokit.paginate( octokit.rest.actions.listRepoWorkflows.endpoint.merge({ owner, - repo, - ref, - inputs + repo }) ); debug("### START List Workflows response data"); @@ -39321,8 +39323,8 @@ async function run() { workflow.path == workflowRef; }); if (!foundWorkflow) throw new Error(`Unable to find workflow '${workflowRef}' in ${owner}/${repo} \u{1F625}`); - console.log(`\u{1F50E} Found workflow, id: ${foundWorkflow.id}, name: ${foundWorkflow.name}, path: ${foundWorkflow.path}`); - console.log("\u{1F680} Calling GitHub API to dispatch workflow..."); + info(`\u{1F50E} Found workflow, id: ${foundWorkflow.id}, name: ${foundWorkflow.name}, path: ${foundWorkflow.path}`); + info("\u{1F680} Calling GitHub API to dispatch workflow..."); const dispatchResp = await octokit.request( `POST /repos/${owner}/${repo}/actions/workflows/${foundWorkflow.id}/dispatches`, { @@ -39334,15 +39336,19 @@ async function run() { info(`\u{1F3C6} API response status: ${dispatchResp.status}`); info(`\u{1F310} Run URL: ${dispatchResp.data.html_url}`); const waitForCompletion = getInput("wait-for-completion") === "true"; + const syncStatus = getInput("sync-status") === "true"; const timeoutSeconds = parseInt(getInput("wait-timeout-seconds") || "900", 10); + let runStatus = "in_progress"; if (waitForCompletion) { info(`\u23F3 Waiting for workflow run to complete with a timeout of ${timeoutSeconds} seconds...`); - let runStatus = "in_progress"; const startTime = Date.now(); while (runStatus === "in_progress" || runStatus === "queued" || runStatus === "waiting") { if ((Date.now() - startTime) / 1e3 > timeoutSeconds) { - warning(`\u26A0\uFE0F Workflow run did not complete within ${timeoutSeconds} seconds, timing out. -Note: The workflow is still running but we have stopped waiting. You can check the run status here: ${dispatchResp.data.html_url}`); + warning( + `\u26A0\uFE0F Workflow run did not complete within ${timeoutSeconds} seconds, timing out. +Note: The workflow is still running but we have stopped waiting. You can check the run status here: ${dispatchResp.data.html_url}` + ); + runStatus = "timed_out"; break; } await new Promise((resolve) => setTimeout(resolve, 5e3)); @@ -39353,7 +39359,9 @@ Note: The workflow is still running but we have stopped waiting. You can check t info(`\u{1F504} Current run status: ${runStatus}`); } if (runStatus === "completed") { - info("\u2705 Workflow run completed successfully!"); + info("\u2705 Workflow run completed, the final status can be found in the workflow run details."); + } else if (runStatus === "timed_out") { + warning(`\u26A0\uFE0F Workflow run did not complete within the timeout period.`); } else { warning(`\u26A0\uFE0F Workflow run completed with status: ${runStatus}`); } @@ -39362,6 +39370,19 @@ Note: The workflow is still running but we have stopped waiting. You can check t setOutput("runUrl", dispatchResp.data.run_url); setOutput("runUrlHtml", dispatchResp.data.html_url); setOutput("workflowId", foundWorkflow.id); + if (syncStatus && waitForCompletion) { + const { data: finalRunData } = await octokit.request( + `GET /repos/${owner}/${repo}/actions/runs/${dispatchResp.data.workflow_run_id}` + ); + const conclusion = finalRunData.conclusion; + if (conclusion === "failure") { + setFailed(`Workflow run failed. Check the run details here: ${dispatchResp.data.html_url}`); + } else if (conclusion === "cancelled") { + setFailed(`Workflow run was cancelled. Check the run details here: ${dispatchResp.data.html_url}`); + } else { + info(`\u{1F389} Workflow conclusion: ${conclusion}`); + } + } } catch (error2) { const e = error2; if (e.message.endsWith("a disabled workflow")) { @@ -39384,8 +39405,7 @@ async function validateSubscription() { info(""); info("\x1B[1;36mStepSecurity Maintained Action\x1B[0m"); info(`Secure drop-in replacement for ${upstream}`); - if (repoPrivate === false) - info("\x1B[32m\u2713 Free for public repositories\x1B[0m"); + if (repoPrivate === false) info("\x1B[32m\u2713 Free for public repositories\x1B[0m"); info(`\x1B[36mLearn more:\x1B[0m ${docsUrl}`); info(""); if (repoPrivate === false) return; @@ -39400,12 +39420,8 @@ async function validateSubscription() { ); } catch (error2) { if (isAxiosError2(error2) && error2.response?.status === 403) { - error( - `\x1B[1;31mThis action requires a StepSecurity subscription for private repositories.\x1B[0m` - ); - error( - `\x1B[31mLearn how to enable a subscription: ${docsUrl}\x1B[0m` - ); + error(`\x1B[1;31mThis action requires a StepSecurity subscription for private repositories.\x1B[0m`); + error(`\x1B[31mLearn how to enable a subscription: ${docsUrl}\x1B[0m`); process.exit(1); } info("Timeout or API not reachable. Continuing to next step."); diff --git a/package-lock.json b/package-lock.json index 0a6c93a..8c82511 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "workflow-dispatch", - "version": "1.3.0", + "version": "1.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "workflow-dispatch", - "version": "1.3.0", + "version": "1.3.1", "license": "MIT", "devDependencies": { "@actions/core": "^3.0.0", diff --git a/package.json b/package.json index 934a405..2225dd1 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,20 @@ { "name": "workflow-dispatch", - "version": "1.3.0", + "version": "1.3.1", + "author": "step-security", + "license": "MIT", "description": "Trigger running GitHub Actions workflows", "main": "dist/index.js", "scripts": { - "build": "esbuild src/main.ts --bundle --platform=node --target=node22 --outfile=dist/index.js", - "lint": "eslint src/" + "build": "esbuild src/main.ts --bundle --platform=node --target=node24 --outfile=dist/index.js", + "lint": "eslint src/ --ext .ts && prettier --check src/", + "lint-fix": "eslint src/ --fix", + "format": "prettier --write src/" }, "keywords": [ "github", "actions" ], - "license": "MIT", "devDependencies": { "@actions/core": "^3.0.0", "@actions/github": "^9.0.0", diff --git a/src/main.ts b/src/main.ts index 81513c7..f9bc4d3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,9 +18,9 @@ type Workflow = { path: string } -// +// ============================================================================= // Main task function (async wrapper) -// +// ============================================================================= async function run(): Promise { core.info(`🏃 Workflow Dispatch Action v${PackageJSON.version}`) try { @@ -31,7 +31,7 @@ async function run(): Promise { // Optional inputs, with defaults const token = core.getInput('token') - const ref = core.getInput('ref') || github.context.ref + const ref = core.getInput('ref') const [owner, repo] = core.getInput('repo') ? core.getInput('repo').split('/') : [github.context.repo.owner, github.context.repo.repo] @@ -40,7 +40,11 @@ async function run(): Promise { let inputs = {} const inputsJson = core.getInput('inputs') if (inputsJson) { - inputs = JSON.parse(inputsJson) + try { + inputs = JSON.parse(inputsJson) + } catch (e) { + core.error(`Failed to parse 'inputs' JSON string: ${e instanceof Error ? e.message : String(e)}`) + } } // Get octokit client for making API calls @@ -51,8 +55,6 @@ async function run(): Promise { octokit.rest.actions.listRepoWorkflows.endpoint.merge({ owner, repo, - ref, - inputs, }), ) @@ -73,10 +75,10 @@ async function run(): Promise { if (!foundWorkflow) throw new Error(`Unable to find workflow '${workflowRef}' in ${owner}/${repo} 😥`) - console.log(`🔎 Found workflow, id: ${foundWorkflow.id}, name: ${foundWorkflow.name}, path: ${foundWorkflow.path}`) + core.info(`🔎 Found workflow, id: ${foundWorkflow.id}, name: ${foundWorkflow.name}, path: ${foundWorkflow.path}`) // Call workflow_dispatch API - console.log('🚀 Calling GitHub API to dispatch workflow...') + core.info('🚀 Calling GitHub API to dispatch workflow...') const dispatchResp = await octokit.request( `POST /repos/${owner}/${repo}/actions/workflows/${foundWorkflow.id}/dispatches`, { @@ -91,17 +93,23 @@ async function run(): Promise { // Handle wait for completion const waitForCompletion = core.getInput('wait-for-completion') === 'true' + const syncStatus = core.getInput('sync-status') === 'true' const timeoutSeconds = parseInt(core.getInput('wait-timeout-seconds') || '900', 10) // Default to 15 minutes + let runStatus = 'in_progress' + + // Polling loop to check workflow run status until it completes or times out if (waitForCompletion) { core.info(`⏳ Waiting for workflow run to complete with a timeout of ${timeoutSeconds} seconds...`) - let runStatus = 'in_progress' const startTime = Date.now() while (runStatus === 'in_progress' || runStatus === 'queued' || runStatus === 'waiting') { if ((Date.now() - startTime) / 1000 > timeoutSeconds) { - core.warning(`⚠️ Workflow run did not complete within ${timeoutSeconds} seconds, timing out.\nNote: The workflow is still running but we have stopped waiting. You can check the run status here: ${dispatchResp.data.html_url}`) + core.warning( + `⚠️ Workflow run did not complete within ${timeoutSeconds} seconds, timing out.\nNote: The workflow is still running but we have stopped waiting. You can check the run status here: ${dispatchResp.data.html_url}`, + ) + runStatus = 'timed_out' break } - + await new Promise((resolve) => setTimeout(resolve, 5000)) // Wait for 5 seconds before polling again const { data: runData } = await octokit.request( @@ -111,8 +119,10 @@ async function run(): Promise { core.info(`🔄 Current run status: ${runStatus}`) } - if (runStatus === 'completed') { - core.info('✅ Workflow run completed successfully!') + if (runStatus === 'completed') { + core.info('✅ Workflow run completed, the final status can be found in the workflow run details.') + } else if (runStatus === 'timed_out') { + core.warning(`⚠️ Workflow run did not complete within the timeout period.`) } else { core.warning(`⚠️ Workflow run completed with status: ${runStatus}`) } @@ -122,6 +132,24 @@ async function run(): Promise { core.setOutput('runUrl', dispatchResp.data.run_url) core.setOutput('runUrlHtml', dispatchResp.data.html_url) core.setOutput('workflowId', foundWorkflow.id) + + // Sync the status of this action with the triggered workflow run if requested + if (syncStatus && waitForCompletion) { + // Get the final conclusion of the workflow run if we were waiting for completion + const { data: finalRunData } = await octokit.request( + `GET /repos/${owner}/${repo}/actions/runs/${dispatchResp.data.workflow_run_id}`, + ) + const conclusion = finalRunData.conclusion + + // Set this action to failed if the triggered workflow run failed or was cancelled + if (conclusion === 'failure') { + core.setFailed(`Workflow run failed. Check the run details here: ${dispatchResp.data.html_url}`) + } else if (conclusion === 'cancelled') { + core.setFailed(`Workflow run was cancelled. Check the run details here: ${dispatchResp.data.html_url}`) + } else { + core.info(`🎉 Workflow conclusion: ${conclusion}`) + } + } } catch (error) { const e = error as Error @@ -145,43 +173,37 @@ async function validateSubscription(): Promise { const upstream = 'benc-uk/workflow-dispatch' const action = process.env.GITHUB_ACTION_REPOSITORY - const docsUrl = - 'https://docs.stepsecurity.io/actions/stepsecurity-maintained-actions' + const docsUrl = 'https://docs.stepsecurity.io/actions/stepsecurity-maintained-actions' core.info('') core.info('\u001b[1;36mStepSecurity Maintained Action\u001b[0m') core.info(`Secure drop-in replacement for ${upstream}`) - if (repoPrivate === false) - core.info('\u001b[32m\u2713 Free for public repositories\u001b[0m') + if (repoPrivate === false) core.info('\u001b[32m\u2713 Free for public repositories\u001b[0m') core.info(`\u001b[36mLearn more:\u001b[0m ${docsUrl}`) core.info('') if (repoPrivate === false) return const serverUrl = process.env.GITHUB_SERVER_URL || 'https://github.com' - const body: Record = {action: action || ''} + const body: Record = { action: action || '' } if (serverUrl !== 'https://github.com') body.ghes_server = serverUrl try { await axios.post( `https://agent.api.stepsecurity.io/v1/github/${process.env.GITHUB_REPOSITORY}/actions/maintained-actions-subscription`, body, - {timeout: 3000} + { timeout: 3000 }, ) } catch (error) { if (isAxiosError(error) && error.response?.status === 403) { - core.error( - `\u001b[1;31mThis action requires a StepSecurity subscription for private repositories.\u001b[0m` - ) - core.error( - `\u001b[31mLearn how to enable a subscription: ${docsUrl}\u001b[0m` - ) + core.error(`\u001b[1;31mThis action requires a StepSecurity subscription for private repositories.\u001b[0m`) + core.error(`\u001b[31mLearn how to enable a subscription: ${docsUrl}\u001b[0m`) process.exit(1) } core.info('Timeout or API not reachable. Continuing to next step.') } } -// +// ============================================================================= // Call the main task run function -// +// ============================================================================= run()