From f1b58c00291e81d3696cf2a355be8c50affbf094 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 23:47:15 +0000 Subject: [PATCH 1/2] ci: move release cut into a workflow_dispatch GitHub Actions workflow Releases previously required running ./release.sh locally. Add a 'Cut Release' workflow that runs release.sh in CI: bump package.json, run the full gate (typecheck against the live prod OpenAPI spec, lint, test, build), then commit, tag, and push. The v* tag push triggers the existing release.yml publish pipeline unchanged. release.sh changes are minimal and keep it usable locally: - CI mode (CI env set): version arg required, branch must be main, no interactive prompts - RELEASE_DRY_RUN=1: run the bump and all checks, then revert and exit before commit/tag/push The workflow pushes with a RELEASE_PUSH_TOKEN PAT secret rather than GITHUB_TOKEN, because tags pushed with GITHUB_TOKEN do not trigger other workflows (release.yml would never fire). --- .github/workflows/cut-release.yml | 83 +++++++++++++++++++++++++++++++ release.sh | 24 ++++++++- 2 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/cut-release.yml diff --git a/.github/workflows/cut-release.yml b/.github/workflows/cut-release.yml new file mode 100644 index 0000000..615f3d5 --- /dev/null +++ b/.github/workflows/cut-release.yml @@ -0,0 +1,83 @@ +name: Cut Release + +# CI replacement for running ./release.sh on a laptop. Kick it off from the +# Actions tab ("Cut Release" → Run workflow) with the version to release. +# +# This runs release.sh itself — one source of truth: bump package.json → +# typecheck (against the live prod OpenAPI spec) + lint + test + build → +# commit → tag → push. Nothing is committed, tagged, or pushed unless every +# check passes. Once the v* tag reaches origin, release.yml takes over +# (npm publish, GitHub release, Homebrew bump), exactly as before. +# +# The push uses the RELEASE_PUSH_TOKEN secret (a PAT with contents: write on +# this repo), NOT the default GITHUB_TOKEN — tags pushed with GITHUB_TOKEN +# do not trigger other workflows, so release.yml would never fire. + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to release (no v prefix, e.g. 0.3.0)' + required: true + type: string + dry_run: + description: 'Dry run: bump + run all checks, but do not commit/tag/push' + required: false + type: boolean + default: false + +concurrency: + group: cut-release + cancel-in-progress: false + +permissions: + contents: read # pushes go through RELEASE_PUSH_TOKEN, not GITHUB_TOKEN + +jobs: + cut: + name: Bump, verify, tag, push + runs-on: ubuntu-latest + steps: + - name: Ensure RELEASE_PUSH_TOKEN is configured + if: ${{ !inputs.dry_run }} + env: + RELEASE_PUSH_TOKEN: ${{ secrets.RELEASE_PUSH_TOKEN }} + run: | + if [ -z "$RELEASE_PUSH_TOKEN" ]; then + echo "::error::RELEASE_PUSH_TOKEN secret is not set. Add a fine-grained PAT with contents:read/write on coreplanelabs/cli (Settings → Secrets and variables → Actions). The default GITHUB_TOKEN cannot be used: tags it pushes do not trigger release.yml." + exit 1 + fi + + - uses: actions/checkout@v4 + with: + # release.sh pushes a commit + tag back to this branch; the PAT in + # the remote URL is what lets that push trigger release.yml. + token: ${{ secrets.RELEASE_PUSH_TOKEN || github.token }} + fetch-depth: 0 + + # Node 24 to match release.yml (see the npm/Trusted Publishers note there). + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 24.x + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Configure git author + run: | + git config user.name 'coreplane-bot' + git config user.email 'bot@coreplane.dev' + + # release.sh runs the full gate before touching git: typecheck (codegen + # against the live prod spec — hard gate, the polylane.com installer + # serves the latest release), lint, test, build. + - name: Run release.sh + env: + VERSION: ${{ inputs.version }} + RELEASE_DRY_RUN: ${{ inputs.dry_run && '1' || '' }} + POLYLANE_API_DOMAIN: ${{ secrets.POLYLANE_API_DOMAIN }} + POLYLANE_OAUTH_CLIENT_ID: ${{ secrets.POLYLANE_OAUTH_CLIENT_ID }} + POLYLANE_OAUTH_CLIENT_SECRET: ${{ secrets.POLYLANE_OAUTH_CLIENT_SECRET }} + run: ./release.sh "$VERSION" diff --git a/release.sh b/release.sh index 2ec2d73..27343e8 100755 --- a/release.sh +++ b/release.sh @@ -2,10 +2,17 @@ # Cut a new release of the polylane CLI. # # ./release.sh 0.1.0 -# ./release.sh # prompts for version +# ./release.sh # prompts for version (interactive only) # # Steps: bump package.json → typecheck + lint + test + build → commit → tag → # push. `release.yml` takes over once the v* tag reaches origin. +# +# CI mode (CI env var set, as in GitHub Actions): no prompts — the version +# argument is required and the branch must be main. Run via the "Cut Release" +# workflow (.github/workflows/cut-release.yml) instead of a laptop. +# +# RELEASE_DRY_RUN=1 runs the version bump and all checks, then reverts the +# bump and exits before committing, tagging, or pushing. set -euo pipefail @@ -22,9 +29,12 @@ if [ -n "$(git status --porcelain)" ]; then die "working tree is dirty — commit or stash first" fi -# Version: arg or prompt. +# Version: arg or prompt (never prompt in CI). VERSION="${1:-}" if [ -z "$VERSION" ]; then + if [ -n "${CI:-}" ]; then + die "version argument required in CI: ./release.sh " + fi read -rp "Release version (no v prefix, e.g. 0.1.0): " VERSION fi VERSION="${VERSION#v}" @@ -45,6 +55,9 @@ fi BRANCH="$(git rev-parse --abbrev-ref HEAD)" if [ "$BRANCH" != "main" ]; then + if [ -n "${CI:-}" ]; then + die "refusing to release from '$BRANCH' in CI — releases are cut from main" + fi read -rp "Current branch is '$BRANCH', not 'main'. Continue? [y/N] " ans [[ "$ans" =~ ^[Yy]$ ]] || die "aborted" fi @@ -66,6 +79,13 @@ info "build" npm run build ok "all checks passed" +if [ -n "${RELEASE_DRY_RUN:-}" ]; then + info "dry run — reverting version bump; skipping commit, tag, and push" + git checkout -- package.json package-lock.json + ok "Dry run for $TAG passed all checks" + exit 0 +fi + info "committing version bump" git add package.json package-lock.json git commit -m "chore: release $TAG" From adf0fa9d3afeb1055776118a538a6822d50867b0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 15:36:00 +0000 Subject: [PATCH 2/2] ci: source the release push credential from 1Password instead of a stored PAT Replace the RELEASE_PUSH_TOKEN fine-grained PAT with the org-standard pattern: load coreplane-bot GitHub App credentials from the 1Password CI vault (op://CI/coreplane-bot) via load-secrets-action, mint a short-lived App installation token with create-github-app-token, and hand that to actions/checkout so release.sh's push still triggers release.yml. Dry runs skip the 1Password/App-token steps entirely and check out with the default read-only GITHUB_TOKEN, so they keep working without any secret configured. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01LekzRRztX696SLGyefENYm --- .github/workflows/cut-release.yml | 42 ++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/.github/workflows/cut-release.yml b/.github/workflows/cut-release.yml index 615f3d5..3e70438 100644 --- a/.github/workflows/cut-release.yml +++ b/.github/workflows/cut-release.yml @@ -9,9 +9,10 @@ name: Cut Release # check passes. Once the v* tag reaches origin, release.yml takes over # (npm publish, GitHub release, Homebrew bump), exactly as before. # -# The push uses the RELEASE_PUSH_TOKEN secret (a PAT with contents: write on -# this repo), NOT the default GITHUB_TOKEN — tags pushed with GITHUB_TOKEN -# do not trigger other workflows, so release.yml would never fire. +# The push uses a short-lived coreplane-bot GitHub App token (credentials +# loaded from the 1Password CI vault), NOT the default GITHUB_TOKEN — tags +# pushed with GITHUB_TOKEN do not trigger other workflows, so release.yml +# would never fire. on: workflow_dispatch: @@ -31,28 +32,41 @@ concurrency: cancel-in-progress: false permissions: - contents: read # pushes go through RELEASE_PUSH_TOKEN, not GITHUB_TOKEN + contents: read # pushes go through the coreplane-bot App token, not GITHUB_TOKEN jobs: cut: name: Bump, verify, tag, push runs-on: ubuntu-latest steps: - - name: Ensure RELEASE_PUSH_TOKEN is configured + # Dry runs never push, so they skip the 1Password/App-token steps and + # check out with the default read-only GITHUB_TOKEN. + - name: Load secrets from 1Password if: ${{ !inputs.dry_run }} + uses: 1password/load-secrets-action@eb2efd0703da22a93c467f2d1ffbb6826c11e19c # v4.1.1 + with: + export-env: true env: - RELEASE_PUSH_TOKEN: ${{ secrets.RELEASE_PUSH_TOKEN }} - run: | - if [ -z "$RELEASE_PUSH_TOKEN" ]; then - echo "::error::RELEASE_PUSH_TOKEN secret is not set. Add a fine-grained PAT with contents:read/write on coreplanelabs/cli (Settings → Secrets and variables → Actions). The default GITHUB_TOKEN cannot be used: tags it pushes do not trigger release.yml." - exit 1 - fi + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + # Syntax: op://// + COREPLANE_CLIENT_ID: op://CI/coreplane-bot/client-id + COREPLANE_PRIVATE_SIGNING_KEY: op://CI/coreplane-bot/private-key + + # Tags pushed with the default GITHUB_TOKEN don't trigger release.yml; the App token does. + - name: Generate a token + if: ${{ !inputs.dry_run }} + id: generate-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 + with: + client-id: ${{ env.COREPLANE_CLIENT_ID }} + private-key: ${{ env.COREPLANE_PRIVATE_SIGNING_KEY }} + permission-contents: write - uses: actions/checkout@v4 with: - # release.sh pushes a commit + tag back to this branch; the PAT in - # the remote URL is what lets that push trigger release.yml. - token: ${{ secrets.RELEASE_PUSH_TOKEN || github.token }} + # release.sh pushes a commit + tag back to this branch; the App + # token in the remote URL is what lets that push trigger release.yml. + token: ${{ steps.generate-token.outputs.token || github.token }} fetch-depth: 0 # Node 24 to match release.yml (see the npm/Trusted Publishers note there).