Files
openprose__prose/.github/workflows/cli-publish.yml
2026-04-29 20:31:10 -07:00

356 lines
10 KiB
YAML

name: CLI Publish
on:
workflow_dispatch:
inputs:
version:
description: "Version to publish. Must match cli/package.json, for example 1.2.3."
required: true
type: string
npm_tag:
description: "npm dist-tag to publish."
required: true
default: latest
type: choice
options:
- latest
- next
dry_run:
description: "Build and verify without publishing."
required: true
default: true
type: boolean
permissions:
contents: read
concurrency:
group: cli-publish-${{ inputs.version }}
cancel-in-progress: false
env:
PACKAGE_NAME: "@openprose/prose-cli"
RELEASE_ACTORS: "irl-dan josemontesdeoca"
jobs:
authorize:
name: Authorize release
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
npm_tag: ${{ steps.release.outputs.npm_tag }}
release_tag: ${{ steps.release.outputs.release_tag }}
version: ${{ steps.release.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Validate release request
id: release
env:
GH_TOKEN: ${{ github.token }}
INPUT_DRY_RUN: ${{ inputs.dry_run }}
GITHUB_TRIGGERING_ACTOR: ${{ github.triggering_actor }}
INPUT_NPM_TAG: ${{ inputs.npm_tag }}
INPUT_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
fail() {
printf '%s\n' "$*" >&2
exit 1
}
actor="${GITHUB_ACTOR}"
triggering_actor="${GITHUB_TRIGGERING_ACTOR:-$GITHUB_ACTOR}"
case " $RELEASE_ACTORS " in
*" $actor "*) ;;
*) fail "Unauthorized release actor: $actor" ;;
esac
case " $RELEASE_ACTORS " in
*" $triggering_actor "*) ;;
*) fail "Unauthorized triggering actor: $triggering_actor" ;;
esac
[ "$GITHUB_REF" = "refs/heads/main" ] || fail "CLI releases must be run from main, not $GITHUB_REF"
version="${INPUT_VERSION#v}"
case "$version" in
""|*[!A-Za-z0-9._-]*) fail "version may only contain letters, numbers, dots, underscores, and dashes" ;;
esac
package_version="$(node -p 'require("./cli/package.json").version')"
[ "$version" = "$package_version" ] || {
fail "Input version ($version) must match cli/package.json ($package_version)"
}
installer_version="$(sed -n 's/^DEFAULT_VERSION="\([^"]*\)"/\1/p' cli/install.sh)"
[ "$version" = "$installer_version" ] || {
fail "Input version ($version) must match cli/install.sh DEFAULT_VERSION ($installer_version)"
}
release_tag="v$version"
if [ "$INPUT_DRY_RUN" = "true" ]; then
printf '%s\n' "Dry run requested: skipping tag, release, and npm-version uniqueness checks."
else
if git ls-remote --exit-code --tags origin "refs/tags/$release_tag" >/dev/null 2>&1; then
fail "Git tag already exists: $release_tag"
fi
if gh release view "$release_tag" >/dev/null 2>&1; then
fail "GitHub release already exists: $release_tag"
fi
if npm view "$PACKAGE_NAME@$version" version >/tmp/prose-npm-version 2>/dev/null; then
published_version="$(cat /tmp/prose-npm-version)"
fail "npm package version already exists: $PACKAGE_NAME@$published_version"
fi
fi
{
printf 'npm_tag=%s\n' "$INPUT_NPM_TAG"
printf 'release_tag=%s\n' "$release_tag"
printf 'version=%s\n' "$version"
} >> "$GITHUB_OUTPUT"
verify:
name: Verify package
runs-on: ubuntu-latest
timeout-minutes: 20
needs: authorize
defaults:
run:
working-directory: cli
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: cli/package-lock.json
- name: Install dependencies
run: npm ci
- name: Typecheck
run: npm run typecheck
- name: Test
run: npm test
- name: Build
run: npm run build
- name: Audit production dependencies
run: npm audit --omit=dev
- name: Dry-run npm publish
run: npm publish --dry-run
release-assets:
name: Build ${{ matrix.target_os }}-${{ matrix.target_arch }} tarball
runs-on: ubuntu-latest
timeout-minutes: 25
needs:
- authorize
- verify
strategy:
fail-fast: false
matrix:
include:
- target_os: linux
target_arch: x64
- target_os: linux
target_arch: arm64
- target_os: darwin
target_arch: x64
- target_os: darwin
target_arch: arm64
defaults:
run:
working-directory: cli
env:
TARGET_ARCH: ${{ matrix.target_arch }}
TARGET_OS: ${{ matrix.target_os }}
VERSION: ${{ needs.authorize.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: cli/package-lock.json
- name: Install dependencies
run: npm ci
- name: Build release tarball
run: |
set -euo pipefail
release_dir="$RUNNER_TEMP/prose-release"
npm run release:tarball -- \
--version "$VERSION" \
--os "$TARGET_OS" \
--arch "$TARGET_ARCH" \
--out-dir "$release_dir"
cd "$release_dir"
sha256sum --check "prose-$VERSION-$TARGET_OS-$TARGET_ARCH.tar.gz.sha256"
tar -tzf "prose-$VERSION-$TARGET_OS-$TARGET_ARCH.tar.gz" >/dev/null
- name: Smoke native Linux x64 tarball
if: ${{ matrix.target_os == 'linux' && matrix.target_arch == 'x64' }}
run: |
set -euo pipefail
release_dir="$RUNNER_TEMP/prose-release"
install_dir="$RUNNER_TEMP/prose-install"
bin_dir="$RUNNER_TEMP/prose-bin"
env \
PROSE_VERSION="$VERSION" \
PROSE_OS="$TARGET_OS" \
PROSE_ARCH="$TARGET_ARCH" \
PROSE_BASE_URL="file://$release_dir" \
PROSE_INSTALL_DIR="$install_dir" \
PROSE_BIN_DIR="$bin_dir" \
sh install.sh
"$bin_dir/prose" --version
"$bin_dir/prose" run hello.md --harness mock
- name: Upload release tarball
uses: actions/upload-artifact@v4
with:
name: cli-release-${{ matrix.target_os }}-${{ matrix.target_arch }}
path: ${{ runner.temp }}/prose-release/*
if-no-files-found: error
publish:
name: Publish npm package and GitHub release
if: ${{ !inputs.dry_run }}
runs-on: ubuntu-latest
timeout-minutes: 25
needs:
- authorize
- release-assets
environment: release
permissions:
contents: write
id-token: write
defaults:
run:
working-directory: cli
env:
GH_TOKEN: ${{ github.token }}
NPM_CONFIG_PROVENANCE: "true"
NPM_TAG: ${{ needs.authorize.outputs.npm_tag }}
RELEASE_TAG: ${{ needs.authorize.outputs.release_tag }}
VERSION: ${{ needs.authorize.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node for trusted publishing
uses: actions/setup-node@v4
with:
node-version: 24
registry-url: https://registry.npmjs.org
cache: npm
cache-dependency-path: cli/package-lock.json
- name: Install npm with trusted publishing support
run: npm install --global npm@latest
- name: Install dependencies
run: npm ci
- name: Download release assets
uses: actions/download-artifact@v4
with:
pattern: cli-release-*
path: ${{ runner.temp }}/prose-release-assets
merge-multiple: true
- name: Validate release assets
run: |
set -euo pipefail
asset_dir="$RUNNER_TEMP/prose-release-assets"
expected_assets="
prose-$VERSION-linux-x64.tar.gz
prose-$VERSION-linux-arm64.tar.gz
prose-$VERSION-darwin-x64.tar.gz
prose-$VERSION-darwin-arm64.tar.gz
"
for asset in $expected_assets; do
[ -f "$asset_dir/$asset" ] || {
printf 'Missing release asset: %s\n' "$asset" >&2
exit 1
}
[ -f "$asset_dir/$asset.sha256" ] || {
printf 'Missing release checksum: %s.sha256\n' "$asset" >&2
exit 1
}
done
cd "$asset_dir"
for checksum in *.sha256; do
sha256sum --check "$checksum"
done
- name: Create draft GitHub release
id: create_draft_release
run: |
set -euo pipefail
asset_dir="$RUNNER_TEMP/prose-release-assets"
notes_path="$RUNNER_TEMP/prose-release-notes.md"
cat > "$notes_path" <<EOF
Prose CLI $VERSION.
Install with npm:
npm install --global $PACKAGE_NAME@$VERSION
Install with the release tarball:
curl -fsSL https://raw.githubusercontent.com/openprose/prose/main/cli/install.sh | sh
EOF
gh release create "$RELEASE_TAG" "$asset_dir"/* \
--target "$GITHUB_SHA" \
--title "Prose CLI $VERSION" \
--notes-file "$notes_path" \
--draft
- name: Publish npm package
id: publish_npm
run: |
set -euo pipefail
node --version
npm --version
npm publish --access public --tag "$NPM_TAG"
- name: Publish GitHub release
id: publish_github_release
run: gh release edit "$RELEASE_TAG" --draft=false
- name: Delete draft release after npm publish failure
if: ${{ failure() && steps.create_draft_release.outcome == 'success' && steps.publish_npm.outcome == 'failure' }}
run: gh release delete "$RELEASE_TAG" --yes --cleanup-tag || true