Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/boxel-cli-pr-title.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: PR Title Check [boxel-cli]

# Validates that PRs touching packages/boxel-cli/** use a conventional-commit
# title (feat:, fix:, chore:, etc.). The boxel-cli-on-main.yml workflow reads
# title (feat:, fix:, chore:, etc.). The boxel-cli-publish.yml workflow reads
# the merged PR's title to decide the unstable version bump level, so this
# check is the contract that keeps that flow working.
#
Expand Down Expand Up @@ -30,7 +30,7 @@ jobs:
steps:
# Sparse-checkout just the one JSON file that is the single source of
# truth for allowed conventional-commit prefixes. The post-merge
# workflow (boxel-cli-on-main.yml) reads the same file via
# workflow (boxel-cli-publish.yml) reads the same file via
# compute-release.ts — keeps the pre-merge gate and post-merge
# classifier in lockstep.
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,41 @@
name: Auto-publish boxel-cli unstable
name: boxel-cli publish

# On every merge to main that touches packages/boxel-cli/**, regenerate plugin
# skill content, decide per-surface version bumps from the merged PR's title
# (conventional-commit prefix), commit the bumps back to main, tag, and publish
# `@cardstack/boxel-cli@<v>-unstable.<n>` to npm under dist-tag `unstable`.
# Single workflow file that owns both publish paths for @cardstack/boxel-cli:
#
# Stable promotion is the manual-boxel-cli-publish.yml workflow.
# • `unstable` job — on every merge to main that touches packages/boxel-cli/**,
# regenerate plugin skill content, decide per-surface version bumps from the
# merged PR's title (conventional-commit prefix), commit the bumps back to
# main, tag, and publish `@cardstack/boxel-cli@<v>-unstable.<n>` to npm under
# dist-tag `unstable`.
#
# Loop safety:
# • `stable` job — manual workflow_dispatch that strips `-unstable.<n>` from the
# current version and publishes the resulting clean semver under dist-tag
# `latest`. The deliberate "cut a release" step.
#
# Both flows live in one file so they share a single npm Trusted-Publisher rule
# (registered against this filename). Splitting them into separate workflow files
# would require a second trusted-publisher rule on npmjs.com.
#
# Loop safety (unstable job):
# - Bot commits end with [skip ci] so GitHub does not re-trigger workflows.
# - The job guards with `if: github.actor != 'github-actions[bot]'` (belt + suspenders).
#
# Concurrency:
# - `group: boxel-cli-release, cancel-in-progress: false` serializes back-to-back
# merges so prerelease counters don't collide.
# merges and any concurrent manual promotion so prerelease counters and stable
# tags can't race.

on:
push:
branches: [main]
paths:
- "packages/boxel-cli/**"
workflow_dispatch:
inputs:
confirm:
description: "Type 'promote' to confirm publishing the latest unstable to npm latest"
required: true
type: string

permissions:
contents: write
Expand All @@ -30,9 +47,9 @@ concurrency:
cancel-in-progress: false

jobs:
release-unstable:
unstable:
name: Regen, bump, publish unstable
if: github.actor != 'github-actions[bot]'
if: github.event_name == 'push' && github.actor != 'github-actions[bot]'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down Expand Up @@ -137,7 +154,6 @@ jobs:
if: steps.pr.outputs.skip != 'true' && steps.release.outputs.npmBump != 'none'
env:
GH_TOKEN: ${{ github.token }}
PREV_TAG_RAW: ""
NEW_TAG: boxel-cli-v${{ steps.release.outputs.nextNpm }}
NEXT_NPM: ${{ steps.release.outputs.nextNpm }}
NEXT_PLUGIN: ${{ steps.release.outputs.nextPlugin }}
Expand Down Expand Up @@ -217,7 +233,10 @@ jobs:
git commit -m "$MSG"
if [ -n "$NEXT_NPM" ]; then
TAG="boxel-cli-v${NEXT_NPM}"
git tag "$TAG"
# Annotated tag so `git push --follow-tags` actually pushes it.
# Lightweight tags are silently skipped by --follow-tags, which
# would leave the tag on the runner only and fail `gh release create`.
git tag -a "$TAG" -m "$TAG"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
fi
git push origin main --follow-tags
Expand Down Expand Up @@ -259,3 +278,161 @@ jobs:
--notes "Auto-published to npm under dist-tag \`unstable\`: ${NPM_URL}" \
--prerelease
fi

stable:
name: Promote latest unstable to stable
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- name: Validate confirmation
run: |
if [ "${{ inputs.confirm }}" != "promote" ]; then
echo "::error::Pass 'promote' as the confirm input to proceed."
exit 1
fi

- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: main
fetch-depth: 0
fetch-tags: true
token: ${{ secrets.GITHUB_TOKEN }}

- uses: ./.github/actions/init

- name: Configure git
run: |
git config user.name "${{ github.actor }}"
git config user.email "${{ github.actor }}@users.noreply.github.com"

- name: Strip -unstable.N from package.json
id: strip
working-directory: packages/boxel-cli
run: |
set -euo pipefail
CURRENT=$(node -p "require('./package.json').version")
echo "Current version: $CURRENT"
# Match <base>-unstable.<n>. If no suffix, the package is already
# on a stable version — refuse to no-op so we don't accidentally
# re-publish identical bits under a new tag.
if [[ "$CURRENT" != *-unstable.* ]]; then
echo "::error::Version $CURRENT is already stable; nothing to promote."
exit 1
fi
STABLE="${CURRENT%-unstable.*}"
echo "Promoting to: $STABLE"
STABLE="$STABLE" node -e '
const fs = require("fs");
const j = JSON.parse(fs.readFileSync("package.json", "utf8"));
j.version = process.env.STABLE;
fs.writeFileSync("package.json", JSON.stringify(j, null, 2) + "\n");
'
echo "version=$STABLE" >> "$GITHUB_OUTPUT"

- name: Build boxel-cli
run: pnpm build
working-directory: packages/boxel-cli

- name: Generate release notes
id: notes
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.strip.outputs.version }}
REPO: ${{ github.repository }}
RUNNER_TEMP_DIR: ${{ runner.temp }}
run: |
set -euo pipefail
NEW_TAG="boxel-cli-v${VERSION}"
# Find the previous stable boxel-cli-v* tag (skipping any -unstable.*
# tags). We diff against the prior stable so the release notes for a
# stable cut aggregate every PR since the previous stable, not just
# the most recent unstable's single PR.
PREV_TAG=$(git tag --list 'boxel-cli-v*' \
| grep -v -- '-unstable\.' \
| sort -V \
| grep -v -x "$NEW_TAG" \
| tail -n 1 || true)
if [ -z "$PREV_TAG" ]; then
echo "No prior stable boxel-cli-v* tag found (bootstrap). Falling back to one-line release body."
echo "has=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Previous stable tag: $PREV_TAG"
BODY_FILE="$RUNNER_TEMP_DIR/release-body.md"
FRAGMENT_FILE="$RUNNER_TEMP_DIR/changelog-fragment.md"
PREV_TAG="$PREV_TAG" \
NEW_TAG="$NEW_TAG" \
NEXT_NPM="$VERSION" \
NEXT_PLUGIN="" \
NPM_DIST_TAG="latest" \
REPO="$REPO" \
RELEASE_BODY_FILE="$BODY_FILE" \
CHANGELOG_FRAGMENT_FILE="$FRAGMENT_FILE" \
NODE_NO_WARNINGS=1 pnpm --filter @cardstack/boxel-cli exec \
ts-node --transpileOnly scripts/generate-release-notes.ts
FRAG_FILE="$FRAGMENT_FILE" node -e '
const fs = require("fs");
const path = "packages/boxel-cli/CHANGELOG.md";
const ANCHOR = "<!-- New entries are inserted directly below this line";
const fragment = fs.readFileSync(process.env.FRAG_FILE, "utf8");
const existing = fs.readFileSync(path, "utf8");
const idx = existing.indexOf(ANCHOR);
let head, tail;
if (idx === -1) {
head = "";
tail = existing;
} else {
const lineEnd = existing.indexOf("\n", idx);
head = existing.slice(0, lineEnd + 1);
tail = existing.slice(lineEnd + 1);
}
fs.writeFileSync(path, head + "\n" + fragment + "\n" + tail);
'
echo "has=true" >> "$GITHUB_OUTPUT"
echo "body_file=$BODY_FILE" >> "$GITHUB_OUTPUT"

- name: Commit and tag stable
env:
VERSION: ${{ steps.strip.outputs.version }}
run: |
set -euo pipefail
TAG="boxel-cli-v${VERSION}"
# Guard against an orphan tag from a prior failed promotion. If the
# tag exists, the previous run pushed it but failed before publish —
# operator needs to delete the orphan (`git push origin :refs/tags/$TAG`)
# and verify npm state before re-running.
if git rev-parse --verify --quiet "refs/tags/$TAG" >/dev/null; then
echo "::error::Tag $TAG already exists. A prior promotion likely failed mid-flight. Delete the orphan tag and verify npm state before re-running."
exit 1
fi
git add packages/boxel-cli/package.json packages/boxel-cli/CHANGELOG.md
git commit -m "Release @cardstack/boxel-cli v${VERSION} [skip ci]"
# Annotated tag so `git push --follow-tags` actually pushes it.
git tag -a "$TAG" -m "$TAG"
git push origin main --follow-tags

- name: Publish to npm under latest
working-directory: packages/boxel-cli
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: pnpm publish --access public --provenance --no-git-checks

- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.strip.outputs.version }}
NOTES_HAS: ${{ steps.notes.outputs.has }}
NOTES_BODY_FILE: ${{ steps.notes.outputs.body_file }}
run: |
set -euo pipefail
TAG="boxel-cli-v${VERSION}"
if [ "$NOTES_HAS" = "true" ] && [ -f "$NOTES_BODY_FILE" ]; then
gh release create "$TAG" \
--title "@cardstack/boxel-cli v${VERSION}" \
--notes-file "$NOTES_BODY_FILE"
else
NPM_URL="https://www.npmjs.com/package/@cardstack/boxel-cli/v/${VERSION}"
gh release create "$TAG" \
--title "@cardstack/boxel-cli v${VERSION}" \
--notes "Promoted from unstable. Published to npm under dist-tag \`latest\`: ${NPM_URL}"
fi
Loading
Loading