diff --git a/README.md b/README.md index dea0555..b26ab4d 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,24 @@ This option is also left for backwards compatibility with older versions where t ### `sync-status` -**Optional.** Set to `'true'` to sync the status of this action with the triggered workflow run. If the triggered workflow run fails or is cancelled, this action will also be set to failed. This only applies if `wait-for-completion` is set to `true`. Default is `false`. +**Optional.** Set to `'true'` to sync the status of this action with the triggered workflow run. If the triggered workflow run does not succeed, this action will also be set to failed. This only applies if `wait-for-completion` is set to `true`. Default is `false`. + +### `success-conclusions` + +**Optional.** Comma-separated list of run conclusions that `sync-status` should treat as a pass. Anything else fails this action, as does a run that never reaches `completed`. This only applies if `sync-status` is set to `true`. Default is `success,neutral,skipped,action_required`. + +Set it to `success` when the triggered run is a gate and only a genuine pass should go green: + +```yaml +- uses: step-security/workflow-dispatch@v1 + with: + workflow: e2e.yml + wait-for-completion: 'true' + sync-status: 'true' + success-conclusions: success +``` + +Valid values are `success`, `failure`, `neutral`, `cancelled`, `skipped`, `timed_out`, `action_required`, `stale` and `startup_failure`. An unrecognised value fails the action before the workflow is dispatched. ## Action Outputs diff --git a/action.yaml b/action.yaml index 5813872..2189819 100644 --- a/action.yaml +++ b/action.yaml @@ -35,6 +35,10 @@ inputs: 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 + success-conclusions: + description: 'Comma-separated run conclusions that sync-status should treat as a pass. Set to "success" to fail on anything else. Only applies if sync-status is true. Valid values: success, failure, neutral, cancelled, skipped, timed_out, action_required, stale, startup_failure.' + required: false + default: 'success,neutral,skipped,action_required' outputs: runId: diff --git a/dist/index.js b/dist/index.js index 7f07ab0..9281535 100644 --- a/dist/index.js +++ b/dist/index.js @@ -40787,11 +40787,32 @@ var version = "1.3.2"; // src/main.ts var API_VERSION = "2026-03-10"; +var DEFAULT_SUCCESS_CONCLUSIONS = "success,neutral,skipped,action_required"; +var KNOWN_CONCLUSIONS = /* @__PURE__ */ new Set([ + "success", + "failure", + "neutral", + "cancelled", + "skipped", + "timed_out", + "action_required", + "stale", + "startup_failure" +]); async function run() { info(`\u{1F3C3} Workflow Dispatch Action v${version}`); try { await validateSubscription(); const workflowRef = getInput("workflow"); + const successConclusions = new Set( + (getInput("success-conclusions") || DEFAULT_SUCCESS_CONCLUSIONS).split(",").map((c) => c.trim().toLowerCase()).filter((c) => c.length > 0) + ); + const unknownConclusions = [...successConclusions].filter((c) => !KNOWN_CONCLUSIONS.has(c)); + if (unknownConclusions.length > 0) { + throw new Error( + `Invalid 'success-conclusions' value(s): ${unknownConclusions.join(", ")}. Valid values: ${[...KNOWN_CONCLUSIONS].join(", ")}` + ); + } const token = getInput("token"); const ref = getInput("ref"); const [owner, repo] = getInput("repo") ? getInput("repo").split("/") : [context2.repo.owner, context2.repo.repo]; @@ -40878,13 +40899,22 @@ Note: The workflow is still running but we have stopped waiting. You can check t headers: { "x-github-api-version": API_VERSION } } ); + const runStatusNow = finalRunData.status; const conclusion = finalRunData.conclusion; - if (conclusion === "failure") { + if (runStatusNow !== "completed") { + setFailed( + `Workflow run did not complete (status: ${runStatusNow}). Check the run details here: ${dispatchResp.data.html_url}` + ); + } else if (successConclusions.has(String(conclusion))) { + info(`\u{1F389} Workflow conclusion: ${conclusion}`); + } else 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}`); + setFailed( + `Workflow run concluded '${conclusion}'. Check the run details here: ${dispatchResp.data.html_url}` + ); } } } catch (error2) { diff --git a/src/main.ts b/src/main.ts index afe4f50..a5f4ba9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,6 +14,23 @@ import * as PackageJSON from '../package.json' const API_VERSION = '2026-03-10' // Latest API version as of March 2026, update as needed +// Conclusions `sync-status` treats as a pass unless `success-conclusions` says +// otherwise. The default keeps runs that deliberately end without doing work +// passing, as they always have. +const DEFAULT_SUCCESS_CONCLUSIONS = 'success,neutral,skipped,action_required' + +const KNOWN_CONCLUSIONS = new Set([ + 'success', + 'failure', + 'neutral', + 'cancelled', + 'skipped', + 'timed_out', + 'action_required', + 'stale', + 'startup_failure', +]) + type Workflow = { id: number name: string @@ -31,6 +48,21 @@ async function run(): Promise { // Required inputs const workflowRef = core.getInput('workflow') + // Read and validate before dispatching: a typo here changes the verdict, so + // failing now beats triggering a run we then refuse to judge. + const successConclusions = new Set( + (core.getInput('success-conclusions') || DEFAULT_SUCCESS_CONCLUSIONS) + .split(',') + .map((c) => c.trim().toLowerCase()) + .filter((c) => c.length > 0), + ) + const unknownConclusions = [...successConclusions].filter((c) => !KNOWN_CONCLUSIONS.has(c)) + if (unknownConclusions.length > 0) { + throw new Error( + `Invalid 'success-conclusions' value(s): ${unknownConclusions.join(', ')}. Valid values: ${[...KNOWN_CONCLUSIONS].join(', ')}`, + ) + } + // Optional inputs, with defaults const token = core.getInput('token') const ref = core.getInput('ref') @@ -154,15 +186,25 @@ async function run(): Promise { headers: { 'x-github-api-version': API_VERSION }, }, ) + const runStatusNow = finalRunData.status const conclusion = finalRunData.conclusion - // Set this action to failed if the triggered workflow run failed or was cancelled - if (conclusion === 'failure') { + // An incomplete run has no conclusion yet, so passing here would report + // success for work still in flight (e.g. after the wait above timed out). + if (runStatusNow !== 'completed') { + core.setFailed( + `Workflow run did not complete (status: ${runStatusNow}). Check the run details here: ${dispatchResp.data.html_url}`, + ) + } else if (successConclusions.has(String(conclusion))) { + core.info(`🎉 Workflow conclusion: ${conclusion}`) + } else 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}`) + core.setFailed( + `Workflow run concluded '${conclusion}'. Check the run details here: ${dispatchResp.data.html_url}`, + ) } } } catch (error) {