diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fd9d1fe45c2..28727503cc1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,9 +4,19 @@ on: workflow_dispatch: inputs: tag: - description: 'NPM dist-tag (e.g., latest, beta, snapshot)' + description: '1st-gen NPM dist-tag (e.g., next, latest). 2nd-gen always publishes as beta.' required: false default: 'next' + beta_2nd_gen: + description: 'Also cut a 2nd-gen beta (2.0.0-beta.N under the beta tag) when 2nd-gen changesets exist. Ignored for the latest tag.' + required: false + type: boolean + default: true + dry_run: + description: 'Dry run the 2nd-gen beta: build, version, and pack but use npm publish --dry-run (no registry writes).' + required: false + type: boolean + default: false pull_request: types: [labeled, synchronize] push: @@ -28,6 +38,7 @@ jobs: outputs: has_changesets: ${{ steps.check.outputs.has_changesets }} has_1st_gen_changesets: ${{ steps.check.outputs.has_1st_gen_changesets }} + has_2nd_gen_changesets: ${{ steps.check.outputs.has_2nd_gen_changesets }} steps: - name: Checkout repository uses: actions/checkout@v4 @@ -38,6 +49,7 @@ jobs: if [ -z "$(ls -A .changeset/*.md 2>/dev/null | grep -v README)" ]; then echo "has_changesets=false" >> $GITHUB_OUTPUT echo "has_1st_gen_changesets=false" >> $GITHUB_OUTPUT + echo "has_2nd_gen_changesets=false" >> $GITHUB_OUTPUT echo "No changesets found - skipping publish" else echo "has_changesets=true" >> $GITHUB_OUTPUT @@ -47,23 +59,32 @@ jobs: # Check if any changeset mentions 1st-gen packages # 1st-gen packages are @spectrum-web-components/* except core HAS_1ST_GEN=false + HAS_2ND_GEN=false for file in .changeset/*.md; do if [ "$(basename "$file")" != "README.md" ]; then PACKAGES=$(grep -oP "@spectrum-web-components/[a-z-]+" "$file") NON_CORE=$(echo "$PACKAGES" | grep -v "^@spectrum-web-components/core$") if [ -n "$NON_CORE" ]; then HAS_1ST_GEN=true - break + fi + if grep -qE "@spectrum-web-components/core|@adobe/spectrum-wc" "$file"; then + HAS_2ND_GEN=true fi fi done echo "has_1st_gen_changesets=$HAS_1ST_GEN" >> $GITHUB_OUTPUT + echo "has_2nd_gen_changesets=$HAS_2ND_GEN" >> $GITHUB_OUTPUT if [ "$HAS_1ST_GEN" = "true" ]; then echo "Found 1st-gen changesets - React wrappers will be built and published" else echo "No 1st-gen changesets found - React wrappers will be skipped" fi + if [ "$HAS_2ND_GEN" = "true" ]; then + echo "Found 2nd-gen changesets - 2nd-gen beta step can run when requested" + else + echo "No 2nd-gen changesets found - 2nd-gen beta step will be skipped" + fi fi publish: @@ -95,6 +116,8 @@ jobs: env: EVENT_NAME: ${{ github.event_name }} INPUT_TAG: ${{ github.event.inputs.tag }} + INPUT_BETA_2ND_GEN: ${{ github.event.inputs.beta_2nd_gen }} + HAS_2ND_GEN_CHANGESETS: ${{ needs.check-changesets.outputs.has_2nd_gen_changesets }} run: | if [ "$EVENT_NAME" == "pull_request" ]; then # PRs with the snapshot-release label always use snapshot-test @@ -107,8 +130,31 @@ jobs: WORKFLOW_TAG="next" fi + # 2nd-gen (core + @adobe/spectrum-wc) never rides the 1st-gen next/latest + # train; it ships on its own beta cadence. Exclude it from the changeset + # publish for every real release (push + manual dispatch). PR snapshot-test + # runs still publish everything so 2nd-gen changes can be smoke-tested. + if [ "$EVENT_NAME" == "pull_request" ]; then + EXCLUDE_2ND_GEN="false" + else + EXCLUDE_2ND_GEN="true" + fi + + # Cut 2.0.0-beta.N for 2nd-gen only on a deliberate manual dispatch when + # pending changesets mention core or @adobe/spectrum-wc. Never during a + # latest prod run or when there is nothing new for 2nd-gen to ship. + if [ "$EVENT_NAME" == "workflow_dispatch" ] && [ "$INPUT_BETA_2ND_GEN" != "false" ] && [ "$WORKFLOW_TAG" != "latest" ] && [ "$HAS_2ND_GEN_CHANGESETS" == "true" ]; then + RUN_2ND_GEN_BETA="true" + else + RUN_2ND_GEN_BETA="false" + fi + echo "tag=$WORKFLOW_TAG" >> $GITHUB_OUTPUT + echo "exclude_2nd_gen=$EXCLUDE_2ND_GEN" >> $GITHUB_OUTPUT + echo "run_2nd_gen_beta=$RUN_2ND_GEN_BETA" >> $GITHUB_OUTPUT echo "Using npm tag: $WORKFLOW_TAG" + echo "Exclude 2nd-gen from 1st-gen publish: $EXCLUDE_2ND_GEN" + echo "Cut 2nd-gen beta this run: $RUN_2ND_GEN_BETA" - name: Validate release tag env: @@ -171,14 +217,27 @@ jobs: yarn install --refresh-lockfile yarn build - - name: Publish all packages + - name: Exclude 2nd-gen from the 1st-gen publish + if: steps.extract-tag.outputs.exclude_2nd_gen == 'true' + run: | + set -euo pipefail + # 2nd-gen (core + @adobe/spectrum-wc) releases on its own beta cadence, + # never as next/latest. Marking them private makes `changeset publish` + # skip them; the dedicated 2nd-gen beta step publishes them afterwards. + # These edits are never committed (see the restore in later steps). + (cd 2nd-gen/packages/core && npm pkg set private=true --json) + (cd 2nd-gen/packages/swc && npm pkg set private=true --json) + + - 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 all packages (1st-gen, core, and 2nd-gen) - # npm CLI automatically uses: + # Changeset publishes every non-private package. For real releases the + # 2nd-gen packages are marked private just above, so this publishes 1st-gen + # only; PR snapshot-test runs leave them public and publish everything. + # npm CLI selects auth automatically: # - OIDC trusted publishing for 1st-gen packages (configured on npmjs.com) # - Token authentication for 2nd-gen (from .npmrc) if [ "$TAG" == "latest" ]; then @@ -187,6 +246,44 @@ jobs: yarn changeset publish --no-git-tag --tag $TAG fi + - name: Prepare 2nd-gen beta release + id: prepare-2nd-gen-beta + if: >- + steps.npm-auth.outcome == 'success' && + steps.extract-tag.outputs.run_2nd_gen_beta == 'true' && + needs.check-changesets.outputs.has_2nd_gen_changesets == 'true' + run: node scripts/main-2nd-gen-beta-release.js + + - name: Rebuild after 2nd-gen beta versioning + if: steps.prepare-2nd-gen-beta.outcome == 'success' + run: | + yarn install --refresh-lockfile + yarn build + + - name: Publish 2nd-gen beta (core + @adobe/spectrum-wc) + id: publish-2nd-gen + if: steps.prepare-2nd-gen-beta.outcome == 'success' + env: + NODE_AUTH_TOKEN: ${{ secrets.ADOBE_BOT_NPM_TOKEN }} + DRY_RUN: ${{ github.event.inputs.dry_run }} + BETA_VERSION: ${{ steps.prepare-2nd-gen-beta.outputs.beta }} + run: | + set -euo pipefail + echo "Publishing 2nd-gen beta: $BETA_VERSION" + + PUBLISH_FLAGS="--tag beta --access public" + if [ "$DRY_RUN" == "true" ]; then + PUBLISH_FLAGS="$PUBLISH_FLAGS --dry-run" + echo "::notice::DRY RUN - packing only, nothing published to npm" + fi + + # Changelog entries were written by changeset version --snapshot and + # retitled to 2.0.0-beta.N by scripts/main-2nd-gen-beta-release.js. + yarn workspace @spectrum-web-components/core pack --out /tmp/swc-core.tgz + yarn workspace @adobe/spectrum-wc pack --out /tmp/swc-wc.tgz + npm publish /tmp/swc-core.tgz $PUBLISH_FLAGS + npm publish /tmp/swc-wc.tgz $PUBLISH_FLAGS + - name: Build React wrappers if: needs.check-changesets.outputs.has_1st_gen_changesets == 'true' run: yarn workspace @spectrum-web-components/1st-gen build:react @@ -249,6 +346,7 @@ jobs: CORE_VERSION: ${{ steps.versions.outputs.core }} BUNDLE_VERSION: ${{ steps.versions.outputs.bundle }} SWC_VERSION: ${{ steps.versions.outputs.swc }} + BETA_VERSION: ${{ steps.prepare-2nd-gen-beta.outputs.beta }} run: | echo "## Publish summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY @@ -277,14 +375,24 @@ jobs: echo "| **Commit** | \`${COMMIT_SHA}\` |" >> $GITHUB_STEP_SUMMARY fi + # The 2nd-gen beta step (when it runs) publishes the real beta version; + # otherwise 2nd-gen was excluded from this run and only 1st-gen shipped. + if [ -n "$BETA_VERSION" ]; then + CORE_VERSION="$BETA_VERSION" + SWC_VERSION="$BETA_VERSION" + else + CORE_VERSION="" + SWC_VERSION="" + fi + if [ -n "$CORE_VERSION" ] || [ -n "$BUNDLE_VERSION" ] || [ -n "$SWC_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 | \`@spectrum-web-components/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 "| Generation | Package | Version | Tag |" >> $GITHUB_STEP_SUMMARY + echo "|------------|---------|---------|-----|" >> $GITHUB_STEP_SUMMARY + [ -n "$CORE_VERSION" ] && echo "| Core | \`@spectrum-web-components/core\` | \`${CORE_VERSION}\` | \`beta\` |" >> $GITHUB_STEP_SUMMARY + [ -n "$BUNDLE_VERSION" ] && echo "| 1st gen | \`@spectrum-web-components/bundle\` | \`${BUNDLE_VERSION}\` | \`${TAG}\` |" >> $GITHUB_STEP_SUMMARY + [ -n "$SWC_VERSION" ] && echo "| 2nd gen | \`@adobe/spectrum-wc\` | \`${SWC_VERSION}\` | \`beta\` |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY fi @@ -297,6 +405,9 @@ jobs: - name: Commit and push changes if: steps.extract-tag.outputs.tag == 'latest' run: | + # 2nd-gen manifests may carry an ephemeral private flag / version bump from + # this run; restore them so only 1st-gen version + changelog changes land. + git checkout -- 2nd-gen/packages/core 2nd-gen/packages/swc || true git pull --rebase origin main git add . git commit --no-verify -m "chore: release packages #publish" || echo "No changes to commit" diff --git a/package.json b/package.json index 61b121fcb5f..23dbe11ab96 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "postinstall": "husky || true", "publish": "node ./scripts/publish.js", "publish:snapshot": "node ./scripts/publish.js --tag snapshot", + "release:main-2nd-gen-beta": "node ./scripts/main-2nd-gen-beta-release.js", "start": "run-p start:2nd-gen start:1st-gen", "start:1st-gen": "yarn workspace @spectrum-web-components/1st-gen start", "start:2nd-gen": "yarn workspace @spectrum-web-components/2nd-gen start", diff --git a/scripts/main-2nd-gen-beta-release.js b/scripts/main-2nd-gen-beta-release.js new file mode 100644 index 00000000000..1a21efc7a7b --- /dev/null +++ b/scripts/main-2nd-gen-beta-release.js @@ -0,0 +1,147 @@ +#!/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. + */ + +/** + * Prepares a 2nd-gen beta publish on main after `changeset version --snapshot`. + * + * The single-trigger publish workflow versions every package together for 1st-gen + * `next` snapshots. That step also consumes 2nd-gen changesets and writes their + * changelog entries, but under a `0.x-next.*` header. This script retitles those + * entries to `2.0.0-beta.N`, sets core + @adobe/spectrum-wc to that beta version, + * and refreshes the 2nd-gen version.ts file. + * + * Run only when pending changesets mention `@spectrum-web-components/core` or + * `@adobe/spectrum-wc`. The workflow gates on that check before calling this script. + */ + +import { execSync } from 'child_process'; +import { appendFileSync, readFileSync, writeFileSync } from 'fs'; + +const CORE_PKG = '2nd-gen/packages/core/package.json'; +const SWC_PKG = '2nd-gen/packages/swc/package.json'; +const CORE_CHANGELOG = '2nd-gen/packages/core/CHANGELOG.md'; +const SWC_CHANGELOG = '2nd-gen/packages/swc/CHANGELOG.md'; +const FIRST_GEN_VERSION_TS = '1st-gen/tools/base/src/version.ts'; + +function readJson(file) { + return JSON.parse(readFileSync(file, 'utf8')); +} + +function writeJson(file, data) { + writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`); +} + +function escapeRegex(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function getNextBetaVersion() { + const raw = execSync( + 'npm view @adobe/spectrum-wc versions --json 2>/dev/null', + { + encoding: 'utf8', + } + ); + let versions = []; + try { + versions = JSON.parse(raw || '[]'); + } catch { + versions = []; + } + if (!Array.isArray(versions)) { + versions = [versions]; + } + const re = /^2\.0\.0-beta\.(\d+)$/; + const betaNumbers = versions + .map((version) => { + const match = re.exec(version); + return match ? Number(match[1]) : -1; + }) + .filter((n) => n >= 0); + const nextN = (betaNumbers.length ? Math.max(...betaNumbers) : -1) + 1; + return `2.0.0-beta.${nextN}`; +} + +function retitleChangelog(path, snapshotVersion, betaVersion) { + const header = `## ${snapshotVersion}`; + let content = readFileSync(path, 'utf8'); + if (!content.includes(header)) { + throw new Error( + `${path} is missing the expected snapshot section "## ${snapshotVersion}". ` + + 'Was changeset version run with 2nd-gen changesets present?' + ); + } + content = content.replace(header, `## ${betaVersion}`); + content = content.replace( + new RegExp( + `@spectrum-web-components/core@${escapeRegex(snapshotVersion)}`, + 'g' + ), + `@spectrum-web-components/core@${betaVersion}` + ); + writeFileSync(path, content); + console.log(` ✓ Retitled ${path} -> ${betaVersion}`); +} + +function main() { + const corePkg = readJson(CORE_PKG); + const swcPkg = readJson(SWC_PKG); + const snapshotVersion = corePkg.version; + + if (!snapshotVersion.includes('-next.')) { + console.error( + `❌ Expected a snapshot version on core (got "${snapshotVersion}"). ` + + 'Run changeset version --snapshot before this script.' + ); + process.exit(1); + } + if (swcPkg.version !== snapshotVersion) { + console.error( + `❌ core (${snapshotVersion}) and swc (${swcPkg.version}) snapshot versions differ.` + ); + process.exit(1); + } + + const betaVersion = getNextBetaVersion(); + console.log(`\nℹ️ Snapshot version: ${snapshotVersion}`); + console.log(`ℹ️ Next beta version: ${betaVersion}`); + + retitleChangelog(CORE_CHANGELOG, snapshotVersion, betaVersion); + retitleChangelog(SWC_CHANGELOG, snapshotVersion, betaVersion); + + corePkg.version = betaVersion; + corePkg.private = false; + swcPkg.version = betaVersion; + swcPkg.private = false; + swcPkg.dependencies['@spectrum-web-components/core'] = betaVersion; + writeJson(CORE_PKG, corePkg); + writeJson(SWC_PKG, swcPkg); + console.log(' ✓ Updated core + swc package.json'); + + execSync('node scripts/generate-versions.js', { stdio: 'inherit' }); + execSync(`git checkout -- '${FIRST_GEN_VERSION_TS}'`, { stdio: 'inherit' }); + console.log( + ` ✓ Refreshed 2nd-gen version.ts (left ${FIRST_GEN_VERSION_TS} on main)` + ); + + if (process.env.GITHUB_OUTPUT) { + appendFileSync(process.env.GITHUB_OUTPUT, `beta=${betaVersion}\n`); + } + + console.log('\n✅ 2nd-gen beta prepared for publish:'); + console.log(` @spectrum-web-components/core ${betaVersion}`); + console.log(` @adobe/spectrum-wc ${betaVersion}`); +} + +main();