Skip to content

fix(ci): tag the version bump, survive push races, release on dispatch - #327

Merged
mishushakov merged 6 commits into
mainfrom
port-moresby
Aug 12, 2026
Merged

fix(ci): tag the version bump, survive push races, release on dispatch#327
mishushakov merged 6 commits into
mainfrom
port-moresby

Conversation

@mishushakov

@mishushakov mishushakov commented Aug 10, 2026

Copy link
Copy Markdown
Member

Ports the release-workflow fixes from e2b-dev/E2B (#1463, #1469, #1484, #1589, #1619), which its own PR flagged as applying here too, and switches this repo to E2B's process: releases are cut by dispatching the workflow, not by merging to main.

Every release tag in this repo points at the commit before its own version bump (@e2b/code-interpreter@2.7.0 contains "version": "2.6.1") because changesets/action tags whatever HEAD it publishes from, so the version commit now happens before the publish — safe here because changeset version leaves pnpm-lock.yaml untouched (no workspace package depends on a sibling), which also deletes the Update lock file step; the push that follows is gated on git tag --points-at HEAD and reconciles a non-fast-forward with a merge rather than a rebase, the exact race that killed run 30018789653 with the versions already published, and git add -A replaces commit -am, which cannot stage the CHANGELOG.md files changeset version writes fresh.

Also included: Slack start/success notifications with a release itinerary (built by .github/scripts/build_release_itinerary.cjs, non-blocking), is_release + changes collapsed into a single preflight job that refuses to run off main, always()!cancelled() so the workflow can actually be cancelled, pnpm taken from packageManager and Node from .tool-versions instead of drifting pins, and ::set-output replaced with $GITHUB_OUTPUT. Two latent bugs are fixed on the way: build-template and release read needs.changes.outputs.* without depending on changes, and release's !contains(needs.*.result, 'failure') could not see a template or docker failure — a failed upstream makes the test jobs skip, and skipped is not failure — so a broken template build could publish; every upstream job is now listed in needs.

Verified by rehearsing the git logic against a throwaway shallow clone and bare origin with a commit pushed mid-release: the tags land on the bump (@e2b/code-interpreter@2.7.1"version": "2.7.1"), stay reachable from main through the merge fallback, and a publish that uploads nothing leaves origin untouched with the changesets intact. Left alone deliberately: release_candidates.yml (a different, label-driven shape) and the test gating, which still runs JS/Python tests only when the template changes.

🤖 Generated with Claude Code

@cla-bot cla-bot Bot added the cla-signed label Aug 10, 2026
@cursor

cursor Bot commented Aug 10, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes how and when packages are published to npm, PyPI, Docker, and E2B, including git tag alignment and push behavior on main.

Overview
Releases no longer run on push to main; they run only via workflow_dispatch and preflight fails if the ref is not main.

The old is_release and changes jobs are merged into preflight, which also builds a Slack release itinerary from changeset status via build_release_itinerary.cjs.

The release job commits version bumps with git add -A before changeset publish so tags point at the bumped commit; push runs only when tags exist at HEAD and uses merge on non-fast-forward instead of rebasing. The post-publish lockfile update step is removed.

release now depends on all upstream jobs so docker or template failures block publish; job conditions use !cancelled() instead of always(). Slack start and success notifications include the itinerary; report-failure includes preflight in needs.

Charts release output uses GITHUB_OUTPUT instead of deprecated set-output.

Reviewed by Cursor Bugbot for commit 6be6880. Bugbot is set up for automated code reviews on this repo. Configure here.

@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.

Stale comment

Comment thread .github/workflows/release.yml Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a5545535ff

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

- main
# A release that publishes nothing leaves the changesets intact, so it can be
# re-dispatched by hand without another push to main.
workflow_dispatch: {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restrict manual releases to main

Adding workflow_dispatch without a ref guard means this release workflow can be manually run from any branch selected in the UI/CLI; GitHub's manual-run docs describe both the branch dropdown and gh workflow run --ref. If a maintainer dispatches this on a feature branch that has changesets, the release job will still publish packages/tags and git push the version bump back to that branch instead of main, so the manual path should validate github.ref == 'refs/heads/main' before publishing.

Useful? React with 👍 / 👎.

@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 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.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a554553. Configure here.

Comment thread .github/workflows/release.yml
…workflow

Port the release-workflow fixes from e2b-dev/E2B (#1463, #1469, #1484,
#1589, #1619).

Every release tag here points at the commit *before* its own version bump
(`@e2b/code-interpreter@2.7.0` contains `"version": "2.6.1"`), because
`changesets/action` tags whatever HEAD it publishes from and the commit
came afterwards. `changeset version` leaves `pnpm-lock.yaml` untouched in
this repo — no workspace package depends on a sibling — so the release
commit is already complete before anything is uploaded, and it can simply
move ahead of the publish. That also drops the `Update lock file` step.

The push is now gated on `git tag --points-at HEAD` rather than on whether
publish succeeded: the tags are what has to end up reachable, and
`changeset publish` pushes them before propagating a partial failure. A
non-fast-forward is reconciled with a merge, not a rebase, which would
strand those tags — run 30018789653 died on exactly that race with the
versions already published. `git add -A` replaces `commit -am`, which
cannot stage the CHANGELOG.md files `changeset version` writes fresh.

Also: Slack start/success notifications with a release itinerary,
`is_release` + `changes` collapsed into one `preflight` job, `always()` ->
`!cancelled()` so the workflow can be cancelled, pnpm from `packageManager`
and Node from `.tool-versions` instead of drifting pins, and `::set-output`
replaced with `$GITHUB_OUTPUT`.

Two latent bugs fixed on the way: `build-template` and `release` read
`needs.changes.outputs.*` without depending on `changes`, and `release`'s
`!contains(needs.*.result, 'failure')` could not see a template or docker
failure, since a failed upstream makes the test jobs *skip* and skipped is
not failure — so a broken template build could publish. All upstream jobs
are now listed in `needs`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment on lines 92 to +112

- name: Build release itinerary
id: itinerary
if: steps.version.outputs.release == 'true'
run: |
pnpm changeset status --output=.cs-status.json
ITINERARY=$(node -e '
const data = require("./.cs-status.json");
const labels = {
"@e2b/code-interpreter": "JS SDK (@e2b/code-interpreter)",
"@e2b/code-interpreter-python": "Python SDK (e2b-code-interpreter)",
"@e2b/data-extractor": "Charts (e2b-charts)",
"@e2b/code-interpreter-template": "Sandbox template (code-interpreter)",
};
const order = Object.keys(labels);
const byName = Object.fromEntries(data.releases.map(r => [r.name, r]));
const lines = order.filter(n => byName[n]).map(n => `• ${labels[n]} v${byName[n].newVersion}`);
process.stdout.write(lines.join("\n") || "• No packages were published");
')
rm -f .cs-status.json
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The 'Build release itinerary' step embeds a ~12-line multi-line node -e script (label map, ordering, formatting) directly in release.yml, rather than following the repo's own convention of putting release logic in standalone files under .github/scripts/ (alongside is_release.sh and is_release_for_package.sh). Consider extracting it to something like .github/scripts/build_release_itinerary.cjs so it can be linted/unit-tested and any syntax error surfaces before an actual release run.

Extended reasoning...

The Build release itinerary step (release.yml lines ~92-116) builds the Slack release itinerary with an inline node -e block: it reads .cs-status.json (written by pnpm changeset status --output=...), maps package names to human-readable labels, orders them, formats each as a bullet line, and falls back to a "No packages were published" message. This is meaningfully more logic than the other steps in this job — none of which embed multi-line JS — and it is only exercised on an actual release run against real changeset data.

The repo already has an established pattern for this kind of thing: .github/scripts/is_release.sh and .github/scripts/is_release_for_package.sh are standalone, executable scripts invoked from adjacent steps in the very same job. Those scripts can be shellchecked and run locally against fixture data; the inline node -e block cannot be linted, cannot be unit-tested, and any typo or logic bug (e.g. a broken template-string interpolation, a bad property access on data.releases) will only be discovered when a real release runs — at which point the failure blocks the Slack notification step (and, depending on ordering, could fail the whole preflight job) rather than being caught in CI or code review.

Concretely, imagine a future edit adds a fifth package to the labels map but a stray comma or bracket is introduced. YAML itself would still parse fine — the script is just a string value to the run: block — so no workflow-level validation catches it. The mistake would only surface the next time a release actually runs, when node -e throws a SyntaxError and the itinerary step fails, which is the worst possible moment to discover it (mid-release, without a fast local repro path).

Extracting the transform to .github/scripts/build_release_itinerary.cjs (reading .cs-status.json from cwd or an argv path, same as the shell scripts take package names as argv) would let it be run and tested locally with mock changeset-status JSON, matches the existing convention in this same job, and does not change behavior — the workflow step would just become node .github/scripts/build_release_itinerary.cjs.

This is a maintainability nit, not a correctness bug — the current inline script is not wrong, just harder to test and lint than the surrounding scripts in the same job. It sits on a non-critical path (a Slack notification), so it should never block merging this PR.

Comment thread .github/workflows/release.yml Outdated
Comment on lines 4 to 12
push:
branches:
- main
# A release that publishes nothing leaves the changesets intact, so it can be
# re-dispatched by hand without another push to main.
workflow_dispatch: {}

concurrency: Release-${{ github.ref }}-foxtrot

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The new workflow_dispatch: {} trigger has no branch guard, so it can be manually run on any branch (not just main) — unlike the push trigger, which is scoped to branches: [main]. is_release.sh only checks for the presence of unmerged .changeset/*.md files, not the ref, so dispatching on a feature branch that still has an open changeset (the normal state of an open PR) will let release commit a version bump and genuinely publish to npm/PyPI/tags from unreviewed code. Consider adding if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) to the preflight job (or the whole workflow) to restrict dispatch to main, matching the PR description's stated "recovery, not swapped in" intent.

Extended reasoning...

What the bug is: workflow_dispatch: {} is added at .github/workflows/release.yml:9 with no if condition or ref restriction. Compare this to the existing push trigger just above it, which is explicitly scoped with branches: [main]. GitHub Actions runs a manually-dispatched workflow using the code and ref selected in the dispatch UI/API — any branch, not just main. Nothing in this workflow constrains that choice.

The code path that triggers it: preflight's "Check if new version" step calls .github/scripts/is_release.sh, which (confirmed by reading the script) does nothing more than require('@changesets/read')(cwd).then(r => !!r.length) — it reports release=true purely because .changeset/*.md files exist in the checked-out tree. It never compares the current ref/SHA to main. An open feature/PR branch that adds a changeset (the completely normal state of any PR following this repo's changesets workflow) will therefore also report release=true when dispatched. From there, release only checks needs.preflight.outputs.release == 'true' plus !cancelled()/no-failure — there's no additional ref check anywhere downstream, and secrets (PYPI_TOKEN, the VERSION_BUMPER app token, id-token: write for npm OIDC) are all available on workflow_dispatch runs the same as on push.

Why existing code doesn't prevent it: the whole gating mechanism (preflightrelease) was designed around "does the checked-out ref have a pending changeset," not "is this ref main." That assumption was safe when the only trigger was push: branches: [main], but adding an unguarded workflow_dispatch breaks the implicit invariant without anything else changing to compensate.

Concrete walk-through:

  1. A contributor opens a feature branch feat/x and adds .changeset/silly-cats-jump.md (standard practice for any user-facing change in a changesets repo).
  2. Before merging, someone with write access to the repo (a maintainer doing a routine re-run, or simply exploring the Actions tab) opens the Release workflow's "Run workflow" dropdown and selects feat/x instead of main.
  3. preflight checks out feat/x, finds the changeset file, and sets release=true.
  4. release runs pnpm run version (bumping versions per the changeset), commits, and changesets/action publishes to npm/PyPI and pushes tags — all from feat/x, which never went through review or a merge to main.
  5. The result: unreviewed code is now live on public package registries, and main's branch protection was never consulted, contradicting the PR description's own statement that dispatch is "for recovery, not swapped in" (i.e., meant to be main-only).

Fix: gate the dispatch path to main, e.g. add if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) to the preflight job (and thus transitively everything gated on it), or apply the same check to every job/release directly.

Severity reasoning: all three verifiers independently confirmed the mechanism is real and newly introduced by this PR (the workflow previously only triggered on push-to-main). However, exploiting or misfiring it requires an actor who already has write/workflow-run access to deliberately (or accidentally) pick a non-default branch in the dispatch UI — it does not fire automatically on a normal PR merge, and two of the three verifiers explicitly recommended nit/hardening rather than blocking. I'm filing this as a nit: it's a real supply-chain hardening gap worth a one-line fix, but not something that breaks on the happy path or blocks this PR.

…y script

Addresses the bot review on #327.

`workflow_dispatch` offers every branch in the picker, so a feature branch
carrying changesets could publish real packages and push the version bump
to that branch; preflight now fails fast unless the run is on main.

`changesets/action` runs the publish script with `ignoreReturnCode: true`
and pushes each tag it can parse out of the output before it fails the
step, so a partial publish does get its tags to origin — but only the ones
that appear as `New tag:` lines. `git push --tags` after the branch lands
covers anything else sitting at the release commit; the checkout is
shallow and fetches no tags, so the only local tags are this run's, and
re-pushing the rest is a no-op.

The itinerary transform moves to `.github/scripts/`, next to
`is_release.sh`, where it can be run against fixture JSON instead of only
during a release; a package missing from the label map now shows up under
its workspace name rather than vanishing. The step is `continue-on-error`
and the messages fall back to a placeholder — it feeds a Slack notification
and should never be what blocks a release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mishushakov

Copy link
Copy Markdown
Member Author

Went through the bot review. Three findings addressed in 6dc0e1f, one declined.

Manual dispatch could publish from any branch (Codex) — correct and worth fixing. preflight now fails fast unless github.ref == 'refs/heads/main'.

"Push path drops release tags" (Bugbot) — the stated mechanism is wrong: changesets/action runs the publish script with ignoreReturnCode: true and pushes each tag it parses out of the output before failing the step, so a postPublish failure does get its tags to origin (run.ts). But it only pushes tags that appear as New tag: lines, so a git push --tags after the branch lands is cheap insurance — the checkout is shallow and fetches no tags, so the only local tags are this run's.

Inline node -e in the workflow (Claude) — fair; moved to .github/scripts/build_release_itinerary.cjs next to is_release.sh, where it runs against fixture JSON. A package missing from the label map now shows under its workspace name instead of vanishing, and the step is continue-on-error with a placeholder fallback in the messages, since a Slack itinerary should never block a release.

wistia/parse-tool-versions@v2.1.1 not pinned to a SHA (Cursor security) — declining here. The same action at the same tag is already used in four other steps of this file, and actions/checkout, changesets/action, docker/login-action, snok/install-poetry and pnpm/action-setup are all on mutable tags too. Pinning one of them is theatre; a repo-wide pass is worth doing as its own PR.

Verified the new push path against a throwaway shallow clone with a racing commit and two tags — one pre-pushed, one not: both end up on origin and reachable from main after the merge fallback.

Merging a changeset to main no longer publishes on its own; changesets
accumulate until someone dispatches the workflow. Nothing else depended on
the push trigger — no workflow here uses `workflow_run`, and every other
workflow fires on `pull_request` — so this is only a change to who decides
when a release happens.

The ref check added for the dispatch path now guards the only path there
is, which E2B's own release workflow does not do: with production releases
dispatchable from any branch in the picker, a feature branch carrying
changesets would publish for real and push the version bump to itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mishushakov mishushakov changed the title fix(ci): tag the version bump and survive a push race in the release workflow fix(ci): tag the version bump, survive push races, release on dispatch Aug 11, 2026
mishushakov and others added 2 commits August 11, 2026 15:51
`report-failure` did not list `preflight`, so whether a broken preflight —
a failed install, an erroring `is_release.sh`, a dispatch off `main` — pings
the release channel depended on `failure()` looking past this job's direct
dependencies. Listing it removes the doubt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`changesets/action` runs the publish script with `ignoreReturnCode: true`
and pushes every tag it parses out of the output before failing the step,
including the ones for private packages: in e2b-dev/E2B the private
`@e2b/python-sdk` tags all reach origin with no such step. So there is
nothing left at the release commit for `git push --tags` to catch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mishushakov added a commit to e2b-dev/desktop that referenced this pull request Aug 11, 2026
… releases

Adopts the refinements from e2b-dev/code-interpreter#327, which ported the
same E2B fixes to the sibling repo.

Releases are now cut by dispatching the workflow rather than by merging to
main, matching E2B and code-interpreter. Changesets accumulate until
someone dispatches, and since a release that publishes nothing leaves them
intact, a re-dispatch is also the recovery path. `workflow_dispatch` offers
every branch in the picker, so preflight refuses to run off main before
anything is checked out -- a feature branch carrying changesets would
otherwise publish real packages and push the bump to that branch.

This also settles the open question from the previous commits: gating
publish on the SDK tests no longer risks a flaky test blocking an
automatic release, because releases are no longer automatic.

The itinerary moves out of an inline `node -e` heredoc into
.github/scripts/build_release_itinerary.cjs. Besides being readable, it
fixes a silent omission: the inline version iterated a hardcoded package
order, so a package added later would have been left out of the Slack
message while still being published. The script sorts known packages
first and falls back to the workspace name for anything else. Verified:
an unlisted `@e2b/desktop-cli` now appears rather than vanishing.

The step is `continue-on-error` and the messages fall back to
'• (itinerary unavailable)'. It only feeds Slack, so a hiccup in
`changeset status` must not be the thing that fails preflight and blocks
the release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
main moved under this branch: #324 pinned every action to a commit SHA,
#323 upgraded to pnpm 10, and both added entries to `.tool-versions`.
Conflicts resolved by taking main's toolchain conventions and re-applying
this branch's changes on top of them:

- `.tool-versions` is main's verbatim. This branch had added `nodejs 24`;
  main instead keeps `node 20` as the build/test baseline and has the
  release job override it with Node 24 for npm 11, which is the same
  outcome with the reason written down. The preflight job now reads
  `TOOL_VERSION_NODE`.
- pnpm comes from `TOOL_VERSION_PNPM` in every job, as on main, rather
  than from `packageManager` via an unpinned `pnpm/action-setup@v4`. Main
  documents the sync requirement in `.tool-versions`, which closes the
  drift this branch was working around.
- Every action keeps main's pinned SHA, including in the jobs this branch
  restructured. Nothing here introduces a new action.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mishushakov added a commit to e2b-dev/E2B that referenced this pull request Aug 12, 2026
Three fixes found while porting this workflow to
`e2b-dev/code-interpreter`
([#327](e2b-dev/code-interpreter#327)).

**`release.yml` can be dispatched from any branch.** It is dispatch-only
and `workflow_dispatch` offers every branch in the picker, so a feature
branch carrying changesets would publish real packages to npm and PyPI
and push the version bump to itself. `preflight` now fails fast unless
the run is on `main`; candidates cut from a branch already go through
`release-candidate.yml`.

**The itinerary step can block a release.** It only feeds the Slack
messages, but a `changeset status` hiccup — or a typo in a future edit
to that inline `node -e` block, which no YAML validation catches — fails
`preflight` and stops the release. It is now `continue-on-error` with a
placeholder fallback in both messages, and the transform moved to
`.github/scripts/build_release_itinerary.cjs` next to `is_release.sh`,
where it can be run against fixture JSON. A package missing from the
label map now shows under its workspace name instead of being dropped by
`order.filter`, so a fourth publishable package would not silently
vanish from the notification.

**`report-failure` did not list `preflight`**, so whether a broken
preflight pings `#monitoring-releases` rested on `failure()` looking
past the job's direct dependencies — not documented either way, so the
job now depends on it explicitly.

Verified: the extracted script reproduces the current output exactly for
`e2b` / `@e2b/python-sdk` / `@e2b/cli`, in the same order, and handles
the empty and unlabeled-package cases; workflow validated against the
Actions schema; the script matches the repo's prettier config.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
mishushakov added a commit to e2b-dev/desktop that referenced this pull request Aug 12, 2026
#246)

* fix(ci): commit the version bump before publishing so release tags point at it

Ports the release workflow fixes from e2b-dev/E2B (#1463, #1469, #1484,

  $ git show '@e2b/desktop@2.3.1:packages/js-sdk/package.json' | grep version
    "version": "2.3.0",      # <- tagged 2.3.1

`changeset publish` tags whatever HEAD it publishes from, so committing
the bump afterwards left every tag on the commit preceding its own
version bump. Anything that builds from a git tag rather than a registry
got the previous release. The commit now happens first.

That is safe here without E2B's `workspace:^` prerequisite: no workspace
package depends on another by registry range (examples/ is outside
pnpm-workspace.yaml), so `changeset version` never touches
pnpm-lock.yaml -- true of every past release commit. The release commit
is complete before anything uploads, which also deletes the
`Update lock file` step that forced the commit to come last. A comment
records the invariant so a future cross-package dep cannot silently
reintroduce the cycle.

The push is now gated on `git tag --points-at HEAD` rather than on
publish success: the tags are what has to end up reachable, and
changesets/action pushes them before propagating a non-zero exit. A
non-fast-forward is reconciled with a merge, not a rebase, which would
strand the tags off the branch. A publish that uploads nothing leaves
the branch untouched with the changesets intact for a clean re-run.

`git add -A` replaces `commit -am`, which cannot stage new files.
`changeset version` writes each CHANGELOG.md fresh, so no release commit
has ever contained one -- `git ls-files | grep -i changelog` is empty.

In release.yml, `is_release` and `changes` are merged into one preflight
job: `changes` did a second full checkout and pnpm install to compute
js-sdk/python-sdk outputs that nothing consumed. Those flags now gate
the SDK test workflows, which run before publish. Slack gains start and
success notifications with a release itinerary; success is gated on
`needs.publish.result` rather than `success()`, which a skipped test job
would suppress. report-failure also needs preflight, so a release that
dies before deciding what to ship still notifies.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ci): don't let an empty changeset start a no-op release

Addresses three review findings on the previous commit.

1. An empty changeset (`changeset add --empty`) marks a change as needing
   no release, but `is_release.sh` counted it, so it started a release
   that published nothing. Verified against @changesets/read:

     [{"releases":[],"summary":"","id":"zz-empty-probe"}]  ->  release=true

   The version step's only effect is deleting the marker, and that commit
   is deliberately not pushed when no tags were created, so the marker
   survived on the branch and re-triggered a no-op release -- Slack
   notifications included -- on every later push. The old workflow
   committed unconditionally after publishing, so this was a regression
   introduced with the tag fix. `is_release.sh` now requires a changeset
   that actually releases a package; the marker is consumed by the next
   real release.

2. The tag gate exited 0 when publish reported success but tagged
   nothing. A release only starts when a changeset releases something, so
   `changeset version` bumped a package, so that state means those
   versions are already on the registry -- the tail of an earlier release
   whose branch push failed. Re-running cannot fix it: nothing is left to
   publish, nothing will ever be tagged, and exiting 0 reported success
   on every later push while main kept the old versions. It now fails
   with an error naming the manual recovery.

3. The push step's merge fallback needs a merge base, which a depth-1
   clone does not have once the branch has moved by more than the graft
   point, so the checkout now uses fetch-depth: 0.

Not changed: pinning `pnpm/action-setup` to a SHA. Every action in this
repo floats on a tag, including `actions/checkout`,
`actions/create-github-app-token` (which mints the push token) and
`changesets/action` (which runs the publish). Pinning the one action this
PR happened to bump would not reduce the exposure it describes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ci): scope the release commit to the paths versioning touches

`git add -A` staged the whole tree, so any non-ignored untracked file
written by an earlier step in the job -- a dependency lifecycle script
during `pnpm install`, a build -- would be committed and pushed to main
with release credentials. Scoped to `.changeset` and `packages`, which is
everything `changeset version` and postVersion write, and still uses `-A`
so new CHANGELOG.md files are staged. Directories rather than explicit
file lists, so a package added later stays covered.

pnpm-lock.yaml is now deliberately outside that scope. It is not supposed
to move at release time -- that is what makes the release commit complete
before anything is published -- so a change to it means the tree being
tagged is missing part of the release. Guarded with an error rather than
silently committing an incomplete release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ci): release on dispatch, extract the itinerary, stop it blocking releases

Adopts the refinements from e2b-dev/code-interpreter#327, which ported the
same E2B fixes to the sibling repo.

Releases are now cut by dispatching the workflow rather than by merging to
main, matching E2B and code-interpreter. Changesets accumulate until
someone dispatches, and since a release that publishes nothing leaves them
intact, a re-dispatch is also the recovery path. `workflow_dispatch` offers
every branch in the picker, so preflight refuses to run off main before
anything is checked out -- a feature branch carrying changesets would
otherwise publish real packages and push the bump to that branch.

This also settles the open question from the previous commits: gating
publish on the SDK tests no longer risks a flaky test blocking an
automatic release, because releases are no longer automatic.

The itinerary moves out of an inline `node -e` heredoc into
.github/scripts/build_release_itinerary.cjs. Besides being readable, it
fixes a silent omission: the inline version iterated a hardcoded package
order, so a package added later would have been left out of the Slack
message while still being published. The script sorts known packages
first and falls back to the workspace name for anything else. Verified:
an unlisted `@e2b/desktop-cli` now appears rather than vanishing.

The step is `continue-on-error` and the messages fall back to
'• (itinerary unavailable)'. It only feeds Slack, so a hiccup in
`changeset status` must not be the thing that fails preflight and blocks
the release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mishushakov
mishushakov merged commit 73f4974 into main Aug 12, 2026
16 checks passed
@mishushakov
mishushakov deleted the port-moresby branch August 12, 2026 12:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants