Skip to content

CI: Harden the release pipeline after the v8.17.0 release - #1642

Merged
Sergej-Vlasov merged 2 commits into
mainfrom
harden-release-ci
Sep 11, 2026
Merged

Sergej-Vlasov merged 2 commits into
mainfrom
harden-release-ci

Conversation

@Sergej-Vlasov

Copy link
Copy Markdown
Contributor

Hardening for the release pipeline, driven by what actually happened releasing v8.17.0 — the first live run of the release-PR flow. Every change below traces to a real failure in that run, not a hypothetical.

Recap of the run: the release PR opened, was approved and auto-merged correctly, and release-pr/canary skipped exactly as designed. Then publish failed twice — once half-published, once fully released but still red — and needed a manual re-run.

1. Pin the publish tree to the release commit

On the workflow_dispatch path actions/checkout takes main's tip, not the release commit, and everything downstream reads that tree: the build, lerna.json, the gitHead lerna stamps into npm metadata, and the commit range auto release turns into notes.

A re-drive after later merges would publish a tree the tag doesn't describe. Push release tag already resolved the Release v commit; the working tree now follows it. No-op on the push path, where HEAD is already that commit.

This was a gap in the earlier workflow_dispatch fix — that pinned the tag's SHA but left the checkout on the tip.

2. Retry the npm publish

v8.17.0 aborted partway with a Rekor 409 (TLOG_CREATE_ENTRY_ERROR), leaving @grafana/scenes published and @grafana/scenes-react not. The identical command succeeded minutes later on a manual re-run — so the retry is precisely what a human was doing by hand.

The registry, not lerna's exit code, decides completion: after a non-zero exit the step re-probes npm, treats "everything is on npm" as success, and only fails once packages are still missing after three attempts. Safe because from-package recomputes the publish set from the registry each call.

Deliberately not applied to Create GitHub releaseauto release calls repos.createRelease unconditionally, so an in-job retry would 422 once the release exists and turn a good release permanently red. Its re-entrancy correctly comes from the release gate across runs.

3. Assert the GitHub release exists, don't trust auto's exit code

auto release created the release and commented on 10 PRs, then hit issue #1621 with 403 Resource not accessible by integration and failed the step — after the release was already correct.

This is not a one-off. In @auto-it/released, the PR-comment path is wrapped in try/catch but the issue-comment path is not, so any release containing a Closes #N PR fails the same way. A post-release failure is now a ::warning; a genuinely missing release is still an error.

Granting the app issues: write is still worth doing, but it's now a quality-of-life fix rather than a blocker.

4. Report the end state to the step summary

Both v8.17.0 attempts surfaced only a bare red X — one half-published, one fully released. Working out which took reading ~2,000 log lines. The summary now lists each package, the tag (and whether it points at the right commit) and the release, then says plainly whether a re-run would change anything.

Also: Resolve now fails if lerna list returns no publishable packages, which would otherwise leave PUBLISH=false and skip the publish silently green.

Verification

The release path can't be exercised by CI, so this is verified by targeted local testing rather than a live run:

  • YAML parses; publish has the expected 13 steps; canary untouched
  • Release-commit resolution returns f52184de on main — the correct v8.17.0 commit
  • Retry loop tested across four cases: success first try → 0; non-zero exit but registry complete → 0; transient then success → 0; genuinely broken → 1 (failure detection preserved)
  • specs output format verified as space-separated

Honest caveat: the real test is the next release. These paths only execute during a publish. The design intent is that every change either fails closed or is a no-op on the happy path.

Four changes, each from something that actually happened releasing v8.17.0.

Pin the publish tree to the release commit. On the workflow_dispatch path
actions/checkout takes main's tip, not the release commit, and everything
downstream reads that tree: the build, lerna.json, the gitHead lerna stamps
into the npm metadata, and the commit range auto release turns into notes. A
re-drive after later merges would publish a tree the tag does not describe.
Push release tag already resolved the Release v commit; the working tree now
follows it. No-op on the push path, where HEAD is already that commit.

Retry the npm publish. Provenance signing aborted partway through v8.17.0
with a Rekor 409, leaving @grafana/scenes published and @grafana/scenes-react
not. The identical command succeeded minutes later on a manual re-run, so the
retry is what a human was doing by hand. The registry, not lerna's exit code,
decides completion: after a non-zero exit the step re-probes npm and only
fails once packages are still missing after three attempts.

Assert the GitHub release exists instead of trusting auto's exit code. auto
release created the release, then failed commenting on a referenced issue
(the app has pull_requests:write, not issues:write). The released plugin
guards its PR-comment path with a try/catch but not the issue path, so every
release closing an issue would fail the same way after doing everything
right. A post-release failure is now a warning, while a missing release is
still an error.

Report the end state to the step summary. Both v8.17.0 attempts surfaced only
a red X: one was half-published, the other fully released. The summary now
lists each package, the tag and the release, and says plainly whether a
re-run would change anything.

Also fail Resolve when lerna lists no publishable packages, which would
otherwise leave PUBLISH=false and skip the publish silently green.
@Sergej-Vlasov
Sergej-Vlasov requested a review from a team as a code owner September 10, 2026 12:35
@Sergej-Vlasov
Sergej-Vlasov requested review from dprokop, harisrozajac, ivanortegaalba and mdvictor and removed request for a team September 10, 2026 12:35

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1914893. Configure here.

Comment thread .github/workflows/node-ci.yml
ivanortegaalba
ivanortegaalba previously approved these changes Sep 10, 2026

@ivanortegaalba ivanortegaalba left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested with the agent, and we found some small things. Can you have a look?

Comment thread .github/workflows/node-ci.yml Outdated
Comment on lines +308 to +312
RELEASE_SHA="$(git rev-list -1 --grep "^Release v[0-9]" HEAD)"
if [ -z "$RELEASE_SHA" ]; then
echo "No 'Release v' commit reachable from HEAD" >&2
exit 1
fi

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--grep matches any line of a commit message, not just the subject, so a commit whose body contains a line starting with Release v matches too

Asserting on the subject after resolving should be enough

Suggested change
RELEASE_SHA="$(git rev-list -1 --grep "^Release v[0-9]" HEAD)"
if [ -z "$RELEASE_SHA" ]; then
echo "No 'Release v' commit reachable from HEAD" >&2
exit 1
fi
RELEASE_SHA="$(git rev-list -1 --grep "^Release v[0-9]" HEAD)"
if [ -z "$RELEASE_SHA" ]; then
echo "No 'Release v' commit reachable from HEAD" >&2
exit 1
fi
case "$(git log -1 --format=%s "$RELEASE_SHA")" in
"Release v"[0-9]*) ;;
*) echo "Resolved ${RELEASE_SHA} is not a release commit" >&2; exit 1 ;;
esac

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, and taken a step further than the suggestion.

Reproduced it first in a scratch repo: a commit whose body contained Release v8.17.0 was wrong resolved AHEAD of the real release commit, so --grep returned entirely the wrong sha. Confirmed the subject assertion catches it.

Rather than assert-and-fail, candidates are now filtered on the subject:

git log --grep "^Release v[0-9]" --format='%H%x09%s' HEAD | awk -F'\t' '$2 ~ /^Release v[0-9]/ {print $1; exit}'

Same fail-closed guarantee, but it skips the decoy instead of blocking a legitimate release. Verified by extracting the rendered step out of the YAML and running it against that scratch repo — it picks the real commit.

Worth flagging: this bug is already live on main, since the merged Push release tag greps the same way. This PR fixes it there too.

Comment thread .github/workflows/node-ci.yml Outdated
# Always runs, because the end state is least obvious exactly when a step
# above failed: v8.17.0 attempt 1 was half-published and attempt 2 was fully
# released but still red, and both looked like a bare red X.
if: always() && steps.version.outputs.version != ''

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

always() also runs on cancellation, so a cancelled run would write a summary and exit 1

Suggested change
if: always() && steps.version.outputs.version != ''
if: ${{ !cancelled() && steps.version.outputs.version != '' }}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, now !cancelled() && steps.version.outputs.version != ''. A cancelled run would have written a summary claiming the release was incomplete and then exited 1.

Comment on lines +484 to +491
if [ -z "$STATE" ]; then
if printf '%s' "$NPM_OUT" | grep -q 'E404'; then
STATE="**MISSING from npm**"
else
STATE="unknown (npm view failed)"
fi
INCOMPLETE=true
fi

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

npm view failure that is not an E404 sets INCOMPLETE, which reds the job and prints "re-run this job" for a release that may be entirely complete. That looks like the same false red this step was written to remove.

Would it make sense to keep the hard failure for a confirmed E404 and warn on the unknown case?

Suggested change
if [ -z "$STATE" ]; then
if printf '%s' "$NPM_OUT" | grep -q 'E404'; then
STATE="**MISSING from npm**"
else
STATE="unknown (npm view failed)"
fi
INCOMPLETE=true
fi
if [ -z "$STATE" ]; then
if printf '%s' "$NPM_OUT" | grep -q 'E404'; then
STATE="**MISSING from npm**"
INCOMPLETE=true
else
STATE="unknown (npm view failed)"
echo "::warning::npm view failed for ${SPEC}, could not confirm it is published"
fi
fi

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed as suggested. Only a confirmed E404 sets INCOMPLETE; an inconclusive probe emits ::warning:: and leaves the job's own result alone.

You identified the actual flaw in it — reddening a complete release because npm was briefly unreachable is exactly the false red this step was written to eliminate.

Comment thread .github/workflows/node-ci.yml Outdated
fi
echo "| npm \`${SPEC}\` | ${STATE} |" >> "$GITHUB_STEP_SUMMARY"
done
EXPECTED_SHA="$(git rev-list -1 --grep "^Release v${VERSION}" HEAD || true)"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The release commit is resolved three times, independently: here, in Check out the release commit, and in Push release tag. This passes even when that value is wrong

Would it make sense to resolve it once in Check out the release commit, write it to $GITHUB_OUTPUT, and have this step and Push release tag read that output? A wrong resolution would then stay a single point of failure instead of being confirmed twice

Also ${VERSION} goes into a regex unescaped, so 8.17.0 also matches 8x17y0

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both fixed. The sha is now resolved once in Check out the release commit, written to $GITHUB_OUTPUT, and read by Push release tag, Create local tag and the report. rev-list --grep appears zero times now.

That also removes the unescaped ${VERSION} regex by deletion rather than by escaping — you were right that 8.17.0 matched 8x17y0, and now there is no second pattern to get wrong.

INCOMPLETE=true
fi
if [ "$INCOMPLETE" = true ]; then
printf '\n**v%s is INCOMPLETE - re-run this job.**\n' "$VERSION" >> "$GITHUB_STEP_SUMMARY"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If only the tag is missing here, a re-run cannot help

Push release tag is gated on publish == 'true' || release == 'true', so on the re-run there is nothing left to publish or release and the tag step is skipped again.

Wondering if we have any way to solve it 🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — that was genuinely unrecoverable. Resolve now computes a third gate:

TAG_SHA="$(gh api .../git/ref/tags/v${VERSION} --jq .object.sha)" || TAG_SHA=""
[ "$TAG_SHA" = "$RELEASE_SHA" ] && echo tag=false || echo tag=true

and both tag steps are gated on publish || release || tag. So a fully published release whose tag is missing or on the wrong commit now re-tags on a re-run instead of skipping forever.

- Resolve the release commit by SUBJECT, once. `git log --grep` matches any
  line of a commit message, so an unrelated commit whose body quoted a
  "Release v..." line resolved ahead of the real release commit. Candidates
  are now filtered on the subject, which skips the decoy rather than failing
  the release. This bug is already live on main, where `Push release tag`
  greps the same way.

  The sha is resolved once and exported, and the tag steps and the report
  read that output. A wrong value is now a single point of failure instead of
  being independently re-derived three times, and the version no longer goes
  into a regex unescaped (8.17.0 also matched 8x17y0).

- Track the tag as its own gate. `Push release tag` was gated on publish or
  release, so a fully published release whose tag was missing or on the wrong
  commit would skip the tag step on every re-run, leaving it unfixable.

- Report on `!cancelled()` rather than `always()`, so a cancelled run does
  not write a summary claiming the release is incomplete and then fail.

- Only a confirmed E404 marks a package missing in the report. An unreachable
  registry now warns instead of reddening the job, which would have recreated
  the false red the report step exists to remove.
@Sergej-Vlasov
Sergej-Vlasov merged commit 84be5f6 into main Sep 11, 2026
21 checks passed
@Sergej-Vlasov
Sergej-Vlasov deleted the harden-release-ci branch September 11, 2026 13:21
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.

3 participants