From f5bf0cf0682d5267cdc06d502efc0fd9eaaabcb7 Mon Sep 17 00:00:00 2001 From: blunteshwar Date: Wed, 22 Jul 2026 11:21:51 +0530 Subject: [PATCH 01/11] ci(release): split gen1/gen2 changesets and publish workflows Splits the shared publish pipeline into independent gen1 (main -> next/latest) and gen2 (gen2-beta -> beta, pre-release mode) release streams per SWC-2315, each with its own changeset folder so a version bump for one generation no longer forces a decision about the other. Introduces a fine-grained bot PAT (RELEASE_BOT_TOKEN, to be added as a repo secret + branch-protection bypass actor) for the version-bump push, replacing the default GITHUB_TOKEN. Adds a release-branch-lock composite action that temporarily blocks PR merges to the target branch for the duration of a real release run, closing the window where a concurrent merge could land between this job's checkout and its final push-back. --- .changeset-2nd-gen/README.md | 38 ++++ .../close-button-2nd-gen-migration.md | 0 .../color-handle-migration.md | 0 .../color-loupe-adaptive-border.md | 0 .changeset-2nd-gen/config.json | 20 ++ .../feat-live-selection-controller.md | 0 .../fix-global-link-css-import-path.md | 0 ...ustrated-message-actions-slot-alignment.md | 0 ...ix-popover-trigger-pointer-active-stuck.md | 0 ...pagation-controller-optional-attributes.md | 0 .../fix-tabs-keyboard-activation-default.md | 0 .../pending-reusable-primitives.md | 0 .../publish-custom-elements-manifest.md | 0 .../soft-ravens-think.md | 0 .../spicy-melon-parade.md | 0 .../tabs-focusgroup-controller.md | 0 .changeset/README.md | 2 + .changeset/config.json | 5 +- .../actions/release-branch-lock/action.yaml | 25 +++ .github/scripts/toggle-release-lock.mjs | 107 +++++++++ .github/workflows/publish-2nd-gen.yml | 207 ++++++++++++++++++ .github/workflows/publish.yml | 85 ++++--- package.json | 1 + scripts/route-2nd-gen-changeset.mjs | 60 +++++ 24 files changed, 515 insertions(+), 35 deletions(-) create mode 100644 .changeset-2nd-gen/README.md rename {.changeset => .changeset-2nd-gen}/close-button-2nd-gen-migration.md (100%) rename {.changeset => .changeset-2nd-gen}/color-handle-migration.md (100%) rename {.changeset => .changeset-2nd-gen}/color-loupe-adaptive-border.md (100%) create mode 100644 .changeset-2nd-gen/config.json rename {.changeset => .changeset-2nd-gen}/feat-live-selection-controller.md (100%) rename {.changeset => .changeset-2nd-gen}/fix-global-link-css-import-path.md (100%) rename {.changeset => .changeset-2nd-gen}/fix-illustrated-message-actions-slot-alignment.md (100%) rename {.changeset => .changeset-2nd-gen}/fix-popover-trigger-pointer-active-stuck.md (100%) rename {.changeset => .changeset-2nd-gen}/fix-slot-attribute-propagation-controller-optional-attributes.md (100%) rename {.changeset => .changeset-2nd-gen}/fix-tabs-keyboard-activation-default.md (100%) rename {.changeset => .changeset-2nd-gen}/pending-reusable-primitives.md (100%) rename {.changeset => .changeset-2nd-gen}/publish-custom-elements-manifest.md (100%) rename {.changeset => .changeset-2nd-gen}/soft-ravens-think.md (100%) rename {.changeset => .changeset-2nd-gen}/spicy-melon-parade.md (100%) rename {.changeset => .changeset-2nd-gen}/tabs-focusgroup-controller.md (100%) create mode 100644 .github/actions/release-branch-lock/action.yaml create mode 100644 .github/scripts/toggle-release-lock.mjs create mode 100644 .github/workflows/publish-2nd-gen.yml create mode 100644 scripts/route-2nd-gen-changeset.mjs diff --git a/.changeset-2nd-gen/README.md b/.changeset-2nd-gen/README.md new file mode 100644 index 00000000000..5c79bc61a96 --- /dev/null +++ b/.changeset-2nd-gen/README.md @@ -0,0 +1,38 @@ +# Changesets (2nd-gen) + +This folder holds changesets for 2nd-gen packages only (`@adobe/spectrum-wc`, `@adobe/spectrum-wc-core`). 1st-gen (`@spectrum-web-components/*`) changesets live in [`.changeset/`](../.changeset/README.md). + +## Why a separate folder + +1st-gen ships to `next`/`latest` from `main`. 2nd-gen ships to `beta` from `gen2-beta` on its own cadence. `@changesets/cli` always reads `.changeset/` at the repo root, so it can't natively watch two folders — the `publish-2nd-gen.yml` workflow copies this folder's contents into `.changeset/` at release time before invoking the CLI. Authors should not rely on that copy step locally; always add changesets to the folder matching the generation you touched. + +## Adding a changeset for a 2nd-gen change + +```bash +yarn changeset:2nd-gen +``` + +This runs `yarn changeset` as usual and then moves the generated file from `.changeset/` into `.changeset-2nd-gen/` for you. Follow the same prompts as any other changeset: pick the affected package(s) (`@adobe/spectrum-wc` and/or `@adobe/spectrum-wc-core`), choose `major`/`minor`/`patch`, and write a summary. + +## Important: `@adobe/spectrum-wc-core` and component updates + +When a change touches `@adobe/spectrum-wc-core`, include the corresponding `@adobe/spectrum-wc` bump in the same changeset so the change shows up in the component-facing changelog — core changes are internal and don't otherwise surface there. + +## Example changeset + +```markdown +--- +'@adobe/spectrum-wc-core': patch +'@adobe/spectrum-wc': minor +--- + +- **Added**: Added new variant `tertiary` to `` [#9999](https://github.com/adobe/spectrum-web-components/pull/9999) +``` + +## Publishing process + +1. Changesets accumulate here as gen2 PRs merge to `main`. +2. `publish-2nd-gen.yml` (triggered on push to `gen2-beta`, or manually) copies these files into `.changeset/`, runs changesets in pre-release mode, and publishes to the `beta` npm tag. +3. Consumed changesets are removed as part of that release commit on `gen2-beta`. + +See [Changesets documentation](https://github.com/changesets/changesets) for details on the underlying tool. diff --git a/.changeset/close-button-2nd-gen-migration.md b/.changeset-2nd-gen/close-button-2nd-gen-migration.md similarity index 100% rename from .changeset/close-button-2nd-gen-migration.md rename to .changeset-2nd-gen/close-button-2nd-gen-migration.md diff --git a/.changeset/color-handle-migration.md b/.changeset-2nd-gen/color-handle-migration.md similarity index 100% rename from .changeset/color-handle-migration.md rename to .changeset-2nd-gen/color-handle-migration.md diff --git a/.changeset/color-loupe-adaptive-border.md b/.changeset-2nd-gen/color-loupe-adaptive-border.md similarity index 100% rename from .changeset/color-loupe-adaptive-border.md rename to .changeset-2nd-gen/color-loupe-adaptive-border.md diff --git a/.changeset-2nd-gen/config.json b/.changeset-2nd-gen/config.json new file mode 100644 index 00000000000..c959cc0c4e2 --- /dev/null +++ b/.changeset-2nd-gen/config.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", + "access": "public", + "baseBranch": "gen2-beta", + "changelog": [ + "@changesets/changelog-github", + { + "disableThanks": true, + "repo": "adobe/spectrum-web-components" + } + ], + "commit": false, + "ignore": ["@spectrum-web-components/*"], + "linked": [["@adobe/spectrum-wc", "@adobe/spectrum-wc-core"]], + "snapshot": { + "prereleaseTemplate": "{tag}.{datetime}", + "useCalculatedVersion": true + }, + "updateInternalDependencies": "patch" +} diff --git a/.changeset/feat-live-selection-controller.md b/.changeset-2nd-gen/feat-live-selection-controller.md similarity index 100% rename from .changeset/feat-live-selection-controller.md rename to .changeset-2nd-gen/feat-live-selection-controller.md diff --git a/.changeset/fix-global-link-css-import-path.md b/.changeset-2nd-gen/fix-global-link-css-import-path.md similarity index 100% rename from .changeset/fix-global-link-css-import-path.md rename to .changeset-2nd-gen/fix-global-link-css-import-path.md diff --git a/.changeset/fix-illustrated-message-actions-slot-alignment.md b/.changeset-2nd-gen/fix-illustrated-message-actions-slot-alignment.md similarity index 100% rename from .changeset/fix-illustrated-message-actions-slot-alignment.md rename to .changeset-2nd-gen/fix-illustrated-message-actions-slot-alignment.md diff --git a/.changeset/fix-popover-trigger-pointer-active-stuck.md b/.changeset-2nd-gen/fix-popover-trigger-pointer-active-stuck.md similarity index 100% rename from .changeset/fix-popover-trigger-pointer-active-stuck.md rename to .changeset-2nd-gen/fix-popover-trigger-pointer-active-stuck.md diff --git a/.changeset/fix-slot-attribute-propagation-controller-optional-attributes.md b/.changeset-2nd-gen/fix-slot-attribute-propagation-controller-optional-attributes.md similarity index 100% rename from .changeset/fix-slot-attribute-propagation-controller-optional-attributes.md rename to .changeset-2nd-gen/fix-slot-attribute-propagation-controller-optional-attributes.md diff --git a/.changeset/fix-tabs-keyboard-activation-default.md b/.changeset-2nd-gen/fix-tabs-keyboard-activation-default.md similarity index 100% rename from .changeset/fix-tabs-keyboard-activation-default.md rename to .changeset-2nd-gen/fix-tabs-keyboard-activation-default.md diff --git a/.changeset/pending-reusable-primitives.md b/.changeset-2nd-gen/pending-reusable-primitives.md similarity index 100% rename from .changeset/pending-reusable-primitives.md rename to .changeset-2nd-gen/pending-reusable-primitives.md diff --git a/.changeset/publish-custom-elements-manifest.md b/.changeset-2nd-gen/publish-custom-elements-manifest.md similarity index 100% rename from .changeset/publish-custom-elements-manifest.md rename to .changeset-2nd-gen/publish-custom-elements-manifest.md diff --git a/.changeset/soft-ravens-think.md b/.changeset-2nd-gen/soft-ravens-think.md similarity index 100% rename from .changeset/soft-ravens-think.md rename to .changeset-2nd-gen/soft-ravens-think.md diff --git a/.changeset/spicy-melon-parade.md b/.changeset-2nd-gen/spicy-melon-parade.md similarity index 100% rename from .changeset/spicy-melon-parade.md rename to .changeset-2nd-gen/spicy-melon-parade.md diff --git a/.changeset/tabs-focusgroup-controller.md b/.changeset-2nd-gen/tabs-focusgroup-controller.md similarity index 100% rename from .changeset/tabs-focusgroup-controller.md rename to .changeset-2nd-gen/tabs-focusgroup-controller.md diff --git a/.changeset/README.md b/.changeset/README.md index ef01193317e..68477baa6b1 100644 --- a/.changeset/README.md +++ b/.changeset/README.md @@ -1,5 +1,7 @@ # Changesets +> **1st-gen only.** This folder holds changesets for `@spectrum-web-components/*` packages, released from `main`. For `@adobe/spectrum-wc` / `@adobe/spectrum-wc-core` (2nd-gen) changes, use [`.changeset-2nd-gen/`](../.changeset-2nd-gen/README.md) (`yarn changeset:2nd-gen`) instead. + Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets) diff --git a/.changeset/config.json b/.changeset/config.json index 5af48256ab5..129305d1e62 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -19,9 +19,10 @@ ], "ignore": [ "@spectrum-web-components/1st-gen", - "@spectrum-web-components/2nd-gen" + "@spectrum-web-components/2nd-gen", + "@adobe/spectrum-wc", + "@adobe/spectrum-wc-core" ], - "linked": [["@adobe/spectrum-wc", "@adobe/spectrum-wc-core"]], "snapshot": { "prereleaseTemplate": "{tag}.{datetime}", "useCalculatedVersion": true diff --git a/.github/actions/release-branch-lock/action.yaml b/.github/actions/release-branch-lock/action.yaml new file mode 100644 index 00000000000..07293179e12 --- /dev/null +++ b/.github/actions/release-branch-lock/action.yaml @@ -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 }}" diff --git a/.github/scripts/toggle-release-lock.mjs b/.github/scripts/toggle-release-lock.mjs new file mode 100644 index 00000000000..1c596de3bdb --- /dev/null +++ b/.github/scripts/toggle-release-lock.mjs @@ -0,0 +1,107 @@ +#!/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 + * 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 '); + process.exit(1); +} + +function readProtection() { + try { + return JSON.parse( + execSync(`gh api repos/${repo}/branches/${branch}/protection`, { + encoding: 'utf-8', + }) + ); + } catch { + return null; + } +} + +const protection = readProtection(); + +if (!protection) { + 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.' + ); + process.exit(0); +} + +const contexts = new Set(protection.required_status_checks?.contexts ?? []); +if (mode === 'lock') { + contexts.add(LOCK_CONTEXT); +} else { + contexts.delete(LOCK_CONTEXT); +} + +const payload = { + required_status_checks: { + strict: protection.required_status_checks?.strict ?? false, + contexts: [...contexts], + }, + enforce_admins: protection.enforce_admins?.enabled ?? false, + required_pull_request_reviews: protection.required_pull_request_reviews + ? { + dismiss_stale_reviews: + protection.required_pull_request_reviews.dismiss_stale_reviews ?? + false, + require_code_owner_reviews: + protection.required_pull_request_reviews.require_code_owner_reviews ?? + false, + required_approving_review_count: + protection.required_pull_request_reviews + .required_approving_review_count ?? 1, + } + : null, + restrictions: protection.restrictions + ? { + users: protection.restrictions.users.map((user) => user.login), + teams: protection.restrictions.teams.map((team) => team.slug), + apps: protection.restrictions.apps.map((app) => app.slug), + } + : null, +}; + +execSync( + `gh api repos/${repo}/branches/${branch}/protection -X PUT --input -`, + { + input: JSON.stringify(payload), + encoding: 'utf-8', + } +); + +console.log( + `${mode === 'lock' ? 'Locked' : 'Unlocked'} '${branch}': required status check '${LOCK_CONTEXT}' ${ + mode === 'lock' ? 'added' : 'removed' + }.` +); diff --git a/.github/workflows/publish-2nd-gen.yml b/.github/workflows/publish-2nd-gen.yml new file mode 100644 index 00000000000..a6f89b34339 --- /dev/null +++ b/.github/workflows/publish-2nd-gen.yml @@ -0,0 +1,207 @@ +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 + push: + branches: + - gen2-beta + +concurrency: + group: publish-2nd-gen-${{ github.ref }} + cancel-in-progress: false + +jobs: + check-changesets: + runs-on: ubuntu-latest + 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: | + COUNT=$(ls -1 .changeset-2nd-gen/*.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 }} + steps: + - name: Checkout gen2-beta + uses: actions/checkout@v4 + with: + ref: gen2-beta + 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: + INPUT_TAG: ${{ github.event.inputs.tag }} + run: | + WORKFLOW_TAG="${INPUT_TAG:-beta}" + echo "tag=$WORKFLOW_TAG" >> $GITHUB_OUTPUT + echo "Using npm tag: $WORKFLOW_TAG" + + - name: Lock gen2-beta during release + if: env.DRY_RUN != 'true' + uses: ./.github/actions/release-branch-lock + with: + mode: lock + branch: gen2-beta + token: ${{ secrets.RELEASE_BOT_TOKEN }} + + - name: Load 2nd-gen changesets into .changeset/ + run: | + # @changesets/cli always reads .changeset/ at the repo root - there's no flag + # to point it at a different folder - so bring this branch's persisted 2nd-gen + # changeset state in before invoking it. Everything here is consumed by + # `changeset version` below and copied back out after. + cp .changeset-2nd-gen/config.json .changeset/config.json + if [ -f .changeset-2nd-gen/pre.json ]; then + cp .changeset-2nd-gen/pre.json .changeset/pre.json + fi + shopt -s nullglob + for f in .changeset-2nd-gen/*.md; do + [ "$(basename "$f")" = "README.md" ] && continue + cp "$f" .changeset/ + done + + - name: Enter changesets pre-release mode + if: ${{ !hashFiles('.changeset/pre.json') }} + env: + TAG: ${{ steps.extract-tag.outputs.tag }} + run: yarn changeset pre enter $TAG + + - name: Verify NPM authentication + 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 + run: yarn changeset version + + - 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 }} + run: yarn changeset publish --no-git-tag --tag $TAG + + - name: Persist 2nd-gen changeset state back to .changeset-2nd-gen/ + if: env.DRY_RUN != 'true' + run: | + # `changeset version` consumed everything copied in above and updated pre.json; + # mirror that back into the folder this branch actually commits. + rm -f .changeset-2nd-gen/*.md + for f in .changeset/*.md; do + [ "$(basename "$f")" = "README.md" ] && continue + mv "$f" .changeset-2nd-gen/ + done + cp .changeset/pre.json .changeset-2nd-gen/pre.json + rm -f .changeset/config.json .changeset/pre.json + + - name: Commit and push changes + if: env.DRY_RUN != 'true' + env: + RELEASE_BOT_TOKEN: ${{ secrets.RELEASE_BOT_TOKEN }} + run: | + git add . + 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 }} + 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 "| **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() && env.DRY_RUN != 'true' + uses: ./.github/actions/release-branch-lock + with: + mode: unlock + branch: gen2-beta + token: ${{ secrets.RELEASE_BOT_TOKEN }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c8bd031f91d..f0a6106ca39 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,4 +1,4 @@ -name: Publish Packages +name: Publish Packages (1st-gen) on: workflow_dispatch: @@ -35,31 +35,30 @@ jobs: - name: Check for changesets id: check run: | - if [ -z "$(ls -A .changeset/*.md 2>/dev/null | grep -v README)" ]; then + GEN1_COUNT=$(ls -1 .changeset/*.md 2>/dev/null | grep -v README | wc -l | tr -d ' ') + + # snapshot-release PRs exercise both gens under the snapshot-test tag; every + # other trigger (push to main, manual dispatch) only ever ships 1st-gen here - + # 2nd-gen changesets live in .changeset-2nd-gen/ and ship via publish-2nd-gen.yml. + GEN2_COUNT=0 + if [ "${{ github.event_name }}" = "pull_request" ]; then + GEN2_COUNT=$(ls -1 .changeset-2nd-gen/*.md 2>/dev/null | grep -v README | wc -l | tr -d ' ') + fi + + TOTAL_COUNT=$((GEN1_COUNT + GEN2_COUNT)) + if [ "$TOTAL_COUNT" -eq 0 ]; then echo "has_changesets=false" >> $GITHUB_OUTPUT echo "has_1st_gen_changesets=false" >> $GITHUB_OUTPUT echo "No changesets found - skipping publish" else echo "has_changesets=true" >> $GITHUB_OUTPUT - CHANGESET_COUNT=$(ls -1 .changeset/*.md 2>/dev/null | grep -v README | wc -l | tr -d ' ') - echo "Found $CHANGESET_COUNT changesets" - - # Check if any changeset mentions 1st-gen packages - # 1st-gen packages are @spectrum-web-components/* - HAS_1ST_GEN=false - for file in .changeset/*.md; do - if [ "$(basename "$file")" != "README.md" ]; then - if grep -qP "@spectrum-web-components/[a-z-]+" "$file"; then - HAS_1ST_GEN=true - break - fi - fi - done + echo "Found $GEN1_COUNT 1st-gen changeset(s), $GEN2_COUNT 2nd-gen changeset(s) (PR snapshot only)" - echo "has_1st_gen_changesets=$HAS_1ST_GEN" >> $GITHUB_OUTPUT - if [ "$HAS_1ST_GEN" = "true" ]; then + if [ "$GEN1_COUNT" -gt 0 ]; then + echo "has_1st_gen_changesets=true" >> $GITHUB_OUTPUT echo "Found 1st-gen changesets - React wrappers will be built and published" else + echo "has_1st_gen_changesets=false" >> $GITHUB_OUTPUT echo "No 1st-gen changesets found - React wrappers will be skipped" fi fi @@ -118,6 +117,23 @@ jobs: exit 1 fi + - name: Include 2nd-gen changesets for snapshot-release testing + if: github.event_name == 'pull_request' + run: | + shopt -s nullglob + for f in .changeset-2nd-gen/*.md; do + [ "$(basename "$f")" = "README.md" ] && continue + cp "$f" .changeset/ + done + + - name: Lock main during release + if: steps.extract-tag.outputs.tag == 'latest' + uses: ./.github/actions/release-branch-lock + with: + mode: lock + branch: main + token: ${{ secrets.RELEASE_BOT_TOKEN }} + - name: Verify OIDC token and configure NPM authentication id: npm-auth env: @@ -157,12 +173,8 @@ jobs: - name: Capture released versions id: versions run: | - CORE_VERSION=$(node -p "require('./2nd-gen/packages/core/package.json').version") BUNDLE_VERSION=$(node -p "require('./1st-gen/tools/bundle/package.json').version") - SWC_VERSION=$(node -p "require('./2nd-gen/packages/swc/package.json').version") - echo "core=$CORE_VERSION" >> $GITHUB_OUTPUT echo "bundle=$BUNDLE_VERSION" >> $GITHUB_OUTPUT - echo "swc=$SWC_VERSION" >> $GITHUB_OUTPUT - name: Refresh lockfile and rebuild run: | @@ -175,10 +187,10 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.ADOBE_BOT_NPM_TOKEN }} TAG: ${{ steps.extract-tag.outputs.tag }} run: | - # Changeset publishes all packages (1st-gen, core, and 2nd-gen) - # npm CLI automatically uses: - # - OIDC trusted publishing for 1st-gen packages (configured on npmjs.com) - # - Token authentication for 2nd-gen (from .npmrc) + # Changeset publishes 1st-gen packages (and, on a snapshot-release PR only, + # the 2nd-gen packages copied in above for end-to-end testing). + # npm CLI automatically uses OIDC trusted publishing for 1st-gen packages + # (configured on npmjs.com). if [ "$TAG" == "latest" ]; then yarn changeset publish --no-git-tag else @@ -205,7 +217,7 @@ jobs: local dir=$1 local max_attempts=3 local attempt=1 - + while [ $attempt -le $max_attempts ]; do echo "Publishing $dir (attempt $attempt/$max_attempts)..." if (cd "$dir" && $PUBLISH_CMD); then @@ -244,9 +256,7 @@ jobs: ACTOR: ${{ github.actor }} COMMIT_SHA: ${{ github.sha }} AUTH_OUTCOME: ${{ steps.npm-auth.outcome }} - CORE_VERSION: ${{ steps.versions.outputs.core }} BUNDLE_VERSION: ${{ steps.versions.outputs.bundle }} - SWC_VERSION: ${{ steps.versions.outputs.swc }} run: | echo "## Publish summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY @@ -275,14 +285,12 @@ jobs: echo "| **Commit** | \`${COMMIT_SHA}\` |" >> $GITHUB_STEP_SUMMARY fi - if [ -n "$CORE_VERSION" ] || [ -n "$BUNDLE_VERSION" ] || [ -n "$SWC_VERSION" ]; then + if [ -n "$BUNDLE_VERSION" ]; then echo "## Versions released" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Generation | Package | Version |" >> $GITHUB_STEP_SUMMARY echo "|------------|---------|---------|" >> $GITHUB_STEP_SUMMARY - [ -n "$CORE_VERSION" ] && echo "| Core | \`@adobe/spectrum-wc-core\` | \`${CORE_VERSION}\` |" >> $GITHUB_STEP_SUMMARY - [ -n "$BUNDLE_VERSION" ] && echo "| 1st gen | \`@spectrum-web-components/bundle\` | \`${BUNDLE_VERSION}\` |" >> $GITHUB_STEP_SUMMARY - [ -n "$SWC_VERSION" ] && echo "| 2nd gen | \`@adobe/spectrum-wc\` | \`${SWC_VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| 1st gen | \`@spectrum-web-components/bundle\` | \`${BUNDLE_VERSION}\` |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY fi @@ -294,12 +302,23 @@ jobs: - name: Commit and push changes if: steps.extract-tag.outputs.tag == 'latest' + env: + RELEASE_BOT_TOKEN: ${{ secrets.RELEASE_BOT_TOKEN }} run: | git add . git commit --no-verify -m "chore: release 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 main git push - name: Create git tag if: steps.extract-tag.outputs.tag == 'latest' run: node ./1st-gen/scripts/create-git-tag.js + + - name: Unlock main after release + if: always() && steps.extract-tag.outputs.tag == 'latest' + uses: ./.github/actions/release-branch-lock + with: + mode: unlock + branch: main + token: ${{ secrets.RELEASE_BOT_TOKEN }} diff --git a/package.json b/package.json index 61b121fcb5f..554523df716 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "lint:eslint": "echo 'Linting with ESLint...' && eslint ${LINT_PATH:-.} --cache", "lint:prettier": "echo 'Linting with Prettier...' && prettier ${LINT_PATH:-.} --cache --check", "lint:styles": "echo 'Linting with Stylelint...' && stylelint '${LINT_PATH:+$LINT_PATH/**/*.css}${LINT_PATH:-**/*.css}' --cache --allow-empty-input", + "changeset:2nd-gen": "node ./scripts/route-2nd-gen-changeset.mjs", "postinstall": "husky || true", "publish": "node ./scripts/publish.js", "publish:snapshot": "node ./scripts/publish.js --tag snapshot", diff --git a/scripts/route-2nd-gen-changeset.mjs b/scripts/route-2nd-gen-changeset.mjs new file mode 100644 index 00000000000..4b8ac114400 --- /dev/null +++ b/scripts/route-2nd-gen-changeset.mjs @@ -0,0 +1,60 @@ +#!/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. + */ + +/** + * `@changesets/cli` always writes new changesets into `.changeset/` at the repo root; + * it has no option to target a different folder. This script runs `yarn changeset` and + * then moves whatever file it just created into `.changeset-2nd-gen/`, so 2nd-gen changes + * land in the folder that `publish-2nd-gen.yml` actually reads. + */ + +import { execSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const root = path.resolve(__dirname, '..'); +const changesetDir = path.join(root, '.changeset'); +const gen2ChangesetDir = path.join(root, '.changeset-2nd-gen'); + +function listChangesetFiles(dir) { + return new Set( + fs + .readdirSync(dir) + .filter((name) => name.endsWith('.md') && name !== 'README.md') + ); +} + +const before = listChangesetFiles(changesetDir); + +execSync('yarn changeset', { stdio: 'inherit', cwd: root }); + +const after = listChangesetFiles(changesetDir); +const created = [...after].filter((name) => !before.has(name)); + +if (created.length === 0) { + console.log( + '\nNo new changeset file was created (command was likely aborted).' + ); + process.exit(0); +} + +for (const name of created) { + fs.renameSync( + path.join(changesetDir, name), + path.join(gen2ChangesetDir, name) + ); + console.log(`\nMoved .changeset/${name} -> .changeset-2nd-gen/${name}`); +} From da240cc909b42e1647abea68e33111c74737e970 Mon Sep 17 00:00:00 2001 From: blunteshwar Date: Wed, 22 Jul 2026 12:15:17 +0530 Subject: [PATCH 02/11] ci(release): fully separate changesets per generation Moves from a root-level .changeset + .changeset-2nd-gen split (which needed a copy-into-.changeset shim at release time, since @changesets/cli only ever reads .changeset/ relative to wherever it's invoked) to two genuinely independent changesets instances: 1st-gen/.changeset/ and 2nd-gen/.changeset/, each with its own config.json, README, and now its own @changesets/cli devDependency so `yarn changeset` resolves correctly from within either directory. Removes the runtime copy-in/copy-out step from publish-2nd-gen.yml entirely - it's no longer needed. Also fixes every other repo-root reference to the old single .changeset/ path that would have silently broken or degraded (lint-staged, the 1st-gen changelog/test-changed scripts, the CircleCI docs-only-change filter, and two doc links), since 1st-gen's own nested `workspaces` field means changesets invoked there can no longer see out-of-scope packages by design - those ignore/fixed entries are dropped as dead weight rather than kept as unusable safety nets. --- .ai/skills/documentation/SKILL.md | 3 +- .changeset-2nd-gen/README.md | 38 ------------ .circleci/config.yml | 2 +- .github/workflows/publish-2nd-gen.yml | 36 ++--------- .github/workflows/publish.yml | 43 ++++++------- {.changeset => 1st-gen/.changeset}/README.md | 2 +- .../.changeset}/config.json | 15 +---- .../.changeset}/quiet-owls-whisper.md | 0 1st-gen/scripts/escape-changelog-tags.js | 9 ++- 1st-gen/scripts/test-changes.js | 2 +- 1st-gen/scripts/update-global-changelog.js | 2 +- 2nd-gen/.changeset/README.md | 39 ++++++++++++ .../close-button-2nd-gen-migration.md | 0 .../.changeset}/color-handle-migration.md | 0 .../color-loupe-adaptive-border.md | 0 .../.changeset}/config.json | 1 - .../feat-live-selection-controller.md | 0 .../fix-global-link-css-import-path.md | 0 ...ustrated-message-actions-slot-alignment.md | 0 ...ix-popover-trigger-pointer-active-stuck.md | 0 ...pagation-controller-optional-attributes.md | 0 .../fix-tabs-keyboard-activation-default.md | 0 .../pending-reusable-primitives.md | 0 .../publish-custom-elements-manifest.md | 0 .../.changeset}/soft-ravens-think.md | 0 .../.changeset}/spicy-melon-parade.md | 0 .../.changeset}/tabs-focusgroup-controller.md | 0 2nd-gen/package.json | 2 + lint-staged.config.js | 4 +- package.json | 3 +- scripts/route-2nd-gen-changeset.mjs | 60 ------------------- yarn.lock | 2 + 32 files changed, 90 insertions(+), 173 deletions(-) delete mode 100644 .changeset-2nd-gen/README.md rename {.changeset => 1st-gen/.changeset}/README.md (90%) rename {.changeset => 1st-gen/.changeset}/config.json (58%) rename {.changeset => 1st-gen/.changeset}/quiet-owls-whisper.md (100%) create mode 100644 2nd-gen/.changeset/README.md rename {.changeset-2nd-gen => 2nd-gen/.changeset}/close-button-2nd-gen-migration.md (100%) rename {.changeset-2nd-gen => 2nd-gen/.changeset}/color-handle-migration.md (100%) rename {.changeset-2nd-gen => 2nd-gen/.changeset}/color-loupe-adaptive-border.md (100%) rename {.changeset-2nd-gen => 2nd-gen/.changeset}/config.json (91%) rename {.changeset-2nd-gen => 2nd-gen/.changeset}/feat-live-selection-controller.md (100%) rename {.changeset-2nd-gen => 2nd-gen/.changeset}/fix-global-link-css-import-path.md (100%) rename {.changeset-2nd-gen => 2nd-gen/.changeset}/fix-illustrated-message-actions-slot-alignment.md (100%) rename {.changeset-2nd-gen => 2nd-gen/.changeset}/fix-popover-trigger-pointer-active-stuck.md (100%) rename {.changeset-2nd-gen => 2nd-gen/.changeset}/fix-slot-attribute-propagation-controller-optional-attributes.md (100%) rename {.changeset-2nd-gen => 2nd-gen/.changeset}/fix-tabs-keyboard-activation-default.md (100%) rename {.changeset-2nd-gen => 2nd-gen/.changeset}/pending-reusable-primitives.md (100%) rename {.changeset-2nd-gen => 2nd-gen/.changeset}/publish-custom-elements-manifest.md (100%) rename {.changeset-2nd-gen => 2nd-gen/.changeset}/soft-ravens-think.md (100%) rename {.changeset-2nd-gen => 2nd-gen/.changeset}/spicy-melon-parade.md (100%) rename {.changeset-2nd-gen => 2nd-gen/.changeset}/tabs-focusgroup-controller.md (100%) delete mode 100644 scripts/route-2nd-gen-changeset.mjs diff --git a/.ai/skills/documentation/SKILL.md b/.ai/skills/documentation/SKILL.md index ddc4d3d1a2a..af47917c125 100644 --- a/.ai/skills/documentation/SKILL.md +++ b/.ai/skills/documentation/SKILL.md @@ -266,4 +266,5 @@ Data | Data - [In-product word list](https://spectrum.adobe.com/page/in-product-word-list/) - [Writing for errors](https://spectrum.adobe.com/page/writing-for-errors/) - [Writing for onboarding](https://spectrum.adobe.com/page/writing-for-onboarding/) -- [Writing a changeset](https://github.com/adobe/spectrum-web-components/blob/main/.changeset/README.md) +- [Writing a changeset (1st-gen)](https://github.com/adobe/spectrum-web-components/blob/main/1st-gen/.changeset/README.md) +- [Writing a changeset (2nd-gen)](https://github.com/adobe/spectrum-web-components/blob/main/2nd-gen/.changeset/README.md) diff --git a/.changeset-2nd-gen/README.md b/.changeset-2nd-gen/README.md deleted file mode 100644 index 5c79bc61a96..00000000000 --- a/.changeset-2nd-gen/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Changesets (2nd-gen) - -This folder holds changesets for 2nd-gen packages only (`@adobe/spectrum-wc`, `@adobe/spectrum-wc-core`). 1st-gen (`@spectrum-web-components/*`) changesets live in [`.changeset/`](../.changeset/README.md). - -## Why a separate folder - -1st-gen ships to `next`/`latest` from `main`. 2nd-gen ships to `beta` from `gen2-beta` on its own cadence. `@changesets/cli` always reads `.changeset/` at the repo root, so it can't natively watch two folders — the `publish-2nd-gen.yml` workflow copies this folder's contents into `.changeset/` at release time before invoking the CLI. Authors should not rely on that copy step locally; always add changesets to the folder matching the generation you touched. - -## Adding a changeset for a 2nd-gen change - -```bash -yarn changeset:2nd-gen -``` - -This runs `yarn changeset` as usual and then moves the generated file from `.changeset/` into `.changeset-2nd-gen/` for you. Follow the same prompts as any other changeset: pick the affected package(s) (`@adobe/spectrum-wc` and/or `@adobe/spectrum-wc-core`), choose `major`/`minor`/`patch`, and write a summary. - -## Important: `@adobe/spectrum-wc-core` and component updates - -When a change touches `@adobe/spectrum-wc-core`, include the corresponding `@adobe/spectrum-wc` bump in the same changeset so the change shows up in the component-facing changelog — core changes are internal and don't otherwise surface there. - -## Example changeset - -```markdown ---- -'@adobe/spectrum-wc-core': patch -'@adobe/spectrum-wc': minor ---- - -- **Added**: Added new variant `tertiary` to `` [#9999](https://github.com/adobe/spectrum-web-components/pull/9999) -``` - -## Publishing process - -1. Changesets accumulate here as gen2 PRs merge to `main`. -2. `publish-2nd-gen.yml` (triggered on push to `gen2-beta`, or manually) copies these files into `.changeset/`, runs changesets in pre-release mode, and publishes to the `beta` npm tag. -3. Consumed changesets are removed as part of that release commit on `gen2-beta`. - -See [Changesets documentation](https://github.com/changesets/changesets) for details on the underlying tool. diff --git a/.circleci/config.yml b/.circleci/config.yml index 68ca70aff6f..a07aec5d8be 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -69,7 +69,7 @@ commands: # Paths that never affect either gen's tests: contributor docs, AI # rules/skills, changeset files, linter config, scripts, root-level # markdown files, and the license file. - DOCS_PATTERN="^(CONTRIBUTOR-DOCS/|\.ai/|\.changeset/|linters/|scripts/|[^/]+\.md$|LICENSE$)" + DOCS_PATTERN="^(CONTRIBUTOR-DOCS/|\.ai/|(1st-gen|2nd-gen)/\.changeset/|linters/|scripts/|[^/]+\.md$|LICENSE$)" # Strip other-gen files and docs-only files; what remains is relevant. RELEVANT=$(echo "$CHANGED" | grep -v "^${EXCLUDE}" | grep -vE "${DOCS_PATTERN}" || true) diff --git a/.github/workflows/publish-2nd-gen.yml b/.github/workflows/publish-2nd-gen.yml index a6f89b34339..4d0c84d9a07 100644 --- a/.github/workflows/publish-2nd-gen.yml +++ b/.github/workflows/publish-2nd-gen.yml @@ -32,7 +32,7 @@ jobs: - name: Check for 2nd-gen changesets id: check run: | - COUNT=$(ls -1 .changeset-2nd-gen/*.md 2>/dev/null | grep -v README | wc -l | tr -d ' ') + 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" @@ -83,26 +83,11 @@ jobs: branch: gen2-beta token: ${{ secrets.RELEASE_BOT_TOKEN }} - - name: Load 2nd-gen changesets into .changeset/ - run: | - # @changesets/cli always reads .changeset/ at the repo root - there's no flag - # to point it at a different folder - so bring this branch's persisted 2nd-gen - # changeset state in before invoking it. Everything here is consumed by - # `changeset version` below and copied back out after. - cp .changeset-2nd-gen/config.json .changeset/config.json - if [ -f .changeset-2nd-gen/pre.json ]; then - cp .changeset-2nd-gen/pre.json .changeset/pre.json - fi - shopt -s nullglob - for f in .changeset-2nd-gen/*.md; do - [ "$(basename "$f")" = "README.md" ] && continue - cp "$f" .changeset/ - done - - name: Enter changesets pre-release mode - if: ${{ !hashFiles('.changeset/pre.json') }} + if: ${{ !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 @@ -117,6 +102,7 @@ jobs: run: yarn build - name: Version packages + working-directory: 2nd-gen run: yarn changeset version - name: Capture released versions @@ -147,21 +133,9 @@ jobs: 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: Persist 2nd-gen changeset state back to .changeset-2nd-gen/ - if: env.DRY_RUN != 'true' - run: | - # `changeset version` consumed everything copied in above and updated pre.json; - # mirror that back into the folder this branch actually commits. - rm -f .changeset-2nd-gen/*.md - for f in .changeset/*.md; do - [ "$(basename "$f")" = "README.md" ] && continue - mv "$f" .changeset-2nd-gen/ - done - cp .changeset/pre.json .changeset-2nd-gen/pre.json - rm -f .changeset/config.json .changeset/pre.json - - name: Commit and push changes if: env.DRY_RUN != 'true' env: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f0a6106ca39..3a90b6059cf 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -35,14 +35,15 @@ jobs: - name: Check for changesets id: check run: | - GEN1_COUNT=$(ls -1 .changeset/*.md 2>/dev/null | grep -v README | wc -l | tr -d ' ') + GEN1_COUNT=$(ls -1 1st-gen/.changeset/*.md 2>/dev/null | grep -v README | wc -l | tr -d ' ') # snapshot-release PRs exercise both gens under the snapshot-test tag; every # other trigger (push to main, manual dispatch) only ever ships 1st-gen here - - # 2nd-gen changesets live in .changeset-2nd-gen/ and ship via publish-2nd-gen.yml. + # 2nd-gen has its own independent changesets setup in 2nd-gen/.changeset/ and + # ships via publish-2nd-gen.yml. GEN2_COUNT=0 if [ "${{ github.event_name }}" = "pull_request" ]; then - GEN2_COUNT=$(ls -1 .changeset-2nd-gen/*.md 2>/dev/null | grep -v README | wc -l | tr -d ' ') + GEN2_COUNT=$(ls -1 2nd-gen/.changeset/*.md 2>/dev/null | grep -v README | wc -l | tr -d ' ') fi TOTAL_COUNT=$((GEN1_COUNT + GEN2_COUNT)) @@ -117,15 +118,6 @@ jobs: exit 1 fi - - name: Include 2nd-gen changesets for snapshot-release testing - if: github.event_name == 'pull_request' - run: | - shopt -s nullglob - for f in .changeset-2nd-gen/*.md; do - [ "$(basename "$f")" = "README.md" ] && continue - cp "$f" .changeset/ - done - - name: Lock main during release if: steps.extract-tag.outputs.tag == 'latest' uses: ./.github/actions/release-branch-lock @@ -158,18 +150,24 @@ jobs: - name: Confirm build artifacts run: yarn workspace @spectrum-web-components/1st-gen build:confirm - - name: Version packages + - name: Version 1st-gen packages env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ steps.extract-tag.outputs.tag }} run: | if [ "$TAG" == "latest" ]; then yarn workspace @spectrum-web-components/1st-gen changelog:global - yarn changeset version + (cd 1st-gen && yarn changeset version) else - yarn changeset version --snapshot $TAG + (cd 1st-gen && yarn changeset version --snapshot $TAG) fi + - name: Version 2nd-gen packages for snapshot-release testing + if: github.event_name == 'pull_request' + env: + TAG: ${{ steps.extract-tag.outputs.tag }} + run: (cd 2nd-gen && yarn changeset version --snapshot $TAG) + - name: Capture released versions id: versions run: | @@ -181,22 +179,27 @@ jobs: yarn install --refresh-lockfile yarn build - - name: Publish all packages + - name: Publish 1st-gen packages if: steps.npm-auth.outcome == 'success' env: NODE_AUTH_TOKEN: ${{ secrets.ADOBE_BOT_NPM_TOKEN }} TAG: ${{ steps.extract-tag.outputs.tag }} run: | - # Changeset publishes 1st-gen packages (and, on a snapshot-release PR only, - # the 2nd-gen packages copied in above for end-to-end testing). # npm CLI automatically uses OIDC trusted publishing for 1st-gen packages # (configured on npmjs.com). if [ "$TAG" == "latest" ]; then - yarn changeset publish --no-git-tag + (cd 1st-gen && yarn changeset publish --no-git-tag) else - yarn changeset publish --no-git-tag --tag $TAG + (cd 1st-gen && yarn changeset publish --no-git-tag --tag $TAG) fi + - name: Publish 2nd-gen packages for snapshot-release testing + if: github.event_name == 'pull_request' && steps.npm-auth.outcome == 'success' + env: + NODE_AUTH_TOKEN: ${{ secrets.ADOBE_BOT_NPM_TOKEN }} + TAG: ${{ steps.extract-tag.outputs.tag }} + run: (cd 2nd-gen && yarn changeset publish --no-git-tag --tag $TAG) + - name: Build React wrappers if: needs.check-changesets.outputs.has_1st_gen_changesets == 'true' run: yarn workspace @spectrum-web-components/1st-gen build:react diff --git a/.changeset/README.md b/1st-gen/.changeset/README.md similarity index 90% rename from .changeset/README.md rename to 1st-gen/.changeset/README.md index 68477baa6b1..4ebcc914a9d 100644 --- a/.changeset/README.md +++ b/1st-gen/.changeset/README.md @@ -1,6 +1,6 @@ # Changesets -> **1st-gen only.** This folder holds changesets for `@spectrum-web-components/*` packages, released from `main`. For `@adobe/spectrum-wc` / `@adobe/spectrum-wc-core` (2nd-gen) changes, use [`.changeset-2nd-gen/`](../.changeset-2nd-gen/README.md) (`yarn changeset:2nd-gen`) instead. +> **1st-gen only.** This folder holds changesets for `@spectrum-web-components/*` packages, released from `main`. It has its own independent `@changesets/cli` setup (this `README.md` + `config.json`), separate from 2nd-gen's. For `@adobe/spectrum-wc` / `@adobe/spectrum-wc-core` (2nd-gen) changes, use [`2nd-gen/.changeset/`](../../2nd-gen/.changeset/README.md) (`yarn changeset:2nd-gen`) instead. Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can diff --git a/.changeset/config.json b/1st-gen/.changeset/config.json similarity index 58% rename from .changeset/config.json rename to 1st-gen/.changeset/config.json index 129305d1e62..ee36868e395 100644 --- a/.changeset/config.json +++ b/1st-gen/.changeset/config.json @@ -10,19 +10,8 @@ } ], "commit": false, - "fixed": [ - [ - "@spectrum-web-components/*", - "!@spectrum-web-components/1st-gen", - "!@spectrum-web-components/2nd-gen" - ] - ], - "ignore": [ - "@spectrum-web-components/1st-gen", - "@spectrum-web-components/2nd-gen", - "@adobe/spectrum-wc", - "@adobe/spectrum-wc-core" - ], + "fixed": [["@spectrum-web-components/*"]], + "ignore": [], "snapshot": { "prereleaseTemplate": "{tag}.{datetime}", "useCalculatedVersion": true diff --git a/.changeset/quiet-owls-whisper.md b/1st-gen/.changeset/quiet-owls-whisper.md similarity index 100% rename from .changeset/quiet-owls-whisper.md rename to 1st-gen/.changeset/quiet-owls-whisper.md diff --git a/1st-gen/scripts/escape-changelog-tags.js b/1st-gen/scripts/escape-changelog-tags.js index 63d121615f6..d0b4a9a56e1 100644 --- a/1st-gen/scripts/escape-changelog-tags.js +++ b/1st-gen/scripts/escape-changelog-tags.js @@ -14,17 +14,20 @@ import { replaceInFile } from 'replace-in-file'; // make sure inline tags are escaped const tagOptions = { - files: '.changeset/*.md', + files: '{1st-gen,2nd-gen}/.changeset/*.md', from: /(?<=\n\s*-\s.*)`?(<\w+(-\w+)*[^>]*>)`?/g, to: '`$1`', }; try { const results = await replaceInFile(tagOptions); - console.log('Replaced unescaped tags in .changeset/*.md:', results); + console.log( + 'Replaced unescaped tags in {1st-gen,2nd-gen}/.changeset/*.md:', + results + ); } catch (error) { console.error( - 'Error occurred replacing unescaped tags in .changeset/*.md:', + 'Error occurred replacing unescaped tags in {1st-gen,2nd-gen}/.changeset/*.md:', error ); } diff --git a/1st-gen/scripts/test-changes.js b/1st-gen/scripts/test-changes.js index aa8e1e1936f..5369ed16077 100644 --- a/1st-gen/scripts/test-changes.js +++ b/1st-gen/scripts/test-changes.js @@ -27,7 +27,7 @@ const { browser = 'chrome' } = yargs(hideBin(process.argv)).argv; */ export const getChangedPackages = () => { let changedPackages = []; - const changesetDir = '../.changeset'; + const changesetDir = '.changeset'; try { // Check if .changeset directory exists diff --git a/1st-gen/scripts/update-global-changelog.js b/1st-gen/scripts/update-global-changelog.js index 5b167927de7..26b9f397098 100644 --- a/1st-gen/scripts/update-global-changelog.js +++ b/1st-gen/scripts/update-global-changelog.js @@ -103,7 +103,7 @@ function extractChanges(frontmatter, description, pattern, prefix = '') { * @returns {Promise} Object containing categorized changes for both 1st-gen and core */ async function processChangesets() { - const changesetDir = path.resolve(__dirname, '../../.changeset'); + const changesetDir = path.resolve(__dirname, '../.changeset'); // Use non-blocking I/O for directory read const files = await fsPromises.readdir(changesetDir); diff --git a/2nd-gen/.changeset/README.md b/2nd-gen/.changeset/README.md new file mode 100644 index 00000000000..98d69ad86d3 --- /dev/null +++ b/2nd-gen/.changeset/README.md @@ -0,0 +1,39 @@ +# Changesets (2nd-gen) + +This folder is an independent `@changesets/cli` setup for 2nd-gen packages only (`@adobe/spectrum-wc`, `@adobe/spectrum-wc-core`), with its own `config.json`. 1st-gen (`@spectrum-web-components/*`) has its own separate setup in [`1st-gen/.changeset/`](../../1st-gen/.changeset/README.md) — the two do not share a config, a folder, or a versioning run. + +## Why a separate setup + +1st-gen ships to `next`/`latest` from `main`. 2nd-gen ships to `beta` from `gen2-beta` on its own cadence, using changesets pre-release mode. `@changesets/cli` resolves `.changeset/` relative to wherever it's invoked from, so each generation gets its own instance by living in its own package directory (`2nd-gen/`) rather than sharing one at the repo root. + +## Adding a changeset for a 2nd-gen change + +```bash +yarn changeset:2nd-gen +``` + +This runs `yarn changeset` from within `2nd-gen/`, so the file lands here automatically — no manual routing needed. Follow the usual prompts: pick the affected package(s) (`@adobe/spectrum-wc` and/or `@adobe/spectrum-wc-core`), choose `major`/`minor`/`patch`, and write a summary. + +## Important: `@adobe/spectrum-wc-core` and component updates + +When a change touches `@adobe/spectrum-wc-core`, include the corresponding `@adobe/spectrum-wc` bump in the same changeset so the change shows up in the component-facing changelog — core changes are internal and don't otherwise surface there. + +## Example changeset + +```markdown +--- +'@adobe/spectrum-wc-core': patch +'@adobe/spectrum-wc': minor +--- + +- **Added**: Added new variant `tertiary` to `` [#9999](https://github.com/adobe/spectrum-web-components/pull/9999) +``` + +## Publishing process + +1. Changesets accumulate here as gen2 PRs merge to `main`. +2. An operator syncs `main` into `gen2-beta` when ready to cut a release. +3. `publish-2nd-gen.yml` (triggered on push to `gen2-beta`, or manually) runs changesets from within `2nd-gen/` in pre-release mode and publishes to the `beta` npm tag. +4. Consumed changesets are removed as part of that release commit on `gen2-beta`. + +See [Changesets documentation](https://github.com/changesets/changesets) for details on the underlying tool. diff --git a/.changeset-2nd-gen/close-button-2nd-gen-migration.md b/2nd-gen/.changeset/close-button-2nd-gen-migration.md similarity index 100% rename from .changeset-2nd-gen/close-button-2nd-gen-migration.md rename to 2nd-gen/.changeset/close-button-2nd-gen-migration.md diff --git a/.changeset-2nd-gen/color-handle-migration.md b/2nd-gen/.changeset/color-handle-migration.md similarity index 100% rename from .changeset-2nd-gen/color-handle-migration.md rename to 2nd-gen/.changeset/color-handle-migration.md diff --git a/.changeset-2nd-gen/color-loupe-adaptive-border.md b/2nd-gen/.changeset/color-loupe-adaptive-border.md similarity index 100% rename from .changeset-2nd-gen/color-loupe-adaptive-border.md rename to 2nd-gen/.changeset/color-loupe-adaptive-border.md diff --git a/.changeset-2nd-gen/config.json b/2nd-gen/.changeset/config.json similarity index 91% rename from .changeset-2nd-gen/config.json rename to 2nd-gen/.changeset/config.json index c959cc0c4e2..be87441f931 100644 --- a/.changeset-2nd-gen/config.json +++ b/2nd-gen/.changeset/config.json @@ -10,7 +10,6 @@ } ], "commit": false, - "ignore": ["@spectrum-web-components/*"], "linked": [["@adobe/spectrum-wc", "@adobe/spectrum-wc-core"]], "snapshot": { "prereleaseTemplate": "{tag}.{datetime}", diff --git a/.changeset-2nd-gen/feat-live-selection-controller.md b/2nd-gen/.changeset/feat-live-selection-controller.md similarity index 100% rename from .changeset-2nd-gen/feat-live-selection-controller.md rename to 2nd-gen/.changeset/feat-live-selection-controller.md diff --git a/.changeset-2nd-gen/fix-global-link-css-import-path.md b/2nd-gen/.changeset/fix-global-link-css-import-path.md similarity index 100% rename from .changeset-2nd-gen/fix-global-link-css-import-path.md rename to 2nd-gen/.changeset/fix-global-link-css-import-path.md diff --git a/.changeset-2nd-gen/fix-illustrated-message-actions-slot-alignment.md b/2nd-gen/.changeset/fix-illustrated-message-actions-slot-alignment.md similarity index 100% rename from .changeset-2nd-gen/fix-illustrated-message-actions-slot-alignment.md rename to 2nd-gen/.changeset/fix-illustrated-message-actions-slot-alignment.md diff --git a/.changeset-2nd-gen/fix-popover-trigger-pointer-active-stuck.md b/2nd-gen/.changeset/fix-popover-trigger-pointer-active-stuck.md similarity index 100% rename from .changeset-2nd-gen/fix-popover-trigger-pointer-active-stuck.md rename to 2nd-gen/.changeset/fix-popover-trigger-pointer-active-stuck.md diff --git a/.changeset-2nd-gen/fix-slot-attribute-propagation-controller-optional-attributes.md b/2nd-gen/.changeset/fix-slot-attribute-propagation-controller-optional-attributes.md similarity index 100% rename from .changeset-2nd-gen/fix-slot-attribute-propagation-controller-optional-attributes.md rename to 2nd-gen/.changeset/fix-slot-attribute-propagation-controller-optional-attributes.md diff --git a/.changeset-2nd-gen/fix-tabs-keyboard-activation-default.md b/2nd-gen/.changeset/fix-tabs-keyboard-activation-default.md similarity index 100% rename from .changeset-2nd-gen/fix-tabs-keyboard-activation-default.md rename to 2nd-gen/.changeset/fix-tabs-keyboard-activation-default.md diff --git a/.changeset-2nd-gen/pending-reusable-primitives.md b/2nd-gen/.changeset/pending-reusable-primitives.md similarity index 100% rename from .changeset-2nd-gen/pending-reusable-primitives.md rename to 2nd-gen/.changeset/pending-reusable-primitives.md diff --git a/.changeset-2nd-gen/publish-custom-elements-manifest.md b/2nd-gen/.changeset/publish-custom-elements-manifest.md similarity index 100% rename from .changeset-2nd-gen/publish-custom-elements-manifest.md rename to 2nd-gen/.changeset/publish-custom-elements-manifest.md diff --git a/.changeset-2nd-gen/soft-ravens-think.md b/2nd-gen/.changeset/soft-ravens-think.md similarity index 100% rename from .changeset-2nd-gen/soft-ravens-think.md rename to 2nd-gen/.changeset/soft-ravens-think.md diff --git a/.changeset-2nd-gen/spicy-melon-parade.md b/2nd-gen/.changeset/spicy-melon-parade.md similarity index 100% rename from .changeset-2nd-gen/spicy-melon-parade.md rename to 2nd-gen/.changeset/spicy-melon-parade.md diff --git a/.changeset-2nd-gen/tabs-focusgroup-controller.md b/2nd-gen/.changeset/tabs-focusgroup-controller.md similarity index 100% rename from .changeset-2nd-gen/tabs-focusgroup-controller.md rename to 2nd-gen/.changeset/tabs-focusgroup-controller.md diff --git a/2nd-gen/package.json b/2nd-gen/package.json index 5c8d5b9d963..5de50711881 100644 --- a/2nd-gen/package.json +++ b/2nd-gen/package.json @@ -39,6 +39,8 @@ "packages/*" ], "devDependencies": { + "@changesets/changelog-github": "0.5.1", + "@changesets/cli": "2.29.7", "@storybook/addon-vitest": "10.3.5", "eslint": "9.39.2", "eslint-plugin-simple-import-sort": "12.1.1", diff --git a/lint-staged.config.js b/lint-staged.config.js index 7fc3a6d2767..ea0294dd608 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -28,7 +28,9 @@ export default { 'yarn install --refresh-lockfile', 'git add 1st-gen/tools/base/src/version.ts 2nd-gen/packages/core/element/version.ts yarn.lock', ], - '.changeset/*.md': ['node 1st-gen/scripts/escape-changelog-tags.js'], + '{1st-gen,2nd-gen}/.changeset/*.md': [ + 'node 1st-gen/scripts/escape-changelog-tags.js', + ], '!(*.css|*.ts)': [ 'prettier --cache --no-error-on-unmatched-pattern --ignore-unknown --log-level silent --write', ], diff --git a/package.json b/package.json index 554523df716..baee4a307b7 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "lint:eslint": "echo 'Linting with ESLint...' && eslint ${LINT_PATH:-.} --cache", "lint:prettier": "echo 'Linting with Prettier...' && prettier ${LINT_PATH:-.} --cache --check", "lint:styles": "echo 'Linting with Stylelint...' && stylelint '${LINT_PATH:+$LINT_PATH/**/*.css}${LINT_PATH:-**/*.css}' --cache --allow-empty-input", - "changeset:2nd-gen": "node ./scripts/route-2nd-gen-changeset.mjs", + "changeset:1st-gen": "cd 1st-gen && yarn changeset", + "changeset:2nd-gen": "cd 2nd-gen && yarn changeset", "postinstall": "husky || true", "publish": "node ./scripts/publish.js", "publish:snapshot": "node ./scripts/publish.js --tag snapshot", diff --git a/scripts/route-2nd-gen-changeset.mjs b/scripts/route-2nd-gen-changeset.mjs deleted file mode 100644 index 4b8ac114400..00000000000 --- a/scripts/route-2nd-gen-changeset.mjs +++ /dev/null @@ -1,60 +0,0 @@ -#!/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. - */ - -/** - * `@changesets/cli` always writes new changesets into `.changeset/` at the repo root; - * it has no option to target a different folder. This script runs `yarn changeset` and - * then moves whatever file it just created into `.changeset-2nd-gen/`, so 2nd-gen changes - * land in the folder that `publish-2nd-gen.yml` actually reads. - */ - -import { execSync } from 'child_process'; -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const root = path.resolve(__dirname, '..'); -const changesetDir = path.join(root, '.changeset'); -const gen2ChangesetDir = path.join(root, '.changeset-2nd-gen'); - -function listChangesetFiles(dir) { - return new Set( - fs - .readdirSync(dir) - .filter((name) => name.endsWith('.md') && name !== 'README.md') - ); -} - -const before = listChangesetFiles(changesetDir); - -execSync('yarn changeset', { stdio: 'inherit', cwd: root }); - -const after = listChangesetFiles(changesetDir); -const created = [...after].filter((name) => !before.has(name)); - -if (created.length === 0) { - console.log( - '\nNo new changeset file was created (command was likely aborted).' - ); - process.exit(0); -} - -for (const name of created) { - fs.renameSync( - path.join(changesetDir, name), - path.join(gen2ChangesetDir, name) - ); - console.log(`\nMoved .changeset/${name} -> .changeset-2nd-gen/${name}`); -} diff --git a/yarn.lock b/yarn.lock index 434897ac3aa..210d677f93c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6775,6 +6775,8 @@ __metadata: version: 0.0.0-use.local resolution: "@spectrum-web-components/2nd-gen@workspace:2nd-gen" dependencies: + "@changesets/changelog-github": "npm:0.5.1" + "@changesets/cli": "npm:2.29.7" "@storybook/addon-vitest": "npm:10.3.5" eslint: "npm:9.39.2" eslint-plugin-simple-import-sort: "npm:12.1.1" From 01d91fb39a6cd57ad7d365a7f944b3a2d79fbc80 Mon Sep 17 00:00:00 2001 From: blunteshwar Date: Wed, 22 Jul 2026 12:22:35 +0530 Subject: [PATCH 03/11] ci(release): make snapshot-release path fully independent per gen publish.yml no longer touches 2nd-gen at all, even for snapshot-release PRs - it never reads 2nd-gen/.changeset/ and has no 2nd-gen version/publish steps. publish-2nd-gen.yml gets its own pull_request (labeled/synchronize, gated to snapshot-release) trigger and handles its own snapshot testing independently: checks out the PR's own head ref, versions with --snapshot snapshot-test (never touching the persistent pre-release state), publishes under snapshot-test, and skips the branch lock and gen2-beta commit/push entirely since a PR run never touches gen2-beta. --- .github/workflows/publish-2nd-gen.yml | 44 ++++++++++++++++++++++----- .github/workflows/publish.yml | 40 +++++------------------- 2 files changed, 43 insertions(+), 41 deletions(-) diff --git a/.github/workflows/publish-2nd-gen.yml b/.github/workflows/publish-2nd-gen.yml index 4d0c84d9a07..2116e43596a 100644 --- a/.github/workflows/publish-2nd-gen.yml +++ b/.github/workflows/publish-2nd-gen.yml @@ -12,6 +12,8 @@ on: type: boolean required: false default: false + pull_request: + types: [labeled, synchronize] push: branches: - gen2-beta @@ -23,6 +25,11 @@ concurrency: 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: @@ -32,6 +39,8 @@ jobs: - 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 @@ -52,10 +61,10 @@ jobs: YARN_ENABLE_IMMUTABLE_INSTALLS: false DRY_RUN: ${{ github.event.inputs.dry_run }} steps: - - name: Checkout gen2-beta + - name: Checkout uses: actions/checkout@v4 with: - ref: gen2-beta + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || 'gen2-beta' }} fetch-depth: 0 - name: Setup job and install dependencies @@ -69,14 +78,21 @@ jobs: - name: Determine release tag id: extract-tag env: + EVENT_NAME: ${{ github.event_name }} INPUT_TAG: ${{ github.event.inputs.tag }} run: | - WORKFLOW_TAG="${INPUT_TAG:-beta}" + 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 - if: env.DRY_RUN != 'true' + if: github.event_name != 'pull_request' && env.DRY_RUN != 'true' uses: ./.github/actions/release-branch-lock with: mode: lock @@ -84,7 +100,7 @@ jobs: token: ${{ secrets.RELEASE_BOT_TOKEN }} - name: Enter changesets pre-release mode - if: ${{ !hashFiles('2nd-gen/.changeset/pre.json') }} + if: github.event_name != 'pull_request' && !hashFiles('2nd-gen/.changeset/pre.json') env: TAG: ${{ steps.extract-tag.outputs.tag }} working-directory: 2nd-gen @@ -102,8 +118,18 @@ jobs: run: yarn build - name: Version packages + env: + EVENT_NAME: ${{ github.event_name }} + TAG: ${{ steps.extract-tag.outputs.tag }} working-directory: 2nd-gen - run: yarn changeset version + 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 @@ -137,7 +163,7 @@ jobs: run: yarn changeset publish --no-git-tag --tag $TAG - name: Commit and push changes - if: env.DRY_RUN != 'true' + if: github.event_name != 'pull_request' && env.DRY_RUN != 'true' env: RELEASE_BOT_TOKEN: ${{ secrets.RELEASE_BOT_TOKEN }} run: | @@ -151,6 +177,7 @@ jobs: 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 }} @@ -159,6 +186,7 @@ jobs: 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 @@ -173,7 +201,7 @@ jobs: fi - name: Unlock gen2-beta after release - if: always() && env.DRY_RUN != 'true' + if: always() && github.event_name != 'pull_request' && env.DRY_RUN != 'true' uses: ./.github/actions/release-branch-lock with: mode: unlock diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3a90b6059cf..b35d71110e1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -35,33 +35,20 @@ jobs: - name: Check for changesets id: check run: | + # This workflow is 1st-gen only, always - independent of 2nd-gen in every + # trigger (push, manual dispatch, and snapshot-release PRs). 2nd-gen has its + # own changesets setup in 2nd-gen/.changeset/ and its own workflow, + # publish-2nd-gen.yml, including its own snapshot-release PR path. GEN1_COUNT=$(ls -1 1st-gen/.changeset/*.md 2>/dev/null | grep -v README | wc -l | tr -d ' ') - # snapshot-release PRs exercise both gens under the snapshot-test tag; every - # other trigger (push to main, manual dispatch) only ever ships 1st-gen here - - # 2nd-gen has its own independent changesets setup in 2nd-gen/.changeset/ and - # ships via publish-2nd-gen.yml. - GEN2_COUNT=0 - if [ "${{ github.event_name }}" = "pull_request" ]; then - GEN2_COUNT=$(ls -1 2nd-gen/.changeset/*.md 2>/dev/null | grep -v README | wc -l | tr -d ' ') - fi - - TOTAL_COUNT=$((GEN1_COUNT + GEN2_COUNT)) - if [ "$TOTAL_COUNT" -eq 0 ]; then + if [ "$GEN1_COUNT" -eq 0 ]; then echo "has_changesets=false" >> $GITHUB_OUTPUT echo "has_1st_gen_changesets=false" >> $GITHUB_OUTPUT echo "No changesets found - skipping publish" else echo "has_changesets=true" >> $GITHUB_OUTPUT - echo "Found $GEN1_COUNT 1st-gen changeset(s), $GEN2_COUNT 2nd-gen changeset(s) (PR snapshot only)" - - if [ "$GEN1_COUNT" -gt 0 ]; then - echo "has_1st_gen_changesets=true" >> $GITHUB_OUTPUT - echo "Found 1st-gen changesets - React wrappers will be built and published" - else - echo "has_1st_gen_changesets=false" >> $GITHUB_OUTPUT - echo "No 1st-gen changesets found - React wrappers will be skipped" - fi + echo "has_1st_gen_changesets=true" >> $GITHUB_OUTPUT + echo "Found $GEN1_COUNT 1st-gen changeset(s) - React wrappers will be built and published" fi publish: @@ -162,12 +149,6 @@ jobs: (cd 1st-gen && yarn changeset version --snapshot $TAG) fi - - name: Version 2nd-gen packages for snapshot-release testing - if: github.event_name == 'pull_request' - env: - TAG: ${{ steps.extract-tag.outputs.tag }} - run: (cd 2nd-gen && yarn changeset version --snapshot $TAG) - - name: Capture released versions id: versions run: | @@ -193,13 +174,6 @@ jobs: (cd 1st-gen && yarn changeset publish --no-git-tag --tag $TAG) fi - - name: Publish 2nd-gen packages for snapshot-release testing - if: github.event_name == 'pull_request' && steps.npm-auth.outcome == 'success' - env: - NODE_AUTH_TOKEN: ${{ secrets.ADOBE_BOT_NPM_TOKEN }} - TAG: ${{ steps.extract-tag.outputs.tag }} - run: (cd 2nd-gen && yarn changeset publish --no-git-tag --tag $TAG) - - name: Build React wrappers if: needs.check-changesets.outputs.has_1st_gen_changesets == 'true' run: yarn workspace @spectrum-web-components/1st-gen build:react From ed10b60b8b911b1102eb9deb720be0914eae8014 Mon Sep 17 00:00:00 2001 From: blunteshwar Date: Wed, 22 Jul 2026 17:43:04 +0530 Subject: [PATCH 04/11] fix(ci): use dedicated status-check endpoint for release-branch-lock Replace the read-whole-protection-object-then-PUT-it-back approach with GitHub's dedicated add/remove-contexts endpoints (POST|DELETE .../protection/required_status_checks/contexts). The prior PUT only round-tripped the fields this script knew about (required_status_checks, enforce_admins, required_pull_request_reviews, restrictions), so any other configured protection setting (allow_force_pushes, required_linear_history, etc.) would silently reset to its API default on every lock and unlock. Also renames action.yaml -> action.yml to match the repo's predominant action/workflow file extension. --- .../{action.yaml => action.yml} | 0 .github/scripts/toggle-release-lock.mjs | 70 +++++-------------- 2 files changed, 19 insertions(+), 51 deletions(-) rename .github/actions/release-branch-lock/{action.yaml => action.yml} (100%) diff --git a/.github/actions/release-branch-lock/action.yaml b/.github/actions/release-branch-lock/action.yml similarity index 100% rename from .github/actions/release-branch-lock/action.yaml rename to .github/actions/release-branch-lock/action.yml diff --git a/.github/scripts/toggle-release-lock.mjs b/.github/scripts/toggle-release-lock.mjs index 1c596de3bdb..6d4a7a0e439 100644 --- a/.github/scripts/toggle-release-lock.mjs +++ b/.github/scripts/toggle-release-lock.mjs @@ -35,67 +35,35 @@ if (!['lock', 'unlock'].includes(mode) || !branch) { process.exit(1); } -function readProtection() { +function branchProtectionExists() { try { - return JSON.parse( - execSync(`gh api repos/${repo}/branches/${branch}/protection`, { - encoding: 'utf-8', - }) + 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})` ); - } catch { - return null; + return false; } } -const protection = readProtection(); - -if (!protection) { - 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.' - ); +if (!branchProtectionExists()) { process.exit(0); } -const contexts = new Set(protection.required_status_checks?.contexts ?? []); -if (mode === 'lock') { - contexts.add(LOCK_CONTEXT); -} else { - contexts.delete(LOCK_CONTEXT); -} - -const payload = { - required_status_checks: { - strict: protection.required_status_checks?.strict ?? false, - contexts: [...contexts], - }, - enforce_admins: protection.enforce_admins?.enabled ?? false, - required_pull_request_reviews: protection.required_pull_request_reviews - ? { - dismiss_stale_reviews: - protection.required_pull_request_reviews.dismiss_stale_reviews ?? - false, - require_code_owner_reviews: - protection.required_pull_request_reviews.require_code_owner_reviews ?? - false, - required_approving_review_count: - protection.required_pull_request_reviews - .required_approving_review_count ?? 1, - } - : null, - restrictions: protection.restrictions - ? { - users: protection.restrictions.users.map((user) => user.login), - teams: protection.restrictions.teams.map((team) => team.slug), - apps: protection.restrictions.apps.map((app) => app.slug), - } - : null, -}; - +// 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 -X PUT --input -`, + `gh api repos/${repo}/branches/${branch}/protection/required_status_checks/contexts -X ${method} --input -`, { - input: JSON.stringify(payload), + input: JSON.stringify({ contexts: [LOCK_CONTEXT] }), encoding: 'utf-8', } ); From aa210d78fd6f98899738af6e928e2de33e846668 Mon Sep 17 00:00:00 2001 From: blunteshwar Date: Mon, 27 Jul 2026 17:03:56 +0530 Subject: [PATCH 05/11] chore: update release workflows and documentation for gen1/gen2 split --- .github/scripts/toggle-release-lock.mjs | 2 +- .github/workflows/publish-2nd-gen.yml | 28 ++++++++++------ .github/workflows/publish.yml | 23 ++++++++----- .github/workflows/release-branch-unlock.yml | 33 +++++++++++++++++++ .../01_contributor-guides/06_releasing-swc.md | 25 +++++++++----- .../15_changelog-strategy.md | 8 ++--- 6 files changed, 87 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/release-branch-unlock.yml diff --git a/.github/scripts/toggle-release-lock.mjs b/.github/scripts/toggle-release-lock.mjs index 6d4a7a0e439..864736b56fb 100644 --- a/.github/scripts/toggle-release-lock.mjs +++ b/.github/scripts/toggle-release-lock.mjs @@ -19,7 +19,7 @@ * 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 + * NOTE: classic branch protection only; repos on the newer rulesets API instead of * 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. */ diff --git a/.github/workflows/publish-2nd-gen.yml b/.github/workflows/publish-2nd-gen.yml index 2116e43596a..a9b414ba100 100644 --- a/.github/workflows/publish-2nd-gen.yml +++ b/.github/workflows/publish-2nd-gen.yml @@ -59,8 +59,23 @@ jobs: contents: write # Required for git push (via RELEASE_BOT_TOKEN below) env: YARN_ENABLE_IMMUTABLE_INSTALLS: false - DRY_RUN: ${{ github.event.inputs.dry_run }} + DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }} steps: + # Locked before checkout (via the remote action ref, not the local ./path one - + # a local path reference needs the repo already checked out to resolve) so the + # race window is closed from the very start of the job, not just from after + # checkout + dependency install. + - name: Lock gen2-beta during release + if: github.event_name != 'pull_request' && env.DRY_RUN != 'true' + uses: adobe/spectrum-web-components/.github/actions/release-branch-lock@main + with: + mode: lock + branch: gen2-beta + token: ${{ secrets.RELEASE_BOT_TOKEN }} + + # head.ref (no repository:) resolves against the base repo - fine for same-repo + # snapshot-release PRs, which is the only way this workflow's pull_request + # trigger fires; would need a fork-aware checkout if that ever changes. - name: Checkout uses: actions/checkout@v4 with: @@ -91,14 +106,6 @@ jobs: echo "tag=$WORKFLOW_TAG" >> $GITHUB_OUTPUT echo "Using npm tag: $WORKFLOW_TAG" - - name: Lock gen2-beta during release - 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: @@ -112,6 +119,7 @@ jobs: NPM_TOKEN: ${{ secrets.ADOBE_BOT_NPM_TOKEN }} run: | echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc + npm whoami --registry=https://registry.npmjs.org echo "✓ NPM authentication configured for 2nd-gen (Adobe namespace)" - name: Build all packages @@ -167,7 +175,7 @@ jobs: env: RELEASE_BOT_TOKEN: ${{ secrets.RELEASE_BOT_TOKEN }} run: | - git add . + git add 2nd-gen yarn.lock 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 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b35d71110e1..2a9dbedfc29 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -62,6 +62,19 @@ jobs: env: YARN_ENABLE_IMMUTABLE_INSTALLS: false steps: + # Locked before checkout (via the remote action ref, not the local ./path one - + # a local path reference needs the repo already checked out to resolve) so the + # race window is closed from the very start of the job, not just from after + # checkout + dependency install. Only main@latest ever locks; every other trigger + # (push to next, snapshot-test) never reaches tag == 'latest'. + - name: Lock main during release + if: github.event_name == 'workflow_dispatch' && github.event.inputs.tag == 'latest' + uses: adobe/spectrum-web-components/.github/actions/release-branch-lock@main + with: + mode: lock + branch: main + token: ${{ secrets.RELEASE_BOT_TOKEN }} + - name: Checkout repository uses: actions/checkout@v4 with: @@ -105,14 +118,6 @@ jobs: exit 1 fi - - name: Lock main during release - if: steps.extract-tag.outputs.tag == 'latest' - uses: ./.github/actions/release-branch-lock - with: - mode: lock - branch: main - token: ${{ secrets.RELEASE_BOT_TOKEN }} - - name: Verify OIDC token and configure NPM authentication id: npm-auth env: @@ -282,7 +287,7 @@ jobs: env: RELEASE_BOT_TOKEN: ${{ secrets.RELEASE_BOT_TOKEN }} run: | - git add . + git add 1st-gen yarn.lock git commit --no-verify -m "chore: release 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 main diff --git a/.github/workflows/release-branch-unlock.yml b/.github/workflows/release-branch-unlock.yml new file mode 100644 index 00000000000..a1d0cfd7fde --- /dev/null +++ b/.github/workflows/release-branch-unlock.yml @@ -0,0 +1,33 @@ +name: Force unlock release branch + +# Manual recovery for a stuck release lock. `if: always()` on the unlock step in +# publish.yml / publish-2nd-gen.yml covers a failed step, but not a cancelled run or a +# dead runner - in either of those cases the release/in-progress required status check +# is left on the branch forever, blocking every PR merge into it until removed by hand. +# Run this workflow manually to clear it. + +on: + workflow_dispatch: + inputs: + branch: + description: 'Branch to unlock' + type: choice + required: true + options: + - main + - gen2-beta + +jobs: + unlock: + runs-on: ubuntu-latest + environment: npm-publish + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Unlock + uses: ./.github/actions/release-branch-lock + with: + mode: unlock + branch: ${{ github.event.inputs.branch }} + token: ${{ secrets.RELEASE_BOT_TOKEN }} diff --git a/CONTRIBUTOR-DOCS/01_contributor-guides/06_releasing-swc.md b/CONTRIBUTOR-DOCS/01_contributor-guides/06_releasing-swc.md index 3d503a7506a..30758f129de 100644 --- a/CONTRIBUTOR-DOCS/01_contributor-guides/06_releasing-swc.md +++ b/CONTRIBUTOR-DOCS/01_contributor-guides/06_releasing-swc.md @@ -28,9 +28,18 @@ +> ⚠️ **This page is out of date and pending a rewrite.** It still describes a single combined release workflow. As of the gen1/gen2 release-architecture split, 1st-gen and 2nd-gen release from **two separate workflows** with their own changeset folders: +> +> | | Workflow | Changesets | Branch | Ships to | +> |---|---|---|---|---| +> | 1st-gen | `.github/workflows/publish.yml` | `1st-gen/.changeset/` | `main` | `next` / `latest` | +> | 2nd-gen | `.github/workflows/publish-2nd-gen.yml` | `2nd-gen/.changeset/` | `gen2-beta` | `beta` (pre-release mode) | +> +> The sections below (package groups, versioning strategy, release types) still describe the old single-workflow model and should not be relied on until this page is rewritten to match. + ## Overview -Releases are fully automated through a GitHub Actions workflow (`.github/workflows/publish-1st-gen.yml`). There is no manual command to run locally — you trigger the release from GitHub and the workflow handles building, versioning, and publishing. +Releases are fully automated through GitHub Actions workflows — see the table above for which workflow covers which generation. There is no manual command to run locally — you trigger the release from GitHub and the workflow handles building, versioning, and publishing. The workflow publishes four package groups: @@ -51,14 +60,15 @@ The workflow publishes four package groups: > For the 2nd-gen changeset format and how entries flow into the CHANGELOG, see the [Changelog strategy](15_changelog-strategy.md). -The workflow only publishes if there are pending changesets in `.changeset/*.md`. If no changesets exist, the publish job is skipped automatically. +Each workflow only publishes if there are pending changesets in its own folder — `1st-gen/.changeset/*.md` for `publish.yml`, `2nd-gen/.changeset/*.md` for `publish-2nd-gen.yml`. If no changesets exist for that generation, its publish job is skipped automatically. -To check what's pending, look at the `.changeset/` directory (exclude `README.md`). Each changeset file lists the packages it affects and the bump type (`patch`, `minor`, or `major`). +To check what's pending, look at the relevant `.changeset/` directory (exclude `README.md`). Each changeset file lists the packages it affects and the bump type (`patch`, `minor`, or `major`). **If changesets are missing for packages you expected to update**, add them before triggering the release: ```bash -yarn changeset +yarn changeset:1st-gen +yarn changeset:2nd-gen ``` Follow the prompts to select packages and bump type. @@ -67,11 +77,10 @@ Follow the prompts to select packages and bump type. ### Understand the versioning strategy -The `.changeset/config.json` defines how packages version together: +Each generation has its own `config.json` (`1st-gen/.changeset/config.json`, `2nd-gen/.changeset/config.json`) defining how its own packages version together: -- **Fixed group** – All `@spectrum-web-components/*` packages (except Core) always version together at the same number. -- **Linked group** – `@adobe/spectrum-wc` and `@adobe/spectrum-wc-core` receive the same bump type when either changes. -- **Ignored** – The workspace root packages (`@spectrum-web-components/1st-gen`, `@spectrum-web-components/2nd-gen`) are never published. +- **Fixed group** (1st-gen) – All `@spectrum-web-components/*` packages always version together at the same number. +- **Linked group** (2nd-gen) – `@adobe/spectrum-wc` and `@adobe/spectrum-wc-core` receive the same bump type when either changes. --- diff --git a/CONTRIBUTOR-DOCS/01_contributor-guides/15_changelog-strategy.md b/CONTRIBUTOR-DOCS/01_contributor-guides/15_changelog-strategy.md index e969efc9d77..9fe44d05d0e 100644 --- a/CONTRIBUTOR-DOCS/01_contributor-guides/15_changelog-strategy.md +++ b/CONTRIBUTOR-DOCS/01_contributor-guides/15_changelog-strategy.md @@ -42,14 +42,14 @@ That's it. Component name in backticks, em dash, consumer-facing description. Yo ## Writing a changeset -Run `yarn changeset` to start the interactive CLI. It will list every package in the monorepo. For 2nd-gen work, select one of these two packages: +Run `yarn changeset:2nd-gen` to start the interactive CLI, scoped to 2nd-gen's own changesets folder (`2nd-gen/.changeset/`). It will list 2nd-gen's packages; select one of these two: | Package | When to select | |---|---| | `@adobe/spectrum-wc` | Any component change (new component, feature, bug fix) | | `@adobe/spectrum-wc-core` | Changes to shared core logic (mixins, controllers, base classes) | -Skip all `@spectrum-web-components/*` 1st-gen packages — those follow a separate process. If your PR changes both core and a component, run `yarn changeset` twice and create one changeset for each package. +1st-gen (`@spectrum-web-components/*`) has its own separate changesets setup in `1st-gen/.changeset/` (`yarn changeset:1st-gen`) and follows a separate process. If your PR changes both core and a component, run `yarn changeset:2nd-gen` twice and create one changeset for each package. After selecting the package, choose a bump type, then write the body using the format above. @@ -116,7 +116,7 @@ Bump types follow [semantic versioning](https://semver.org/) — the version num ### Rules - **One changeset per PR** is the default. Most PRs touch a single component and need a single changeset. Create multiple changesets only when the PR contains changes with different bump types (e.g., a minor addition and a patch fix). -- **Select the right package** in the changeset frontmatter. The `linked` config in `.changeset/config.json` keeps `@adobe/spectrum-wc` and `@adobe/spectrum-wc-core` at the same version automatically — you only need to list the one you touched: +- **Select the right package** in the changeset frontmatter. The `linked` config in `2nd-gen/.changeset/config.json` keeps `@adobe/spectrum-wc` and `@adobe/spectrum-wc-core` at the same version automatically — you only need to list the one you touched: | What changed | Frontmatter | |---|---| @@ -169,6 +169,6 @@ Each entry is automatically prefixed with the PR link and commit reference by `@ ## How it works -`.changeset/config.json` uses `@changesets/changelog-github` with `disableThanks: true`. This is the standard changesets GitHub changelog generator — it auto-prepends the PR link and commit reference to each entry, groups entries by bump type (`### Minor Changes`, `### Patch Changes`), and handles version headings. The `disableThanks` option suppresses the `Thanks @author!` attribution so entries stay focused on the change itself. +`2nd-gen/.changeset/config.json` uses `@changesets/changelog-github` with `disableThanks: true`. This is the standard changesets GitHub changelog generator — it auto-prepends the PR link and commit reference to each entry, groups entries by bump type (`### Minor Changes`, `### Patch Changes`), and handles version headings. The `disableThanks` option suppresses the `Thanks @author!` attribution so entries stay focused on the change itself. No custom scripts are involved. The changeset body you write is preserved as-is; changesets handles all formatting and collation. From 3876201fbb58f622182989e485d2df0227ddec6d Mon Sep 17 00:00:00 2001 From: blunteshwar Date: Mon, 27 Jul 2026 19:54:18 +0530 Subject: [PATCH 06/11] fix(ci): pin release-branch-lock pre-checkout ref to github.sha @main doesn't have .github/actions/release-branch-lock yet - it only exists on this unmerged branch, so every pre-checkout lock step hard-failed with "Can't find action.yml" (seen in run 30274425726). Pin to ${{ github.sha }} instead, which resolves the action from whatever commit is actually running: works now while testing on this branch, and continues to work after merge. --- .github/workflows/publish-2nd-gen.yml | 2 +- .github/workflows/publish.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-2nd-gen.yml b/.github/workflows/publish-2nd-gen.yml index a9b414ba100..af7d48c16f7 100644 --- a/.github/workflows/publish-2nd-gen.yml +++ b/.github/workflows/publish-2nd-gen.yml @@ -67,7 +67,7 @@ jobs: # checkout + dependency install. - name: Lock gen2-beta during release if: github.event_name != 'pull_request' && env.DRY_RUN != 'true' - uses: adobe/spectrum-web-components/.github/actions/release-branch-lock@main + uses: adobe/spectrum-web-components/.github/actions/release-branch-lock@${{ github.sha }} with: mode: lock branch: gen2-beta diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2a9dbedfc29..04e2a7431f6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -69,7 +69,7 @@ jobs: # (push to next, snapshot-test) never reaches tag == 'latest'. - name: Lock main during release if: github.event_name == 'workflow_dispatch' && github.event.inputs.tag == 'latest' - uses: adobe/spectrum-web-components/.github/actions/release-branch-lock@main + uses: adobe/spectrum-web-components/.github/actions/release-branch-lock@${{ github.sha }} with: mode: lock branch: main From 4520f825b73264e2905532ae3c72edcd543cdd51 Mon Sep 17 00:00:00 2001 From: blunteshwar Date: Mon, 27 Jul 2026 19:59:13 +0530 Subject: [PATCH 07/11] fix(ci): replace composite-action self-reference with inline gh api call uses: doesn't support any expression context, including github - so pinning to ${{ github.sha }} was invalid syntax, not just a wrong ref (confirmed by the workflow validator: "Unrecognized named-value: 'github'"). And a literal @main reference 404s since this action doesn't exist on main yet. Drop the composite-action self-reference for the pre-checkout lock step entirely and inline the gh api call directly - a raw run: step needs no uses: and no repo checkout, so there's no ref to get wrong. Post-checkout steps (unlock in both workflows, both lock/unlock in release-branch-unlock.yml) keep using the composite action via its local ./path reference, which is fine once checkout has happened. --- .github/workflows/publish-2nd-gen.yml | 20 +++++++++++--------- .github/workflows/publish.yml | 22 ++++++++++++---------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/.github/workflows/publish-2nd-gen.yml b/.github/workflows/publish-2nd-gen.yml index af7d48c16f7..efed83f7e17 100644 --- a/.github/workflows/publish-2nd-gen.yml +++ b/.github/workflows/publish-2nd-gen.yml @@ -61,17 +61,19 @@ jobs: YARN_ENABLE_IMMUTABLE_INSTALLS: false DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }} steps: - # Locked before checkout (via the remote action ref, not the local ./path one - - # a local path reference needs the repo already checked out to resolve) so the - # race window is closed from the very start of the job, not just from after - # checkout + dependency install. + # Locked before checkout via a raw gh api call (not the composite action - `uses:` + # doesn't support expressions, so it can't be pinned to github.sha, and a literal + # @main ref would 404 until this action is merged there; a raw call needs no repo + # checkout at all) so the race window is closed from the very start of the job, + # not just from after checkout + dependency install. - name: Lock gen2-beta during release if: github.event_name != 'pull_request' && env.DRY_RUN != 'true' - uses: adobe/spectrum-web-components/.github/actions/release-branch-lock@${{ github.sha }} - with: - mode: lock - branch: gen2-beta - token: ${{ secrets.RELEASE_BOT_TOKEN }} + env: + GH_TOKEN: ${{ secrets.RELEASE_BOT_TOKEN }} + run: | + gh api "repos/${{ github.repository }}/branches/gen2-beta/protection/required_status_checks/contexts" \ + -X POST --input - <<< '{"contexts":["release/in-progress"]}' \ + || echo "No branch protection on gen2-beta (or insufficient access) - skipping lock. Concurrent-merge protection during this release is not active." # head.ref (no repository:) resolves against the base repo - fine for same-repo # snapshot-release PRs, which is the only way this workflow's pull_request diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 04e2a7431f6..d7aedcf852a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -62,18 +62,20 @@ jobs: env: YARN_ENABLE_IMMUTABLE_INSTALLS: false steps: - # Locked before checkout (via the remote action ref, not the local ./path one - - # a local path reference needs the repo already checked out to resolve) so the - # race window is closed from the very start of the job, not just from after - # checkout + dependency install. Only main@latest ever locks; every other trigger - # (push to next, snapshot-test) never reaches tag == 'latest'. + # Locked before checkout via a raw gh api call (not the composite action - `uses:` + # doesn't support expressions, so it can't be pinned to github.sha, and a literal + # @main ref would 404 until this action is merged there; a raw call needs no repo + # checkout at all) so the race window is closed from the very start of the job, + # not just from after checkout + dependency install. Only main@latest ever locks; + # every other trigger (push to next, snapshot-test) never reaches tag == 'latest'. - name: Lock main during release if: github.event_name == 'workflow_dispatch' && github.event.inputs.tag == 'latest' - uses: adobe/spectrum-web-components/.github/actions/release-branch-lock@${{ github.sha }} - with: - mode: lock - branch: main - token: ${{ secrets.RELEASE_BOT_TOKEN }} + env: + GH_TOKEN: ${{ secrets.RELEASE_BOT_TOKEN }} + run: | + gh api "repos/${{ github.repository }}/branches/main/protection/required_status_checks/contexts" \ + -X POST --input - <<< '{"contexts":["release/in-progress"]}' \ + || echo "No branch protection on main (or insufficient access) - skipping lock. Concurrent-merge protection during this release is not active." - name: Checkout repository uses: actions/checkout@v4 From 7d55399807a501add2d2c2c630feeb7827f3b7cd Mon Sep 17 00:00:00 2001 From: blunteshwar Date: Mon, 27 Jul 2026 21:42:06 +0530 Subject: [PATCH 08/11] fix(ci): add missing GITHUB_TOKEN to 2nd-gen version step @changesets/changelog-github (2nd-gen's changelog generator, same as 1st-gen's) needs GITHUB_TOKEN to fetch PR/commit info for changelog entries. publish.yml's equivalent step already sets it; publish-2nd-gen.yml's "Version packages" step didn't, so `yarn changeset version` would fail with "Please create a GitHub personal access token ... and add it as the GITHUB_TOKEN environment variable" - reproduced locally before this fix, confirmed resolved after (2.0.0-beta.2 -> 2.0.0-beta.3, CHANGELOG.md and package.json updated correctly for both 2nd-gen packages). --- .github/workflows/publish-2nd-gen.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish-2nd-gen.yml b/.github/workflows/publish-2nd-gen.yml index efed83f7e17..59bbb1c3c78 100644 --- a/.github/workflows/publish-2nd-gen.yml +++ b/.github/workflows/publish-2nd-gen.yml @@ -129,6 +129,7 @@ jobs: - name: Version packages env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} EVENT_NAME: ${{ github.event_name }} TAG: ${{ steps.extract-tag.outputs.tag }} working-directory: 2nd-gen From c009f930a6446a9f42a3c53c40868c89477cc0af Mon Sep 17 00:00:00 2001 From: blunteshwar Date: Tue, 28 Jul 2026 08:01:32 +0530 Subject: [PATCH 09/11] fix(ci): authenticate 2nd-gen npm verify via NODE_AUTH_TOKEN The Verify NPM authentication step wrote ~/.npmrc, but setup-node sets NPM_CONFIG_USERCONFIG so npm reads its own temp .npmrc instead, which authenticates through ${NODE_AUTH_TOKEN}. That token was never set here, so npm whoami got the placeholder and returned 401. Set NODE_AUTH_TOKEN to ADOBE_BOT_NPM_TOKEN and drop the no-op ~/.npmrc write. --- .github/workflows/publish-2nd-gen.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-2nd-gen.yml b/.github/workflows/publish-2nd-gen.yml index 59bbb1c3c78..b50a1992547 100644 --- a/.github/workflows/publish-2nd-gen.yml +++ b/.github/workflows/publish-2nd-gen.yml @@ -117,10 +117,13 @@ jobs: - name: Verify NPM authentication id: npm-auth + # setup-node (in setup-job) already wrote an .npmrc that npm reads via + # NPM_CONFIG_USERCONFIG and that authenticates through ${NODE_AUTH_TOKEN}; + # just point that at the real token. Writing ~/.npmrc here is a no-op because + # the userconfig override wins. env: - NPM_TOKEN: ${{ secrets.ADOBE_BOT_NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.ADOBE_BOT_NPM_TOKEN }} run: | - echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc npm whoami --registry=https://registry.npmjs.org echo "✓ NPM authentication configured for 2nd-gen (Adobe namespace)" From 9f75102b025cfd899e98a79d03246c3d3becdd93 Mon Sep 17 00:00:00 2001 From: blunteshwar Date: Tue, 28 Jul 2026 11:28:43 +0530 Subject: [PATCH 10/11] ci(release): publish 2nd-gen from main instead of gen2-beta 2nd-gen now releases from main on its own cadence (still beta tag, changesets pre-release mode). Retarget every gen2-beta reference to main: push trigger, branch lock/unlock, checkout ref, commit/push target, the changeset baseBranch, the force-unlock branch choice, and the changeset README. No gen2-beta branch is involved anymore. --- .github/workflows/publish-2nd-gen.yml | 22 ++++++++++----------- .github/workflows/release-branch-unlock.yml | 1 - 2nd-gen/.changeset/README.md | 7 +++---- 2nd-gen/.changeset/config.json | 2 +- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/.github/workflows/publish-2nd-gen.yml b/.github/workflows/publish-2nd-gen.yml index b50a1992547..4f34825050f 100644 --- a/.github/workflows/publish-2nd-gen.yml +++ b/.github/workflows/publish-2nd-gen.yml @@ -16,7 +16,7 @@ on: types: [labeled, synchronize] push: branches: - - gen2-beta + - main concurrency: group: publish-2nd-gen-${{ github.ref }} @@ -66,14 +66,14 @@ jobs: # @main ref would 404 until this action is merged there; a raw call needs no repo # checkout at all) so the race window is closed from the very start of the job, # not just from after checkout + dependency install. - - name: Lock gen2-beta during release + - name: Lock main during release if: github.event_name != 'pull_request' && env.DRY_RUN != 'true' env: GH_TOKEN: ${{ secrets.RELEASE_BOT_TOKEN }} run: | - gh api "repos/${{ github.repository }}/branches/gen2-beta/protection/required_status_checks/contexts" \ + gh api "repos/${{ github.repository }}/branches/main/protection/required_status_checks/contexts" \ -X POST --input - <<< '{"contexts":["release/in-progress"]}' \ - || echo "No branch protection on gen2-beta (or insufficient access) - skipping lock. Concurrent-merge protection during this release is not active." + || echo "No branch protection on main (or insufficient access) - skipping lock. Concurrent-merge protection during this release is not active." # head.ref (no repository:) resolves against the base repo - fine for same-repo # snapshot-release PRs, which is the only way this workflow's pull_request @@ -81,7 +81,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || 'gen2-beta' }} + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || 'main' }} fetch-depth: 0 - name: Setup job and install dependencies @@ -139,7 +139,7 @@ jobs: 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. + # the persistent pre-release (beta.N) state used by real main releases. yarn changeset version --snapshot $TAG else yarn changeset version @@ -160,7 +160,7 @@ jobs: 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 + echo "Dry run requested - no packages were published and main was not modified." >> $GITHUB_STEP_SUMMARY - name: Refresh lockfile and rebuild if: env.DRY_RUN != 'true' @@ -184,8 +184,8 @@ jobs: git add 2nd-gen yarn.lock 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 + git pull --rebase origin main + git push origin HEAD:main - name: Publish summary if: always() @@ -214,10 +214,10 @@ jobs: echo "❌ Publish failed — NPM authentication did not succeed" >> $GITHUB_STEP_SUMMARY fi - - name: Unlock gen2-beta after release + - name: Unlock main after release if: always() && github.event_name != 'pull_request' && env.DRY_RUN != 'true' uses: ./.github/actions/release-branch-lock with: mode: unlock - branch: gen2-beta + branch: main token: ${{ secrets.RELEASE_BOT_TOKEN }} diff --git a/.github/workflows/release-branch-unlock.yml b/.github/workflows/release-branch-unlock.yml index a1d0cfd7fde..f2e6f1dbb43 100644 --- a/.github/workflows/release-branch-unlock.yml +++ b/.github/workflows/release-branch-unlock.yml @@ -15,7 +15,6 @@ on: required: true options: - main - - gen2-beta jobs: unlock: diff --git a/2nd-gen/.changeset/README.md b/2nd-gen/.changeset/README.md index 98d69ad86d3..3328e253f5b 100644 --- a/2nd-gen/.changeset/README.md +++ b/2nd-gen/.changeset/README.md @@ -4,7 +4,7 @@ This folder is an independent `@changesets/cli` setup for 2nd-gen packages only ## Why a separate setup -1st-gen ships to `next`/`latest` from `main`. 2nd-gen ships to `beta` from `gen2-beta` on its own cadence, using changesets pre-release mode. `@changesets/cli` resolves `.changeset/` relative to wherever it's invoked from, so each generation gets its own instance by living in its own package directory (`2nd-gen/`) rather than sharing one at the repo root. +1st-gen ships to `next`/`latest` from `main`. 2nd-gen ships to `beta` from `main` on its own cadence, using changesets pre-release mode. `@changesets/cli` resolves `.changeset/` relative to wherever it's invoked from, so each generation gets its own instance by living in its own package directory (`2nd-gen/`) rather than sharing one at the repo root. ## Adding a changeset for a 2nd-gen change @@ -32,8 +32,7 @@ When a change touches `@adobe/spectrum-wc-core`, include the corresponding `@ado ## Publishing process 1. Changesets accumulate here as gen2 PRs merge to `main`. -2. An operator syncs `main` into `gen2-beta` when ready to cut a release. -3. `publish-2nd-gen.yml` (triggered on push to `gen2-beta`, or manually) runs changesets from within `2nd-gen/` in pre-release mode and publishes to the `beta` npm tag. -4. Consumed changesets are removed as part of that release commit on `gen2-beta`. +2. `publish-2nd-gen.yml` (triggered on push to `main`, or manually) runs changesets from within `2nd-gen/` in pre-release mode and publishes to the `beta` npm tag. +3. Consumed changesets are removed as part of that release commit on `main`. See [Changesets documentation](https://github.com/changesets/changesets) for details on the underlying tool. diff --git a/2nd-gen/.changeset/config.json b/2nd-gen/.changeset/config.json index be87441f931..c059464688c 100644 --- a/2nd-gen/.changeset/config.json +++ b/2nd-gen/.changeset/config.json @@ -1,7 +1,7 @@ { "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", "access": "public", - "baseBranch": "gen2-beta", + "baseBranch": "main", "changelog": [ "@changesets/changelog-github", { From b81b4b36957b8777727568a43552efd01fa07fbc Mon Sep 17 00:00:00 2001 From: blunteshwar Date: Wed, 5 Aug 2026 14:13:10 +0530 Subject: [PATCH 11/11] ci(release): split gen2 into next (auto) and beta (manual) streams 2nd-gen now publishes two dist-tags with different mechanics: - push to main -> next: continuous throwaway snapshot (changeset version --snapshot next, publish --tag next). No pre-release mode, no branch lock, nothing committed back to main - a rolling build of main for consumers (e.g. the Photoshop dry-run PR) to pull the latest changes. - workflow_dispatch (main only) -> beta: planned pre-release. Enters changesets pre-release mode (beta.N), locks main, versions + changelog, commits the bump back to main. - pull_request + snapshot-release label -> snapshot-test (unchanged). A manual dispatch is restricted to main (github.ref guard); off-main dispatches are skipped. latest is intentionally not supported yet. --- .github/workflows/publish-2nd-gen.yml | 47 +++++++++++++++++++-------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/.github/workflows/publish-2nd-gen.yml b/.github/workflows/publish-2nd-gen.yml index 4f34825050f..d7d38513394 100644 --- a/.github/workflows/publish-2nd-gen.yml +++ b/.github/workflows/publish-2nd-gen.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: tag: - description: 'NPM dist-tag' + description: 'NPM dist-tag for the planned release' required: false default: 'beta' dry_run: @@ -14,20 +14,31 @@ on: default: false pull_request: types: [labeled, synchronize] + # Every push to main auto-publishes the `next` dist-tag: a continuous, throwaway snapshot + # of main (no commit back, no branch lock) so consumers can always pull the latest main + # build. Planned `beta` releases are cut manually via workflow_dispatch (pre-release mode, + # changelog, commit back to main). Per-step conditions below implement that split. push: branches: - main +# Shares the exact group string with publish.yml (1st-gen) so the two release workflows +# stay mutually exclusive on any shared ref: on a push to main both compute +# publish-refs/heads/main, so one runs while the other queues. cancel-in-progress: false +# queues rather than cancels; no ordering guarantee. concurrency: - group: publish-2nd-gen-${{ github.ref }} + group: publish-${{ github.ref }} cancel-in-progress: false jobs: check-changesets: runs-on: ubuntu-latest + # push to main -> auto `next` snapshot. workflow_dispatch -> planned `beta`, allowed + # only from main (a dispatch against any other ref is skipped). pull_request -> only + # via the snapshot-release label. if: >- - github.event_name == 'workflow_dispatch' || github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'snapshot-release')) outputs: @@ -67,7 +78,8 @@ jobs: # checkout at all) so the race window is closed from the very start of the job, # not just from after checkout + dependency install. - name: Lock main during release - if: github.event_name != 'pull_request' && env.DRY_RUN != 'true' + # Only the planned beta release writes to main, so only it needs the lock. + if: github.event_name == 'workflow_dispatch' && env.DRY_RUN != 'true' env: GH_TOKEN: ${{ secrets.RELEASE_BOT_TOKEN }} run: | @@ -99,17 +111,22 @@ jobs: 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. + # snapshot-release PRs: throwaway snapshot under the snapshot-test tag. WORKFLOW_TAG="snapshot-test" + elif [ "$EVENT_NAME" == "push" ]; then + # every push to main: continuous throwaway snapshot under the next tag. + WORKFLOW_TAG="next" else + # manual workflow_dispatch: planned pre-release, beta by default. WORKFLOW_TAG="${INPUT_TAG:-beta}" fi echo "tag=$WORKFLOW_TAG" >> $GITHUB_OUTPUT echo "Using npm tag: $WORKFLOW_TAG" + # Only the planned beta path (workflow_dispatch) enters persistent pre-release mode. + # push->next and PR->snapshot-test use --snapshot instead and never touch pre.json. - name: Enter changesets pre-release mode - if: github.event_name != 'pull_request' && !hashFiles('2nd-gen/.changeset/pre.json') + if: github.event_name == 'workflow_dispatch' && !hashFiles('2nd-gen/.changeset/pre.json') env: TAG: ${{ steps.extract-tag.outputs.tag }} working-directory: 2nd-gen @@ -137,12 +154,13 @@ jobs: 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 main releases. - yarn changeset version --snapshot $TAG - else + if [ "$EVENT_NAME" == "workflow_dispatch" ]; then + # Planned beta: pre-release versioning (beta.N) against the persistent pre.json. yarn changeset version + else + # push->next and PR->snapshot-test: throwaway snapshot version ({tag}.{datetime}), + # never entering the persistent pre-release state. + yarn changeset version --snapshot $TAG fi - name: Capture released versions @@ -177,7 +195,8 @@ jobs: run: yarn changeset publish --no-git-tag --tag $TAG - name: Commit and push changes - if: github.event_name != 'pull_request' && env.DRY_RUN != 'true' + # Only the planned beta release commits version bumps back to main. + if: github.event_name == 'workflow_dispatch' && env.DRY_RUN != 'true' env: RELEASE_BOT_TOKEN: ${{ secrets.RELEASE_BOT_TOKEN }} run: | @@ -215,7 +234,7 @@ jobs: fi - name: Unlock main after release - if: always() && github.event_name != 'pull_request' && env.DRY_RUN != 'true' + if: always() && github.event_name == 'workflow_dispatch' && env.DRY_RUN != 'true' uses: ./.github/actions/release-branch-lock with: mode: unlock