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
97 changes: 97 additions & 0 deletions .github/workflows/cut-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
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 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:
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 the coreplane-bot App token, not GITHUB_TOKEN

jobs:
cut:
name: Bump, verify, tag, push
runs-on: ubuntu-latest
steps:
# 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:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
# Syntax: op://<vault-name>/<item-name>/<field-name>
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 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).
- 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"
24 changes: 22 additions & 2 deletions release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 <version>"
fi
read -rp "Release version (no v prefix, e.g. 0.1.0): " VERSION
fi
VERSION="${VERSION#v}"
Expand All @@ -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
Expand All @@ -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"
Expand Down
Loading