Skip to content

fix: sync-status reports success for runs that did not succeed - #304

Open
alecsammon wants to merge 1 commit into
step-security:mainfrom
alecsammon:fix_sync_status_non_success
Open

fix: sync-status reports success for runs that did not succeed#304
alecsammon wants to merge 1 commit into
step-security:mainfrom
alecsammon:fix_sync_status_non_success

Conversation

@alecsammon

Copy link
Copy Markdown

The problem

With wait-for-completion: true and sync-status: true, the step only fails on a failure or
cancelled conclusion:

if (conclusion === 'failure') {
  core.setFailed(...)
} else if (conclusion === 'cancelled') {
  core.setFailed(...)
} else {
  core.info(`🎉 Workflow conclusion: ${conclusion}`)
}

Every other non-success outcome therefore sets the step to success:

Conclusion Meaning Current result
timed_out the triggered run hit a job timeout ✅ success
startup_failure the run never started (e.g. invalid workflow file) ✅ success
stale the run was discarded before it could report ✅ success
null the run has not completed ✅ success

The null case is the easiest one to hit in practice, and it does not need anything unusual to go
wrong. When the polling loop gives up at wait-timeout-seconds it sets its own status to timed_out,
emits core.warning, and falls through — and sync-status then reads a null conclusion from a run
that is still in progress and passes the step. So a job that waited the entire timeout and never
observed an outcome reports success.

That matters most where this action is used to gate something: a required status check, or a deploy
that waits on a rollout. In both cases "we never saw it finish" and "it finished successfully" are
reported identically.

The change

Fail when the run has not reached completed, and fail on the conclusions that unambiguously mean it
did not succeed:

const FAILED_CONCLUSIONS = new Set(['failure', 'cancelled', 'timed_out', 'startup_failure', 'stale'])

neutral, skipped and action_required keep passing exactly as before, so runs that deliberately
end without doing work are unaffected. The existing failure and cancelled messages are unchanged.

Is this breaking?

It is a behaviour change, but only for outcomes that are already not successes — a caller relying on a
timed_out or startup_failure run reporting success is relying on the bug. I have kept the ambiguous
conclusions (neutral, skipped, action_required) passing precisely to avoid changing behaviour where
"success" is arguable.

If you would rather not change the default at all, I am happy to rework this behind an opt-in input
(something like fail-on-any-non-success) — just say which you prefer and I will push that instead.

Verification

The repository has no test suite, so I verified in three ways:

  1. npm run lint (eslint + prettier) passes.
  2. npm run build reproduces the committed bundle exactly — the dist/index.js diff is only this
    change, with no unrelated bundler churn.
  3. I table-tested the decision logic across every documented status/conclusion pair:
ok  completed/success          -> pass
ok  completed/failure          -> FAIL
ok  completed/cancelled        -> FAIL
ok  completed/timed_out        -> FAIL
ok  completed/startup_failure  -> FAIL
ok  completed/stale            -> FAIL
ok  completed/neutral          -> pass
ok  completed/skipped          -> pass
ok  completed/action_required  -> pass
ok  pending/null               -> FAIL
ok  queued/null                -> FAIL
ok  in_progress/null           -> FAIL
ok  waiting/null               -> FAIL

Note src/main.ts does not currently typecheck under tsc --noEmit because @types/node is absent
(fs and process are unresolved). That is pre-existing and untouched by this change.

sync-status only fails on a `failure` or `cancelled` conclusion, so every other
non-success outcome sets the step to success:

- `timed_out` — the triggered run hit a job timeout
- `startup_failure` — the run never started (e.g. an invalid workflow file)
- `stale` — the run was discarded before it could report
- a `null` conclusion, which is what the API returns whenever the run has not
  completed, including after `wait-for-completion` gives up at its own timeout

The last case is the easiest to hit: the wait loop exits on its timeout, warns,
and then sync-status reads a `null` conclusion and passes the step — so a job
that waited the full timeout reports success while the run it triggered is
still going.

Fail on the conclusions that unambiguously mean the run did not succeed, and on
any run that has not reached `completed`. `neutral`, `skipped` and
`action_required` keep passing as before, so runs that deliberately end without
doing work are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant