diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml new file mode 100644 index 0000000..f5dce5d --- /dev/null +++ b/.github/workflows/publish-cli.yml @@ -0,0 +1,76 @@ +name: Publish CLI + +# Publishes @codeplans/cli to npm. It runs after Publish Release creates a +# release, when a release is published from the GitHub UI, or by hand. The +# version in cli/package.json must match the tag (v0.7.7 → 0.7.7), and a +# version that is already on npm (or a tag from before the CLI existed) is +# skipped, so re-running and backfilling releases are safe. +on: + release: + types: [published] + workflow_call: + inputs: + tag: + required: true + type: string + secrets: + NPM_TOKEN: + required: true + workflow_dispatch: + inputs: + tag: + description: Release tag to publish the CLI from (e.g. v0.7.7) + required: true + type: string + +permissions: + contents: read + id-token: write # npm provenance + +jobs: + publish: + runs-on: ubuntu-latest + env: + TAG: ${{ inputs.tag || github.event.release.tag_name }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ env.TAG }} + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + registry-url: https://registry.npmjs.org + - run: pnpm install --frozen-lockfile + - name: Check the version + id: version + run: | + if [ ! -f cli/package.json ]; then + echo "$TAG has no CLI; nothing to publish." + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + version=$(node -p "require('./cli/package.json').version") + if [ "v$version" != "$TAG" ]; then + echo "::error::cli/package.json is $version, but the tag is $TAG" + exit 1 + fi + if npm view "@codeplans/cli@$version" version >/dev/null 2>&1; then + echo "@codeplans/cli@$version is already on npm; nothing to publish." + echo "skip=true" >> "$GITHUB_OUTPUT" + fi + - name: Test + if: steps.version.outputs.skip != 'true' + run: pnpm vitest run cli + - name: Build and publish + if: steps.version.outputs.skip != 'true' + working-directory: cli + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + npm run build + test "$(node dist/index.js --version)" = "${TAG#v}" + npm publish --access public --provenance diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index b01f845..0332ff2 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -46,3 +46,16 @@ jobs: else gh release create "$TAG" --verify-tag --title "${TITLE:-$TAG}" --notes "$NOTES" fi + + # Releases made here use the workflow token, which doesn't trigger other + # workflows, so the CLI is published from here explicitly. + cli: + needs: publish + permissions: + contents: read + id-token: write + uses: ./.github/workflows/publish-cli.yml + with: + tag: ${{ inputs.tag }} + secrets: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/cli/package.json b/cli/package.json index e0928d7..d5d53ff 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@codeplans/cli", - "version": "0.1.0", + "version": "0.7.7", "description": "Command-line tool for CodePlans: script workspace setups, work with code plans, tasks and work items from your terminal.", "license": "MIT", "type": "module", diff --git a/cli/src/cli.ts b/cli/src/cli.ts index d6c4527..52cc741 100644 --- a/cli/src/cli.ts +++ b/cli/src/cli.ts @@ -2,12 +2,14 @@ // node:util parseArgs; the runner adds --json, --profile and --help, turns // errors into messages on stderr and exit codes, and creates the client lazily // (login and help never need one). +import { readFileSync } from 'node:fs' import { parseArgs, type ParseArgsConfig } from 'node:util' import { Client, CliError, EXIT, type FetchLike } from './client.js' import { configPath, resolveConnection, type Connection, type Env } from './config.js' import { createOut, paint, table, type Out, type Sink } from './output.js' -export const VERSION = '0.1.0' +/** From the package's own package.json (one level up from both src/ and dist/), so it can't drift. */ +export const VERSION: string = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version type Options = NonNullable export type Values = Record diff --git a/package.json b/package.json index daebff2..bede5cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codeplans", - "version": "0.7.6", + "version": "0.7.7", "description": "Manage and track coordinated changes across your software architecture.", "author": "Sai Prakash ", "homepage": "https://codeplans.ai",