-
Notifications
You must be signed in to change notification settings - Fork 226
fix(ci): tag the version bump, survive push races, release on dispatch #327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b88d58a
6dc0e1f
b864215
6bafc58
a14d9c3
6be6880
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| // This script formats the JSON written by `changeset status --output=<file>` | ||
| // as the package list that goes into the release's Slack notifications. | ||
|
|
||
| const path = require('path') | ||
|
|
||
| // Packages are listed in this order; anything not named here still shows up, | ||
| // under its workspace name, after the ones that are. | ||
| 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 statusFile = process.argv[2] | ||
| if (!statusFile) { | ||
| console.error('Usage: build_release_itinerary.cjs <changeset-status.json>') | ||
| process.exit(1) | ||
| } | ||
|
|
||
| const order = Object.keys(labels) | ||
| const rank = (release) => { | ||
| const index = order.indexOf(release.name) | ||
| return index === -1 ? order.length : index | ||
| } | ||
|
|
||
| const { releases } = require(path.resolve(statusFile)) | ||
| const lines = [...releases] | ||
| .sort((a, b) => rank(a) - rank(b)) | ||
| .map( | ||
| (release) => | ||
| `• ${labels[release.name] ?? release.name} v${release.newVersion}` | ||
| ) | ||
|
|
||
| process.stdout.write(lines.join('\n') || '• No packages were published') |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,11 @@ | ||
| name: Release | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| # Releases are cut by hand, as in e2b-dev/E2B: merging a changeset to main no | ||
| # longer publishes on its own, so changesets accumulate until someone | ||
| # dispatches this. A release that publishes nothing leaves them intact, which | ||
| # makes a re-dispatch the recovery path too. | ||
| workflow_dispatch: {} | ||
|
|
||
| concurrency: Release-${{ github.ref }}-foxtrot | ||
|
|
||
|
|
@@ -12,12 +14,28 @@ permissions: | |
| contents: write | ||
|
|
||
| jobs: | ||
| is_release: | ||
| name: Is release? | ||
| preflight: | ||
| name: Release preflight | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| release: ${{ steps.version.outputs.release }} | ||
| js: ${{ steps.js.outputs.release }} | ||
| python: ${{ steps.python.outputs.release }} | ||
| charts: ${{ steps.charts.outputs.release }} | ||
| template: ${{ steps.template.outputs.release }} | ||
| itinerary: ${{ steps.itinerary.outputs.itinerary }} | ||
| steps: | ||
| - name: Check the ref | ||
| # `workflow_dispatch` offers every branch in the picker, and a feature | ||
| # branch carrying changesets would otherwise publish real packages and | ||
| # push the version bump to that branch. | ||
| if: github.ref != 'refs/heads/main' | ||
| env: | ||
| REF: ${{ github.ref }} | ||
| run: | | ||
| echo "::error::Releases must run on main; this run is on ${REF}." | ||
| exit 1 | ||
|
|
||
| - name: Checkout Repo | ||
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 | ||
|
|
||
|
|
@@ -56,77 +74,54 @@ jobs: | |
| IS_RELEASE=$(./.github/scripts/is_release.sh) | ||
| echo "release=$IS_RELEASE" >> "$GITHUB_OUTPUT" | ||
|
|
||
| changes: | ||
| name: Repository changes | ||
| needs: [is_release] | ||
| if: needs.is_release.outputs.release == 'true' | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| js: ${{ steps.js.outputs.release }} | ||
| python: ${{ steps.python.outputs.release }} | ||
| charts: ${{ steps.charts.outputs.release }} | ||
| template: ${{ steps.template.outputs.release }} | ||
| steps: | ||
| - name: Checkout Repo | ||
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 | ||
|
|
||
| - name: Parse .tool-versions | ||
| uses: wistia/parse-tool-versions@32f568a4ffd4bfa7720ebf93f171597d1ebc979a # v2.1.1 | ||
| with: | ||
| filename: '.tool-versions' | ||
| uppercase: 'true' | ||
| prefix: 'tool_version_' | ||
|
|
||
| - name: Install pnpm | ||
| uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 | ||
| id: pnpm-install | ||
| with: | ||
| version: ${{ env.TOOL_VERSION_PNPM }} | ||
|
|
||
| - name: Setup Node | ||
| uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 | ||
| with: | ||
| node-version: '${{ env.TOOL_VERSION_NODE }}' | ||
| registry-url: "https://registry.npmjs.org" | ||
| cache: pnpm | ||
| cache-dependency-path: pnpm-lock.yaml | ||
|
|
||
| - name: Configure pnpm | ||
| run: | | ||
| pnpm config set auto-install-peers true | ||
| pnpm config set exclude-links-from-lockfile true | ||
|
|
||
| - name: Install dependencies | ||
| run: pnpm install --frozen-lockfile | ||
|
|
||
| - name: Check JavasScript SDK Release | ||
| - name: Check JavaScript SDK Release | ||
| id: js | ||
| if: steps.version.outputs.release == 'true' | ||
| run: | | ||
| IS_RELEASE=$(./.github/scripts/is_release_for_package.sh "@e2b/code-interpreter") | ||
| echo "release=$IS_RELEASE" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Check Python SDK Release | ||
| id: python | ||
| if: steps.version.outputs.release == 'true' | ||
| run: | | ||
| IS_RELEASE=$(./.github/scripts/is_release_for_package.sh "@e2b/code-interpreter-python") | ||
| echo "release=$IS_RELEASE" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Check Charts SDK Release | ||
| id: charts | ||
| if: steps.version.outputs.release == 'true' | ||
| run: | | ||
| IS_RELEASE=$(./.github/scripts/is_release_for_package.sh "@e2b/data-extractor") | ||
| echo "release=$IS_RELEASE" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Check Template SDK Release | ||
| id: template | ||
| if: steps.version.outputs.release == 'true' | ||
| run: | | ||
| IS_RELEASE=$(./.github/scripts/is_release_for_package.sh "@e2b/code-interpreter-template") | ||
| echo "release=$IS_RELEASE" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Build release itinerary | ||
| id: itinerary | ||
| if: steps.version.outputs.release == 'true' | ||
| # This only feeds the Slack notifications, so it must never be the thing | ||
| # that blocks a release; the messages fall back to a placeholder. | ||
| continue-on-error: true | ||
| run: | | ||
| pnpm changeset status --output=.cs-status.json | ||
| ITINERARY=$(node ./.github/scripts/build_release_itinerary.cjs .cs-status.json) | ||
| rm -f .cs-status.json | ||
| { | ||
|
Comment on lines
104
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 The 'Build release itinerary' step embeds a ~12-line multi-line Extended reasoning...The The repo already has an established pattern for this kind of thing: Concretely, imagine a future edit adds a fifth package to the Extracting the transform to 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. |
||
| echo "itinerary<<EOF" | ||
| echo "$ITINERARY" | ||
| echo "EOF" | ||
| } >> "$GITHUB_OUTPUT" | ||
|
|
||
| charts-release: | ||
| name: Charts release | ||
| needs: [changes] | ||
| if: needs.changes.outputs.charts == 'true' | ||
| needs: [preflight] | ||
| if: needs.preflight.outputs.charts == 'true' | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| version: ${{ steps.output_version.outputs.version }} | ||
|
|
@@ -187,16 +182,15 @@ jobs: | |
| id: output_version | ||
| working-directory: ./chart_data_extractor | ||
| run: | | ||
| echo "::set-output name=version::$(pnpm pkg get version --workspaces=false | tr -d \\\")" | ||
| echo "version=$(pnpm pkg get version --workspaces=false | tr -d \\\")" >> "$GITHUB_OUTPUT" | ||
|
|
||
| build-docker-image: | ||
| name: Build Docker Image | ||
| runs-on: ubuntu-latest | ||
| needs: [changes, charts-release] | ||
| if: always() && | ||
| needs: [preflight, charts-release] | ||
| if: (!cancelled()) && | ||
| !contains(needs.*.result, 'failure') && | ||
| !contains(needs.*.result, 'cancelled') && | ||
| (needs.changes.outputs.template == 'true' || needs.changes.outputs.charts == 'true') | ||
| (needs.preflight.outputs.template == 'true' || needs.preflight.outputs.charts == 'true') | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 | ||
|
|
@@ -248,11 +242,10 @@ jobs: | |
| build-template: | ||
| name: Build E2B template | ||
| runs-on: ubuntu-latest | ||
| needs: [build-docker-image] | ||
| if: always() && | ||
| needs: [preflight, build-docker-image] | ||
| if: (!cancelled()) && | ||
| !contains(needs.*.result, 'failure') && | ||
| !contains(needs.*.result, 'cancelled') && | ||
| (needs.changes.outputs.template == 'true' || needs.changes.outputs.charts == 'true') | ||
| (needs.preflight.outputs.template == 'true' || needs.preflight.outputs.charts == 'true') | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 | ||
|
|
@@ -283,30 +276,30 @@ jobs: | |
|
|
||
| python-tests: | ||
| name: Python Tests | ||
| needs: [changes, build-template] | ||
| if: always() && | ||
| needs: [preflight, build-template] | ||
| if: (!cancelled()) && | ||
| !contains(needs.*.result, 'failure') && | ||
| !contains(needs.*.result, 'cancelled') && | ||
| needs.changes.outputs.template == 'true' | ||
| needs.preflight.outputs.template == 'true' | ||
| uses: ./.github/workflows/python_tests.yml | ||
| secrets: inherit | ||
|
|
||
| js-tests: | ||
| name: JS Tests | ||
| needs: [changes, build-template] | ||
| if: always() && | ||
| needs: [preflight, build-template] | ||
| if: (!cancelled()) && | ||
| !contains(needs.*.result, 'failure') && | ||
| !contains(needs.*.result, 'cancelled') && | ||
| needs.changes.outputs.template == 'true' | ||
| needs.preflight.outputs.template == 'true' | ||
| uses: ./.github/workflows/js_tests.yml | ||
| secrets: inherit | ||
|
|
||
| release: | ||
| needs: [python-tests, js-tests] | ||
| if: always() && | ||
| # Every upstream job is listed, not just the tests: a job that fails makes its | ||
| # dependents *skip*, and a skipped test job is not a failure — so gating on | ||
| # `needs.*.result` only works for the jobs this one depends on directly. | ||
| needs: [preflight, charts-release, build-docker-image, build-template, python-tests, js-tests] | ||
| if: (!cancelled()) && | ||
| !contains(needs.*.result, 'failure') && | ||
| !contains(needs.*.result, 'cancelled') && | ||
| (needs.changes.outputs.js == 'true' || needs.changes.outputs.python == 'true' || needs.changes.outputs.charts == 'true' || needs.changes.outputs.template == 'true') | ||
| needs.preflight.outputs.release == 'true' | ||
| name: Release | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
|
|
@@ -346,6 +339,8 @@ jobs: | |
| version: ${{ env.TOOL_VERSION_PNPM }} | ||
|
|
||
| - name: Setup Node.js 24 | ||
| # Deliberately ahead of the `node` baseline in .tool-versions: npm 11 is | ||
| # what OIDC trusted publishing needs (see #259). | ||
| uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 | ||
| with: | ||
| node-version: "24.x" | ||
|
|
@@ -365,6 +360,29 @@ jobs: | |
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Commit new versions | ||
| # Commit before publishing, because `changeset publish` tags whatever HEAD | ||
| # it publishes from — tagging afterwards is what left every tag on the | ||
| # commit before its own version bump (SDK-298). Nothing in the tree | ||
| # references the artifacts about to be uploaded — `changeset version` | ||
| # leaves `pnpm-lock.yaml` untouched here, since no workspace package | ||
| # depends on another — so the commit is already complete. It stays local | ||
| # until something is actually published, so a publish that uploads | ||
| # nothing leaves the branch untouched and the changesets intact for a | ||
| # re-run. | ||
| run: | | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "github-actions[bot]@users.noreply.github.com" | ||
|
|
||
| # `add -A`, not `commit -a`: `changeset version` writes each package's | ||
| # CHANGELOG.md as a new file the first time, which `-a` would drop. | ||
| git add -A | ||
| if git diff --cached --quiet; then | ||
| echo "::error::'changeset version' produced no changes, so there is no version bump to publish or tag." | ||
| exit 1 | ||
| fi | ||
| git commit -m "[skip ci] Release new versions" | ||
|
|
||
| - name: Release new versions | ||
| uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0 | ||
| with: | ||
|
|
@@ -375,20 +393,52 @@ jobs: | |
| NPM_TOKEN: "" # See https://github.com/changesets/changesets/issues/1152#issuecomment-3190884868 | ||
| PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} | ||
|
|
||
| - name: Update lock file | ||
| run: pnpm i --no-link --no-frozen-lockfile | ||
|
|
||
| - name: Commit new versions | ||
| - name: Push new versions | ||
| # Gate on the tags rather than on whether the publish step succeeded: they | ||
| # are what has to end up reachable. `changeset publish` tags at HEAD and the | ||
| # step above pushes them to origin, so if any exist — even from a publish | ||
| # that then failed partway — the commit they point at has to land, or they | ||
| # hang off no branch, which is the SDK-298 breakage this change prevents. | ||
| if: always() | ||
| run: | | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "github-actions[bot]@users.noreply.github.com" | ||
| git commit -am "[skip ci] Release new versions" || exit 0 | ||
| git push | ||
| if [ -z "$(git tag --points-at HEAD)" ]; then | ||
| echo "Nothing was published; the version bump stays local and the changesets are intact for a re-run." | ||
| exit 0 | ||
| fi | ||
|
|
||
| if ! git push; then | ||
| # A PR merged mid-release. Merge rather than rebase: the tags already | ||
| # point at this commit and rewriting it would strand them off the branch. | ||
| git fetch origin "${GITHUB_REF_NAME}" | ||
| git merge --no-edit FETCH_HEAD | ||
| git push | ||
| fi | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| report-start: | ||
| needs: [preflight] | ||
| if: needs.preflight.outputs.release == 'true' | ||
| name: Code Interpreter Release Started - Slack Notification | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Release Started - Slack Notification | ||
| uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # v2.4.0 | ||
| env: | ||
| SLACK_COLOR: "#3aa3e3" | ||
| SLACK_MESSAGE: | | ||
| :rocket: A new release has been triggered :hourglass_flowing_sand: | ||
|
|
||
| *Releasing:* | ||
| ${{ needs.preflight.outputs.itinerary || '• (itinerary unavailable)' }} | ||
| SLACK_TITLE: Code Interpreter Release Started | ||
| SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} | ||
| SLACK_CHANNEL: "monitoring-releases" | ||
|
|
||
| report-failure: | ||
| needs: [python-tests, js-tests, release] | ||
| # `preflight` included so a failure there is reported too, whether or not | ||
| # `failure()` looks past this job's direct dependencies. | ||
| needs: [preflight, charts-release, build-docker-image, build-template, python-tests, js-tests, release] | ||
| if: failure() | ||
| name: Code Interpreter Release Failed - Slack Notification | ||
| runs-on: ubuntu-latest | ||
|
|
@@ -401,3 +451,22 @@ jobs: | |
| SLACK_TITLE: Code Interpreter Release Failed | ||
| SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} | ||
| SLACK_CHANNEL: "monitoring-releases" | ||
|
|
||
| report-success: | ||
| needs: [preflight, release] | ||
| if: (!cancelled()) && needs.release.result == 'success' | ||
| name: Code Interpreter Release Succeeded - Slack Notification | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Release Succeeded - Slack Notification | ||
| uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # v2.4.0 | ||
| env: | ||
| SLACK_COLOR: "#36a64f" | ||
| SLACK_MESSAGE: | | ||
| :tada: A new version has been released successfully! :ship-it-parrot: | ||
|
|
||
| *Released:* | ||
| ${{ needs.preflight.outputs.itinerary || '• (itinerary unavailable)' }} | ||
| SLACK_TITLE: Code Interpreter Release Succeeded | ||
| SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} | ||
| SLACK_CHANNEL: "monitoring-releases" | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding
workflow_dispatchwithout 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 andgh workflow run --ref. If a maintainer dispatches this on a feature branch that has changesets, the release job will still publish packages/tags andgit pushthe version bump back to that branch instead ofmain, so the manual path should validategithub.ref == 'refs/heads/main'before publishing.Useful? React with 👍 / 👎.