Skip to content

fix(credentials): correct keyless setup instructions and enforce the … #285

fix(credentials): correct keyless setup instructions and enforce the …

fix(credentials): correct keyless setup instructions and enforce the … #285

name: publish-sim-cli
on:
push:
branches: [main, staging, dev]
paths:
- 'packages/sim-cli/**'
# The build bundles its workspace dependencies, so a change there
# changes what ships.
- 'packages/utils/**'
# A change to how the package is built or published has to republish it.
- '.github/workflows/publish-sim-cli.yml'
permissions:
contents: read
concurrency:
group: publish-sim-cli-${{ github.ref }}
cancel-in-progress: false
jobs:
build:
name: build
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
timeout-minutes: 15
outputs:
version: ${{ steps.release.outputs.version }}
tag: ${{ steps.release.outputs.tag }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
with:
persist-credentials: false
# Vitest 5 requires Node 22.12+; the published bundle is still
# smoke-tested on Node 20 below, its minimum supported runtime.
- name: Setup workspace
uses: ./.github/actions/setup
with:
provider: ${{ vars.CI_PROVIDER }}
node-version: '22'
- name: Auto-bump package version
run: bun run bump:npm-packages sim-cli
- name: Run tests
working-directory: packages/sim-cli
run: bun run test
- name: Type-check package
working-directory: packages/sim-cli
run: bun run type-check
# The usage-reporting destination is baked into the bundle at build time
# (docs/cli/usage-data), so only this step gets the token, and the
# tarball packed from its output is what ships. Every other step, the
# tests above in particular, runs without it: a test that runs a real
# command must never report to production.
- name: Build package
working-directory: packages/sim-cli
env:
SIM_CLI_TELEMETRY_KEY: ${{ vars.SIM_CLI_TELEMETRY_KEY }}
SIM_CLI_TELEMETRY_HOST: ${{ vars.SIM_CLI_TELEMETRY_HOST }}
run: bun run build
- name: Resolve release channel
id: release
working-directory: packages/sim-cli
env:
BRANCH: ${{ github.ref_name }}
run: |
BASE_VERSION="$(bun -p "require('./package.json').version")"
if [[ ! "$BASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Package version must be a stable X.Y.Z base, got '$BASE_VERSION'." >&2
exit 1
fi
case "$BRANCH" in
dev)
VERSION="${BASE_VERSION}-dev.${GITHUB_RUN_NUMBER}.${GITHUB_RUN_ATTEMPT}"
TAG="dev"
;;
staging)
VERSION="${BASE_VERSION}-preview.${GITHUB_RUN_NUMBER}.${GITHUB_RUN_ATTEMPT}"
TAG="staging"
;;
main)
VERSION="$BASE_VERSION"
TAG="latest"
;;
*)
echo "Unsupported release branch '$BRANCH'." >&2
exit 1
;;
esac
bun pm pkg set "version=$VERSION"
RESOLVED_VERSION="$(bun -p "require('./package.json').version")"
if [ "$RESOLVED_VERSION" != "$VERSION" ]; then
echo "Version injection mismatch: wanted '$VERSION', got '$RESOLVED_VERSION'." >&2
exit 1
fi
{
echo "version=$VERSION"
echo "tag=$TAG"
} >> "$GITHUB_OUTPUT"
# `./embed` and the `#sim-cli/*` imports point at TypeScript source, which
# only the monorepo's own Next.js app consumes (it transpiles the package).
# The tarball ships `dist` alone, so published they would be dead entry
# points; the smoke test below fails on any export that does not exist.
- name: Drop source-only entry points
working-directory: packages/sim-cli
run: |
jq 'del(.imports, .exports["./embed"])' package.json > "$RUNNER_TEMP/package.json"
mv "$RUNNER_TEMP/package.json" package.json
# Bun packs because it rewrites `workspace:` ranges to real versions,
# which `npm pack` would publish verbatim. The smoke test below and the
# publish job both use this one tarball, so what was tested is what ships.
- name: Pack package
working-directory: packages/sim-cli
run: bun pm pack --ignore-scripts --filename "$RUNNER_TEMP/package.tgz" --quiet
- name: Setup Node 20 for the bundle smoke test
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6
with:
node-version: '20'
- name: Smoke-test packed Node bundle
env:
VERSION: ${{ steps.release.outputs.version }}
run: |
set -euo pipefail
SMOKE_DIR="$(mktemp -d "$RUNNER_TEMP/sim-cli-smoke.XXXXXX")"
tar -xzf "$RUNNER_TEMP/package.tgz" -C "$SMOKE_DIR"
test "$("$SMOKE_DIR/package/dist/index.js" --version)" = "$VERSION"
node -e '
const fs = require("node:fs")
const path = require("node:path")
const root = path.join(process.argv[1], "package")
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"))
const targets = Object.values(pkg.exports ?? {}).flatMap((entry) =>
typeof entry === "string" ? [entry] : Object.values(entry)
)
const missing = targets.filter((target) => !fs.existsSync(path.join(root, target)))
if (missing.length > 0) {
console.error("Exports point at files the tarball does not contain: " + missing.join(", "))
process.exit(1)
}
' "$SMOKE_DIR"
- name: Verify version is unpublished
env:
VERSION: ${{ steps.release.outputs.version }}
run: |
if bun pm view "sim@$VERSION" version > /dev/null 2>&1; then
echo "sim@$VERSION is already published. The automatic version bump did not produce a unique release." >&2
exit 1
fi
- name: Upload package
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: package
path: ${{ runner.temp }}/package.tgz
if-no-files-found: error
retention-days: 1
# npm trusted publishing trades this job's OIDC token for a short-lived npm
# credential, so no npm token is stored anywhere. npm accepts the token only
# from GitHub-hosted runners, and only this job can mint one: it runs no
# repository code, it publishes the tarball the build job tested. npm runs no
# lifecycle scripts for a tarball, so nothing is rebuilt here, and provenance
# is attached automatically.
publish:
name: publish
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
id-token: write
steps:
# Node 24 bundles npm 11.5.1+, which trusted publishing requires.
- name: Setup Node
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6
with:
node-version: '24'
- name: Download package
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: package
path: ${{ runner.temp }}
- name: Publish to npm
env:
NPM_TAG: ${{ needs.build.outputs.tag }}
run: npm publish "$RUNNER_TEMP/package.tgz" --access public --tag "$NPM_TAG"
- name: Summarize release
env:
VERSION: ${{ needs.build.outputs.version }}
NPM_TAG: ${{ needs.build.outputs.tag }}
run: echo "Published sim@$VERSION with the '$NPM_TAG' tag."