-
Notifications
You must be signed in to change notification settings - Fork 256
ci(release): split gen1/gen2 changesets and publish workflows #6537
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
base: main
Are you sure you want to change the base?
Changes from 4 commits
f5bf0cf
da240cc
01d91fb
ed10b60
9c937c0
aa210d7
9bd3209
3876201
4520f82
7d55399
c009f93
9f75102
b81b4b3
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,25 @@ | ||
| name: Release branch lock | ||
| description: >- | ||
| Temporarily blocks PR merges to a branch for the duration of a release run, via a | ||
| synthetic required status check. No-ops with a warning if the branch has no protection | ||
| configured, so a missing/unreadable protection rule never fails the release itself. | ||
| inputs: | ||
| mode: | ||
| description: 'lock or unlock' | ||
| required: true | ||
| branch: | ||
| description: 'Branch to lock or unlock' | ||
| required: true | ||
| token: | ||
| description: 'Token with Administration:write on the branch protection settings' | ||
| required: true | ||
| runs: | ||
| using: 'composite' | ||
| steps: | ||
| - name: Toggle release lock | ||
| shell: bash | ||
| env: | ||
| GH_TOKEN: ${{ inputs.token }} | ||
| run: | | ||
| node "${{ github.action_path }}/../../scripts/toggle-release-lock.mjs" \ | ||
| "${{ inputs.mode }}" "${{ inputs.branch }}" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| /** | ||
| * Copyright 2026 Adobe. All rights reserved. | ||
| * This file is licensed to you under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. You may obtain a copy | ||
| * of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software distributed under | ||
| * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS | ||
| * OF ANY KIND, either express or implied. See the License for the specific language | ||
| * governing permissions and limitations under the License. | ||
| */ | ||
|
|
||
| /** | ||
| * Blocks (or unblocks) PR merges into a release branch for the duration of a publish run, | ||
| * by adding (or removing) a synthetic required status check that never reports success. | ||
| * Required status checks only gate the GitHub merge button/API - they do not block a | ||
| * direct `git push` from an actor with push access, so this does not interfere with the | ||
| * release job's own commit-and-push step. | ||
| * | ||
| * ponytail: classic branch protection only; repos on the newer rulesets API instead of | ||
|
Contributor
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. nit:
Contributor
Author
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. Done! |
||
| * classic protection will get a 404 here and this no-ops with a warning rather than | ||
| * failing the release. Upgrade to the rulesets API if/when this repo migrates to it. | ||
| */ | ||
|
|
||
| import { execSync } from 'child_process'; | ||
|
|
||
| const [, , mode, branch] = process.argv; | ||
| const LOCK_CONTEXT = 'release/in-progress'; | ||
| const repo = process.env.GITHUB_REPOSITORY; | ||
|
|
||
| if (!['lock', 'unlock'].includes(mode) || !branch) { | ||
| console.error('Usage: toggle-release-lock.mjs <lock|unlock> <branch>'); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| function branchProtectionExists() { | ||
| try { | ||
| execSync(`gh api repos/${repo}/branches/${branch}/protection`, { | ||
| encoding: 'utf-8', | ||
| }); | ||
| return true; | ||
| } catch (err) { | ||
| console.warn( | ||
| `No branch protection found for '${branch}' (or this token can't read it) - skipping ${mode}. ` + | ||
| `Concurrent-merge protection during this release is not active for this branch. (${err.message})` | ||
| ); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| if (!branchProtectionExists()) { | ||
| process.exit(0); | ||
| } | ||
|
|
||
| // Add/remove only this one context via the dedicated endpoint, rather than reading | ||
| // the whole protection object and PUTing it back - a whole-object PUT only sends the | ||
| // fields this script knows about, silently resetting every other configured | ||
| // protection setting (allow_force_pushes, required_linear_history, etc.) to its API | ||
| // default on every lock and unlock. | ||
| const method = mode === 'lock' ? 'POST' : 'DELETE'; | ||
| execSync( | ||
| `gh api repos/${repo}/branches/${branch}/protection/required_status_checks/contexts -X ${method} --input -`, | ||
| { | ||
| input: JSON.stringify({ contexts: [LOCK_CONTEXT] }), | ||
| encoding: 'utf-8', | ||
| } | ||
| ); | ||
|
|
||
| console.log( | ||
| `${mode === 'lock' ? 'Locked' : 'Unlocked'} '${branch}': required status check '${LOCK_CONTEXT}' ${ | ||
| mode === 'lock' ? 'added' : 'removed' | ||
| }.` | ||
| ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| name: Publish Packages (2nd-gen) | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| tag: | ||
| description: 'NPM dist-tag' | ||
| required: false | ||
| default: 'beta' | ||
| dry_run: | ||
| description: 'Version packages and report the diff, but do not publish or push' | ||
| type: boolean | ||
| required: false | ||
| default: false | ||
| pull_request: | ||
| types: [labeled, synchronize] | ||
| push: | ||
| branches: | ||
| - gen2-beta | ||
|
|
||
| concurrency: | ||
| group: publish-2nd-gen-${{ github.ref }} | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| check-changesets: | ||
| runs-on: ubuntu-latest | ||
| if: >- | ||
| github.event_name == 'workflow_dispatch' || | ||
| github.event_name == 'push' || | ||
| (github.event_name == 'pull_request' && | ||
| contains(github.event.pull_request.labels.*.name, 'snapshot-release')) | ||
| outputs: | ||
| has_changesets: ${{ steps.check.outputs.has_changesets }} | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Check for 2nd-gen changesets | ||
| id: check | ||
| run: | | ||
| # Independent of 1st-gen in every trigger - this workflow never reads | ||
| # 1st-gen/.changeset/, including for its own snapshot-release PR path. | ||
| COUNT=$(ls -1 2nd-gen/.changeset/*.md 2>/dev/null | grep -v README | wc -l | tr -d ' ') | ||
| if [ "$COUNT" -eq 0 ]; then | ||
| echo "has_changesets=false" >> $GITHUB_OUTPUT | ||
| echo "No 2nd-gen changesets found - skipping publish" | ||
| else | ||
| echo "has_changesets=true" >> $GITHUB_OUTPUT | ||
| echo "Found $COUNT 2nd-gen changeset(s)" | ||
| fi | ||
|
|
||
| publish: | ||
| needs: check-changesets | ||
| if: needs.check-changesets.outputs.has_changesets == 'true' | ||
| runs-on: ubuntu-latest | ||
| environment: npm-publish | ||
| permissions: | ||
| contents: write # Required for git push (via RELEASE_BOT_TOKEN below) | ||
| env: | ||
| YARN_ENABLE_IMMUTABLE_INSTALLS: false | ||
| DRY_RUN: ${{ github.event.inputs.dry_run }} | ||
|
Contributor
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. nit:
Contributor
Author
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. Fixed: |
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || 'gen2-beta' }} | ||
|
Contributor
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. nit:
Contributor
Author
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. Added a one-line comment above the checkout step documenting that this is same-repo-only. Not building fork support for this — |
||
| fetch-depth: 0 | ||
|
|
||
| - name: Setup job and install dependencies | ||
| uses: ./.github/actions/setup-job | ||
|
|
||
| - name: Set Git identity | ||
| run: | | ||
| git config --global user.email "support+actions@github.com" | ||
| git config --global user.name "github-actions-bot" | ||
|
|
||
| - name: Determine release tag | ||
| id: extract-tag | ||
| env: | ||
| EVENT_NAME: ${{ github.event_name }} | ||
| INPUT_TAG: ${{ github.event.inputs.tag }} | ||
| run: | | ||
| if [ "$EVENT_NAME" == "pull_request" ]; then | ||
| # PRs with the snapshot-release label always use snapshot-test, same as | ||
| # 1st-gen's snapshot-release path - a separate, throwaway run either way. | ||
| WORKFLOW_TAG="snapshot-test" | ||
| else | ||
| WORKFLOW_TAG="${INPUT_TAG:-beta}" | ||
| fi | ||
| echo "tag=$WORKFLOW_TAG" >> $GITHUB_OUTPUT | ||
| echo "Using npm tag: $WORKFLOW_TAG" | ||
|
|
||
| - name: Lock gen2-beta during release | ||
|
Contributor
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. should-fix — race window not closed. The lock runs only after
Contributor
Author
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. Moved the lock to the first step, before checkout. Referencing the composite action via its full remote path ( |
||
| if: github.event_name != 'pull_request' && env.DRY_RUN != 'true' | ||
| uses: ./.github/actions/release-branch-lock | ||
| with: | ||
| mode: lock | ||
| branch: gen2-beta | ||
| token: ${{ secrets.RELEASE_BOT_TOKEN }} | ||
|
|
||
| - name: Enter changesets pre-release mode | ||
| if: github.event_name != 'pull_request' && !hashFiles('2nd-gen/.changeset/pre.json') | ||
| env: | ||
| TAG: ${{ steps.extract-tag.outputs.tag }} | ||
| working-directory: 2nd-gen | ||
| run: yarn changeset pre enter $TAG | ||
|
|
||
| - name: Verify NPM authentication | ||
|
Contributor
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. should-fix — verifies nothing. This step writes
Contributor
Author
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. Added |
||
| id: npm-auth | ||
| env: | ||
| NPM_TOKEN: ${{ secrets.ADOBE_BOT_NPM_TOKEN }} | ||
| run: | | ||
| echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc | ||
| echo "✓ NPM authentication configured for 2nd-gen (Adobe namespace)" | ||
|
|
||
| - name: Build all packages | ||
| run: yarn build | ||
|
|
||
| - name: Version packages | ||
| env: | ||
| EVENT_NAME: ${{ github.event_name }} | ||
| TAG: ${{ steps.extract-tag.outputs.tag }} | ||
| working-directory: 2nd-gen | ||
| run: | | ||
| if [ "$EVENT_NAME" == "pull_request" ]; then | ||
| # Throwaway snapshot version for PR testing only - never enters/touches | ||
| # the persistent pre-release (beta.N) state used by real gen2-beta releases. | ||
| yarn changeset version --snapshot $TAG | ||
| else | ||
| yarn changeset version | ||
| fi | ||
|
|
||
| - name: Capture released versions | ||
| id: versions | ||
| run: | | ||
| CORE_VERSION=$(node -p "require('./2nd-gen/packages/core/package.json').version") | ||
| SWC_VERSION=$(node -p "require('./2nd-gen/packages/swc/package.json').version") | ||
| echo "core=$CORE_VERSION" >> $GITHUB_OUTPUT | ||
| echo "swc=$SWC_VERSION" >> $GITHUB_OUTPUT | ||
|
|
||
| - name: Report dry-run diff | ||
| if: env.DRY_RUN == 'true' | ||
| run: | | ||
| echo "## Dry run - 2nd-gen version diff" >> $GITHUB_STEP_SUMMARY | ||
| echo '```diff' >> $GITHUB_STEP_SUMMARY | ||
| git diff --stat -- 2nd-gen/ >> $GITHUB_STEP_SUMMARY | ||
| echo '```' >> $GITHUB_STEP_SUMMARY | ||
| echo "Dry run requested - no packages were published and gen2-beta was not modified." >> $GITHUB_STEP_SUMMARY | ||
|
|
||
| - name: Refresh lockfile and rebuild | ||
| if: env.DRY_RUN != 'true' | ||
| run: | | ||
| yarn install --refresh-lockfile | ||
| yarn build | ||
|
|
||
| - name: Publish all packages | ||
| if: env.DRY_RUN != 'true' && steps.npm-auth.outcome == 'success' | ||
| env: | ||
| NODE_AUTH_TOKEN: ${{ secrets.ADOBE_BOT_NPM_TOKEN }} | ||
| TAG: ${{ steps.extract-tag.outputs.tag }} | ||
| working-directory: 2nd-gen | ||
| run: yarn changeset publish --no-git-tag --tag $TAG | ||
|
|
||
| - name: Commit and push changes | ||
| if: github.event_name != 'pull_request' && env.DRY_RUN != 'true' | ||
| env: | ||
| RELEASE_BOT_TOKEN: ${{ secrets.RELEASE_BOT_TOKEN }} | ||
| run: | | ||
| git add . | ||
|
Contributor
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. nit:
Contributor
Author
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. Scoped to |
||
| git commit --no-verify -m "chore: release 2nd-gen packages #publish" || echo "No changes to commit" | ||
| git remote set-url origin "https://x-access-token:${RELEASE_BOT_TOKEN}@github.com/${{ github.repository }}.git" | ||
| git pull --rebase origin gen2-beta | ||
| git push origin HEAD:gen2-beta | ||
|
|
||
| - name: Publish summary | ||
| if: always() | ||
| env: | ||
| TAG: ${{ steps.extract-tag.outputs.tag }} | ||
| EVENT_NAME: ${{ github.event_name }} | ||
| CORE_VERSION: ${{ steps.versions.outputs.core }} | ||
| SWC_VERSION: ${{ steps.versions.outputs.swc }} | ||
| AUTH_OUTCOME: ${{ steps.npm-auth.outcome }} | ||
| run: | | ||
| echo "## Publish summary (2nd-gen)" >> $GITHUB_STEP_SUMMARY | ||
| echo "" >> $GITHUB_STEP_SUMMARY | ||
| echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY | ||
| echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY | ||
| echo "| **Trigger** | \`${EVENT_NAME}\` |" >> $GITHUB_STEP_SUMMARY | ||
| echo "| **NPM tag** | \`${TAG}\` |" >> $GITHUB_STEP_SUMMARY | ||
| echo "| **Dry run** | \`${DRY_RUN}\` |" >> $GITHUB_STEP_SUMMARY | ||
| [ -n "$CORE_VERSION" ] && echo "| **@adobe/spectrum-wc-core** | \`${CORE_VERSION}\` |" >> $GITHUB_STEP_SUMMARY | ||
| [ -n "$SWC_VERSION" ] && echo "| **@adobe/spectrum-wc** | \`${SWC_VERSION}\` |" >> $GITHUB_STEP_SUMMARY | ||
|
|
||
| if [ "$DRY_RUN" == "true" ]; then | ||
| echo "🔎 Dry run only — nothing published or pushed" >> $GITHUB_STEP_SUMMARY | ||
| elif [ "$AUTH_OUTCOME" == "success" ]; then | ||
| echo "✅ Publish completed with tag \`${TAG}\`" >> $GITHUB_STEP_SUMMARY | ||
| else | ||
| echo "❌ Publish failed — NPM authentication did not succeed" >> $GITHUB_STEP_SUMMARY | ||
| fi | ||
|
|
||
| - name: Unlock gen2-beta after release | ||
| if: always() && github.event_name != 'pull_request' && env.DRY_RUN != 'true' | ||
|
Contributor
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. blocking — stuck-lock recovery.
Contributor
Author
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. Added |
||
| uses: ./.github/actions/release-branch-lock | ||
| with: | ||
| mode: unlock | ||
| branch: gen2-beta | ||
| token: ${{ secrets.RELEASE_BOT_TOKEN }} | ||
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.
should-fix: this description implies full coverage "for the duration of a release run," but both callers apply the lock only after checkout + dependency install, so a residual race window remains before the lock is active. Either document that caveat here or have callers lock pre-checkout via a direct
gh apicall.Uh oh!
There was an error while loading. Please reload this page.
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.
Resolved as a side effect of closing the lock-timing gap in both callers — the description's "for the duration of a release run" claim is now actually accurate, so no separate documented exception needed.