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
52 changes: 52 additions & 0 deletions .github/scripts/smoke-test-binary.sh
Original file line number Diff line number Diff line change
@@ -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."
154 changes: 127 additions & 27 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 <<EOF
# Installing ct ${GITHUB_REF_NAME}
cat > 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
22 changes: 22 additions & 0 deletions .releaserc.json
Original file line number Diff line number Diff line change
@@ -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" }
]
}
]
]
}
36 changes: 28 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<version>.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 `<version>` 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)

Expand Down
Loading