diff --git a/.github/scripts/smoke-test-binary.sh b/.github/scripts/smoke-test-binary.sh new file mode 100755 index 0000000..8ae7ac0 --- /dev/null +++ b/.github/scripts/smoke-test-binary.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Smoke-tests one compiled `ct` binary (`bun build --compile` output) with no Node +# and no ChurchTools instance available. +# +# `ct --help` alone doesn't prove much: the risky part of compiling this CLI is +# jiti's *runtime* TypeScript transpilation of a user's `.config.ts` — that only +# happens once a command actually loads a config file, not on `--help`. So this +# also runs `ct plan` against a real fixture config (tests/fixtures/sample.config.ts) +# with a bogus CT_HOST. That command is expected to fail — but it must fail at the +# *auth* step (authedSession() in src/api/session.ts: either "Not logged in", the +# expected outcome on a fresh CI runner with no stored credentials, or the +# host-mismatch "Refusing to send the stored login token" if a credential happens +# to be present for a different host), which only runs AFTER the config file has +# been located, transpiled by jiti, and evaluated. If the binary instead fails to +# find/parse the config, that's a real jiti-in-a-compiled-binary bug, not one of +# the expected auth failures, and this script flags it as such. +# +# What this proves: the compiled binary can locate and run, and can dynamically +# transpile + evaluate a TypeScript config file at runtime. +# What this does NOT prove: a real `ct plan`/`ct apply` against a live ChurchTools +# instance — no network call is made. +set -euo pipefail + +bin="$1" +chmod +x "$bin" + +echo "== ct --help ==" +"$bin" --help + +echo +echo "== ct plan (config-load exercise, no network/creds) ==" +set +e +out="$(CT_HOST=https://ci-smoke-test.invalid "$bin" plan \ + --config tests/fixtures/sample.config.ts \ + --state /tmp/ct-smoke-state.json 2>&1)" +rc=$? +set -e + +echo "$out" + +if [ "$rc" -eq 0 ]; then + echo "FAIL: expected a non-zero exit (no stored/CI credentials) but the command succeeded" >&2 + exit 1 +fi + +if ! grep -Eq "Not logged in|Refusing to send the stored login token" <<<"$out"; then + echo "FAIL: expected an auth-layer error ('Not logged in' or the host-mismatch refusal) proving config load succeeded; got a different failure (possible config-load/jiti bug in the compiled binary)" >&2 + exit 1 +fi + +echo +echo "OK: binary parsed the fixture TS config at runtime and failed at the auth step, as expected." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f89671b..38017b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,19 +1,32 @@ name: Release -# Triggered by pushing a version tag (e.g. `v0.1.0`). Runs the same -# lint/typecheck/test/build gate as CI, then packages the built CLI as an -# npm-pack tarball and publishes it as a GitHub Release with -# auto-generated release notes. +# Trunk-based, per-merge releases: every push to `main` runs the CI gate, compiles +# standalone binaries, smoke-tests each one on its native OS/arch, and — only if +# everything above is green — hands off to semantic-release. semantic-release reads +# the conventional-commit history since the last release and, if there is a +# releasable `feat:`/`fix:`/breaking change, creates the version tag, the GitHub +# Release (with auto-generated notes as the changelog), and attaches the tarball + +# binaries built by the `build` job. No manual tag push (see #9; builds on the +# tag-triggered flow from #44, which this replaces). +# +# semantic-release publishes via the GitHub API using the default GITHUB_TOKEN, so +# the tag/release it creates does NOT re-trigger this workflow (GitHub does not fire +# `push` events for repo activity performed with the default token) — no loop risk. on: push: - tags: - - "v*" + branches: [main] + workflow_dispatch: {} permissions: contents: write jobs: - release: + # CI gate + build the release assets every job below needs. Binaries are cross- + # compiled for all three targets from this single Linux runner — `bun build + # --compile` embeds a prebuilt runtime per target, so it doesn't need to run on + # the target OS to produce the binary (only to *execute* it, which is what the + # smoke-test jobs are for). + build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -29,38 +42,125 @@ jobs: - run: npm test - run: npm run build - - name: Package release tarball + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Compile standalone binaries run: | mkdir -p release + bun build --compile --target=bun-darwin-arm64 ./src/index.ts --outfile release/ct-darwin-arm64 + bun build --compile --target=bun-darwin-x64 ./src/index.ts --outfile release/ct-darwin-x64 + bun build --compile --target=bun-linux-x64 ./src/index.ts --outfile release/ct-linux-x64 + chmod +x release/ct-darwin-arm64 release/ct-darwin-x64 release/ct-linux-x64 + + - name: Package npm-pack tarball + run: | npm pack --pack-destination release - mv release/*.tgz "release/ct-cli-${GITHUB_REF_NAME}.tgz" + mv release/*.tgz release/ct-cli.tgz - name: Write install note run: | - cat > release/INSTALL.md < release/INSTALL.md <<'EOF' + # Installing ct - \`\`\`bash - npm install -g ./ct-cli-${GITHUB_REF_NAME}.tgz - \`\`\` + ## Standalone binary (no Node required) - Or directly from the release asset URL: + ```bash + # macOS, Apple Silicon + curl -L -o ct https://github.com/eqrm/ct-cli/releases/latest/download/ct-darwin-arm64 + # macOS, Intel + curl -L -o ct https://github.com/eqrm/ct-cli/releases/latest/download/ct-darwin-x64 + # Linux, x64 + curl -L -o ct https://github.com/eqrm/ct-cli/releases/latest/download/ct-linux-x64 - \`\`\`bash - npm install -g https://github.com/${GITHUB_REPOSITORY}/releases/download/${GITHUB_REF_NAME}/ct-cli-${GITHUB_REF_NAME}.tgz - \`\`\` + chmod +x ct + sudo mv ct /usr/local/bin/ct # or anywhere on your PATH + ct --help + ``` - Verify with: + ## npm tarball (requires Node >= 20) - \`\`\`bash + ```bash + npm install -g https://github.com/eqrm/ct-cli/releases/latest/download/ct-cli.tgz ct --help - \`\`\` + ``` EOF - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 + - uses: actions/upload-artifact@v4 + with: + name: release-assets + path: release/ + retention-days: 1 + if-no-files-found: error + + # Each compiled binary is smoke-tested on its own native OS/arch — cross-compiled + # output can look fine and still fail to *run* jiti's dynamic TS transpilation at + # runtime, which is the whole risk of `bun build --compile` here (see + # .github/scripts/smoke-test-binary.sh for exactly what this does and doesn't prove). + smoke-darwin-arm64: + needs: build + runs-on: macos-14 # Apple Silicon + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: release-assets + path: release + - run: bash .github/scripts/smoke-test-binary.sh release/ct-darwin-arm64 + + smoke-darwin-x64: + needs: build + runs-on: macos-13 # Intel + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: release-assets + path: release + - run: bash .github/scripts/smoke-test-binary.sh release/ct-darwin-x64 + + smoke-linux-x64: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: release-assets + path: release + - run: bash .github/scripts/smoke-test-binary.sh release/ct-linux-x64 + + release: + needs: [smoke-darwin-arm64, smoke-darwin-x64, smoke-linux-x64] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 with: - generate_release_notes: true - files: | - release/ct-cli-${{ github.ref_name }}.tgz - release/INSTALL.md + # semantic-release's commit-analyzer needs the full commit history since + # the last tag to compute the next version — a shallow checkout breaks it. + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - uses: actions/download-artifact@v4 + with: + name: release-assets + path: release + + - run: chmod +x release/ct-darwin-arm64 release/ct-darwin-x64 release/ct-linux-x64 + + # Pinned via npx rather than added as devDependencies (see #9) — this repo + # doesn't otherwise need semantic-release, so keep it out of package.json. + - name: Run semantic-release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + npx --yes \ + -p semantic-release@24 \ + -p @semantic-release/commit-analyzer@13 \ + -p @semantic-release/release-notes-generator@14 \ + -p @semantic-release/github@11 \ + semantic-release diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 0000000..f19f1de --- /dev/null +++ b/.releaserc.json @@ -0,0 +1,22 @@ +{ + "branches": ["main"], + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + [ + "@semantic-release/github", + { + "successCommentCondition": false, + "failCommentCondition": false, + "releasedLabels": false, + "assets": [ + { "path": "release/ct-cli.tgz", "label": "ct-cli.tgz — npm-installable tarball" }, + { "path": "release/ct-darwin-arm64", "label": "ct-darwin-arm64 — macOS (Apple Silicon)" }, + { "path": "release/ct-darwin-x64", "label": "ct-darwin-x64 — macOS (Intel)" }, + { "path": "release/ct-linux-x64", "label": "ct-linux-x64 — Linux x64" }, + { "path": "release/INSTALL.md", "label": "INSTALL.md" } + ] + } + ] + ] +} diff --git a/README.md b/README.md index bc789af..6f20e7d 100644 --- a/README.md +++ b/README.md @@ -44,24 +44,44 @@ Early scaffold. See the [epic (#1)](https://github.com/eqrm/ct-cli/issues/1) and ## Requirements -- Node ≥ 20 (repo pins 22 via `.nvmrc`) +- Node ≥ 20 (repo pins 22 via `.nvmrc`) — only for the npm tarball or dev install; + the standalone binaries below need nothing but the OS - A ChurchTools **personal login token** (ChurchTools → your user settings) ## Install -Grab the latest tarball from the [Releases page](https://github.com/eqrm/ct-cli/releases/latest) -and install it globally with npm — no clone, no build step: +Grab the standalone binary from the [Releases page](https://github.com/eqrm/ct-cli/releases/latest) — +**no Node required**: ```bash -npm install -g https://github.com/eqrm/ct-cli/releases/latest/download/ct-cli-.tgz +# macOS, Apple Silicon +curl -L -o ct https://github.com/eqrm/ct-cli/releases/latest/download/ct-darwin-arm64 +# macOS, Intel +curl -L -o ct https://github.com/eqrm/ct-cli/releases/latest/download/ct-darwin-x64 +# Linux, x64 +curl -L -o ct https://github.com/eqrm/ct-cli/releases/latest/download/ct-linux-x64 + +chmod +x ct +sudo mv ct /usr/local/bin/ct # or anywhere on your PATH ct --help ``` -(Replace `` with the tag of the release you're installing, e.g. `v0.1.0`.) -Each release also attaches an `INSTALL.md` with the exact command for that tag. +Or, with Node ≥ 20 already installed, the npm-pack tarball: -Every push of a `v*` tag runs lint/typecheck/test/build and publishes the -resulting package as a GitHub Release ([`.github/workflows/release.yml`](.github/workflows/release.yml)). +```bash +npm install -g https://github.com/eqrm/ct-cli/releases/latest/download/ct-cli.tgz +ct --help +``` + +Each release also attaches an `INSTALL.md` with the exact commands. + +Every push to `main` that lands a `feat:`/`fix:`/breaking-change commit runs the +full CI gate, compiles the binaries above, smoke-tests each one on its native +OS/arch, and — only if all of that is green — cuts the version + changelog via +[semantic-release](https://semantic-release.gitbook.io/) and publishes the GitHub +Release. No manual tag push. See +[`.github/workflows/release.yml`](.github/workflows/release.yml) and +[`.releaserc.json`](.releaserc.json). ## Install (dev)