diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md index 3f6a395..476796e 100644 --- a/.claude/skills/release/SKILL.md +++ b/.claude/skills/release/SKILL.md @@ -1,74 +1,64 @@ --- name: release -description: Cut a txcript release — preflight the workspace, bump versions, tag, watch the publish-crates and publish-npm workflows, and verify on crates.io and npm. Use when asked to release, publish, or ship a new version of txcript. +description: Cut a txcript release — dispatch the prepare-release workflow, approve the plan, watch the tag publish to crates.io, npm, and GitHub Releases, and verify. Use when asked to release, publish, or ship a new version of txcript. argument-hint: [version] --- # Release txcript -Publishes the `txcript` library to crates.io and the WASM package to npm via -the tag-triggered `publish-crates` and `publish-npm` workflows. Three releases -(v0.1.0–v0.3.0) established this procedure; follow it in order. A crates.io -version is **permanent** — it can be yanked but never deleted or reused — so -every gate runs before the tag exists. +Releases are cut by two workflows. `prepare-release` computes the version +from the conventional-commit PR titles merged since the last tag, generates +the `CHANGELOG.md` section, bumps every manifest, and lands one commit plus +its tag on main. The tag runs `release`, which publishes crates.io, npm, and +CLI binaries, then creates the GitHub Release from the changelog section. + +Nothing runs locally. Main is releasable by construction: `ci` proves the +crate packages, the npm bundle builds, and the CLI builds in release mode on +every PR, and the ruleset requires PRs to be up to date before merge. ## Inputs -`$ARGUMENTS`: optional target version. If absent, propose one from the diff -since the last tag: breaking API change on 0.x → minor bump, otherwise patch. -Confirm the version with the user before tagging — this is the one -irreversible decision. +`$ARGUMENTS`: optional version. Without one, the workflow derives it: a +`feat` PR since the last tag is a minor bump, otherwise patch, and a breaking +change stays a minor bump until 1.0. Preview locally with +`git-cliff --bumped-version` and `git-cliff --unreleased --tag vX.Y.Z --strip all`. -## Preflight (before any version edit) +## Procedure -1. Working tree clean, on `main`, up to date with origin. Uncommitted files - fail `cargo publish --locked`. -2. `cargo test --workspace` — bare `cargo test` skips the CLI member. -3. `cargo clippy --workspace --all-targets` — pedantic baseline, - `unwrap/expect/panic` denied in `src/`. -4. `cargo fmt --all --check` — CI gates on this and the other preflight - commands do not, so unformatted code passes every check here and fails the - run you're waiting on before tagging (cost a round trip at v0.5.0). -5. `cargo test --no-default-features` — the featureless build is a supported - surface and has broken independently of the default build before. -6. `cargo publish --dry-run --locked -p txcript` — catches packaging errors - (missing metadata, dirty files) that the workflow would only surface after - the tag is pushed. +1. Confirm main's latest CI run is green: `gh run list --branch main -L 3`. +2. Dispatch: `gh workflow run prepare-release.yml` (add `-f version=X.Y.Z` + to override). Then `gh run watch` the run. +3. The Plan job writes the version and changelog section to the run summary + and the Tag job waits on the `release` environment. Read the summary, + then approve in the Actions UI. This is the one irreversible decision: a + crates.io version can be yanked but never reused. +4. The Tag job pushes `chore(release): vX.Y.Z` and the tag atomically. If + main moved mid-run it rebuilds the commit on the new head; if the derived + version changed because of what landed, it fails and asks for a rerun. +5. The tag starts `release`. Watch it: `gh run list --workflow release.yml -L 1` + then `gh run watch `. Jobs: verify, crates.io, npm, binaries for five + targets, GitHub Release. -## Bump and tag +## Verify -1. Set the new version in **all four** places: `Cargo.toml` (root `txcript`), - `cli/Cargo.toml` (`txcript-cli` — `publish = false`, but its version tracks - the library), the `txcript = { version = … }` pin inside `cli/Cargo.toml`, - and `package.json` (the npm/WASM package). The pin only *has* to move on a - minor bump — `^0.4.0` admits 0.4.3 but not 0.5.0 — which is exactly when - it's easiest to forget. `package.json` had drifted three releases behind by - v0.5.0; the npm workflow guards tag-vs-`package.json`, so drift there is a - dispatch-time failure, not a tag-time one. `cargo check` once so - `Cargo.lock` picks up the bump; commit the lockfile with the manifests. -2. Commit the bump, push, and confirm CI is green on that commit before - tagging. -3. Annotated tag matching the manifest exactly: - `git tag -a v -m "v" && git push origin v`. - The workflow's first step compares `${GITHUB_REF_NAME#v}` against the - manifest and hard-fails on mismatch. +- crates.io: `cargo search txcript` or `https://crates.io/api/v1/crates/txcript`. +- npm: `npm view txcript version`. +- Release: `gh release view vX.Y.Z` shows the changelog notes, the package + links, and ten assets (five archives, five checksums). +- `cargo binstall --git https://github.com/skillsynchq/txcript txcript-cli` + installs the new binary. -## Watch and verify +## When something fails -1. The tag push fires **two** workflows: `publish-crates` (cargo, with - `cargo publish --locked -p txcript` — only the library ships) and - `publish-npm` (builds the WASM bundle and publishes via OIDC trusted - publishing — no token, npm trusts this repo + workflow filename as of - v0.5.0). Watch both runs (`gh run watch` in the background). -2. Verify crates.io: `cargo search txcript` or fetch - `https://crates.io/api/v1/crates/txcript` and check `max_version`. -3. Verify npm: `npm view txcript version`. If the npm run failed on its - version guard, `package.json` missed the bump (step 1 of Bump and tag); - fix the manifest, then re-dispatch on the tag is not possible — the tag - must carry the right `package.json`, so a failed guard means cutting a - patch release with the manifest fixed. +- `verify` fails on a manifest mismatch: the tag was pushed by hand without + a bump. Delete nothing; run `prepare-release` for the next patch. +- A publish job fails after the tag exists: fix forward. The crates.io + version may already be taken, so the next run needs a new version. +- `release` can be rerun on an existing tag with + `gh workflow run release.yml --ref vX.Y.Z`; the GitHub Release step fails + if the release already exists, which is the intended guard. ## Report -State the published version, both workflow run URLs, and the crates.io and -npm verification results. +State the version, the prepare and release run URLs, the release URL, and +the crates.io and npm verification results. diff --git a/.github/scripts/bump-version.sh b/.github/scripts/bump-version.sh new file mode 100755 index 0000000..1b71ff3 --- /dev/null +++ b/.github/scripts/bump-version.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env sh +# Set the release version everywhere it lives, then refresh the lockfile. +# Usage: bump-version.sh 0.13.0 +# Four locations: the library manifest, the CLI manifest, the CLI's pin on +# the library, and package.json. All must agree or the release guards fail. +set -eu +ver="$1" +case "$ver" in + [0-9]*.[0-9]*.[0-9]*) ;; + *) echo "::error::not a version: $ver" >&2; exit 1 ;; +esac +sed -i.bak -E "s/^version = \"[^\"]+\"/version = \"$ver\"/" Cargo.toml cli/Cargo.toml +sed -i.bak -E "s/^(txcript = \{ version = )\"[^\"]+\"/\1\"$ver\"/" cli/Cargo.toml +sed -i.bak -E "s/^( \"version\": )\"[^\"]+\"/\1\"$ver\"/" package.json +rm -f Cargo.toml.bak cli/Cargo.toml.bak package.json.bak +cargo update --workspace --quiet +for f in Cargo.toml cli/Cargo.toml package.json; do + grep -q "\"$ver\"" "$f" || { echo "::error::$f did not take version $ver" >&2; exit 1; } +done +[ "$(grep -c "\"$ver\"" cli/Cargo.toml)" = 2 ] || { echo "::error::cli/Cargo.toml needs both its version and the txcript pin at $ver" >&2; exit 1; } +echo "version $ver set in Cargo.toml, cli/Cargo.toml, package.json, Cargo.lock" diff --git a/.github/scripts/changelog-insert.sh b/.github/scripts/changelog-insert.sh new file mode 100755 index 0000000..44eb866 --- /dev/null +++ b/.github/scripts/changelog-insert.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env sh +# Insert a generated release section into CHANGELOG.md, directly above the +# newest existing section. Usage: changelog-insert.sh section.md +set -eu +section="$1" +awk -v f="$section" ' + !done && /^## \[/ { while ((getline line < f) > 0) print line; print ""; done = 1 } + { print } + END { if (!done) { while ((getline line < f) > 0) print line } } +' CHANGELOG.md > CHANGELOG.md.new +mv CHANGELOG.md.new CHANGELOG.md diff --git a/.github/scripts/release-notes.sh b/.github/scripts/release-notes.sh new file mode 100755 index 0000000..5500d51 --- /dev/null +++ b/.github/scripts/release-notes.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env sh +# Print the CHANGELOG.md section for one version, followed by links to the +# published packages. Usage: release-notes.sh 0.12.1 > notes.md +# Exits 1 when the version has no section, so a tag without changelog +# coverage fails the release job instead of shipping empty notes. +set -eu +ver="$1" +section="$(awk -v ver="$ver" ' + /^## \[/ { inside = ($0 ~ "^## \\[" ver "\\]") ; next } + inside && /^\[/ { next } + inside { print } +' CHANGELOG.md)" +# Trim leading/trailing blank lines. +section="$(printf '%s\n' "$section" | sed -e '/./,$!d' | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}')" +if [ -z "$section" ]; then + echo "::error::CHANGELOG.md has no section for $ver" >&2 + exit 1 +fi +printf '%s\n\n' "$section" +cat < "$name.tar.gz.sha256" + ./dist/txcript --version + - name: Package (windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $name = "txcript-${{ matrix.target }}" + New-Item -ItemType Directory -Force dist | Out-Null + Copy-Item "target/${{ matrix.target }}/release/txcript.exe", LICENSE dist/ + Compress-Archive -Path dist/txcript.exe, dist/LICENSE -DestinationPath "$name.zip" + (Get-FileHash "$name.zip" -Algorithm SHA256).Hash.ToLower() + " $name.zip" | Out-File -Encoding ascii "$name.zip.sha256" + & ./dist/txcript.exe --version + - uses: actions/upload-artifact@v4 + with: + name: txcript-${{ matrix.target }} + path: | + txcript-${{ matrix.target }}.tar.gz + txcript-${{ matrix.target }}.tar.gz.sha256 + txcript-${{ matrix.target }}.zip + txcript-${{ matrix.target }}.zip.sha256 + if-no-files-found: error diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88b035f..5c59057 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,8 @@ name: ci # The quality gate: every push and PR runs the same checks the publish tags # assume. Formatting, the full clippy deny set, tests across all features, -# the wasm32 build, and the declared MSRV. +# the wasm32 build, the declared MSRV, and the packaging steps the release +# performs: crate dry-run, npm bundle, and a release build of the CLI. on: push: branches: [main] @@ -69,3 +70,49 @@ jobs: with: cache-on-failure: true - run: cargo check --workspace --all-features + + # Publish readiness. Every merged head must be releasable as-is, so the + # packaging steps the release runs are proven here, on the PR. + crate-package: + name: crates.io package (dry-run) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + - run: cargo publish --dry-run --locked -p txcript + + npm-package: + name: npm package (dry-run) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + - uses: taiki-e/install-action@v2 + with: + tool: wasm-bindgen-cli@0.2.126 + - uses: oven-sh/setup-bun@v2 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - run: bun run build + - run: npm pack --dry-run --ignore-scripts + + cli-release-build: + name: CLI release build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + - run: cargo build --release --locked -p txcript-cli + - run: ./target/release/txcript --version diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 0000000..336a084 --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,34 @@ +name: pr-title + +# Every PR title must be a conventional commit. Squash merges make the title +# the commit on main, and the changelog and version bump are computed from +# those commits, so an off-format title means a missing changelog line or a +# wrong version. +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +permissions: + pull-requests: read + +jobs: + check: + name: PR title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + feat + fix + perf + refactor + revert + docs + test + build + ci + chore + style diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 0000000..a99933d --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,131 @@ +name: prepare-release + +# Cuts a release. Computes the next version from the conventional commits +# since the last tag (or takes one as input), generates the changelog +# section, bumps every manifest, and pushes the release commit and its tag +# to main in one atomic push. The tag then runs release.yml. +# +# Main is releasable by construction (ci.yml proves packaging on every PR), +# so this workflow only proves the bump it adds. If main moves while it +# runs, the push is rejected and the release commit is rebuilt on the new +# head; nothing partial ever lands. +# +# Pushes as the release GitHub App, which bypasses the main and tag +# rulesets and whose pushes trigger workflows (GITHUB_TOKEN's do not). +on: + workflow_dispatch: + inputs: + version: + description: "Version to release (X.Y.Z). Leave empty to derive it from the commits since the last tag." + required: false + type: string + +permissions: + contents: read + +# One release at a time. +concurrency: + group: release + cancel-in-progress: false + +jobs: + plan: + name: Plan + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - uses: taiki-e/install-action@v2 + with: + tool: git-cliff@2.14.1 + - name: Resolve version + id: version + run: | + set -eu + input='${{ inputs.version }}' + if [ -n "$input" ]; then + ver="${input#v}" + else + ver="$(git-cliff --bumped-version 2>/dev/null)" + ver="${ver#v}" + fi + echo "$ver" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' || { echo "::error::not a version: $ver"; exit 1; } + current="$(grep -m1 '^version = ' Cargo.toml | sed -E 's/version = "(.*)"/\1/')" + [ "$ver" != "$current" ] || { echo "::error::$ver is already the current version"; exit 1; } + [ "$(printf '%s\n%s\n' "$current" "$ver" | sort -V | tail -1)" = "$ver" ] || { echo "::error::$ver is below current $current"; exit 1; } + ! git rev-parse -q --verify "refs/tags/v$ver" >/dev/null || { echo "::error::tag v$ver already exists"; exit 1; } + echo "version=$ver" >> "$GITHUB_OUTPUT" + - name: Show the plan + run: | + { + echo "## Release v${{ steps.version.outputs.version }}" + echo + echo "Head: \`$(git rev-parse --short HEAD)\` (${{ github.ref_name }})" + echo + git-cliff --unreleased --tag "v${{ steps.version.outputs.version }}" --strip all 2>/dev/null + } >> "$GITHUB_STEP_SUMMARY" + + tag: + name: Tag main + needs: plan + runs-on: ubuntu-latest + # Required reviewer on the environment is the approval step: the plan + # above is in the run summary when the button appears. + environment: release + steps: + - uses: actions/create-github-app-token@v2 + id: app + with: + app-id: ${{ vars.RELEASE_APP_ID }} + private-key: ${{ secrets.RELEASE_APP_KEY }} + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + token: ${{ steps.app.outputs.token }} + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - uses: taiki-e/install-action@v2 + with: + tool: git-cliff@2.14.1 + - name: Commit as the release App + run: | + id="$(gh api "/users/${{ steps.app.outputs.app-slug }}[bot]" --jq .id)" + git config user.name '${{ steps.app.outputs.app-slug }}[bot]' + git config user.email "${id}+${{ steps.app.outputs.app-slug }}[bot]@users.noreply.github.com" + env: + GH_TOKEN: ${{ steps.app.outputs.token }} + - name: Build the release commit and push it with its tag + env: + VERSION: ${{ needs.plan.outputs.version }} + run: | + set -eu + tag="v$VERSION" + for attempt in 1 2 3 4 5; do + git fetch origin main + git reset -q --hard origin/main + git tag -d "$tag" 2>/dev/null || true + # Without an explicit version, the bump must still be what was + # approved; a feat landing mid-run would change it. + if [ -z '${{ inputs.version }}' ]; then + now="$(git-cliff --bumped-version 2>/dev/null)" + [ "${now#v}" = "$VERSION" ] || { echo "::error::main moved and the derived version is now ${now#v}, not $VERSION; rerun"; exit 1; } + fi + git-cliff --unreleased --tag "$tag" --strip all 2>/dev/null > section.md + .github/scripts/changelog-insert.sh section.md + rm section.md + .github/scripts/bump-version.sh "$VERSION" + cargo publish --dry-run --locked -p txcript + git add Cargo.toml Cargo.lock cli/Cargo.toml package.json CHANGELOG.md + git commit -q -m "chore(release): $tag" + git tag -a "$tag" -m "$tag" + if git push --atomic origin HEAD:main "refs/tags/$tag"; then + echo "pushed $tag on $(git rev-parse --short HEAD) (attempt $attempt)" + exit 0 + fi + echo "main moved during attempt $attempt; rebuilding on the new head" + done + echo "::error::could not land the release commit after 5 attempts" + exit 1 diff --git a/.github/workflows/publish-crates.yml b/.github/workflows/publish-crates.yml deleted file mode 100644 index 302b7a3..0000000 --- a/.github/workflows/publish-crates.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: publish-crates - -# Publishes the Rust crate to crates.io on a version tag, e.g. -# `git tag v0.2.0 && git push origin v0.2.0`. -# Guards that the tag matches the manifest version before publishing. -on: - push: - tags: - - "v*" - -permissions: - contents: read - -jobs: - crates-io: - name: Publish to crates.io - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - name: Tag matches Cargo.toml version - run: | - tag="${GITHUB_REF_NAME#v}" - ver="$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "txcript") | .version')" - echo "tag=$tag manifest=$ver" - [ "$tag" = "$ver" ] || { echo "::error::tag $tag != Cargo.toml version $ver"; exit 1; } - # Only the library goes to crates.io; the cli crate is publish = false. - - run: cargo publish --locked -p txcript - env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml deleted file mode 100644 index 129bec6..0000000 --- a/.github/workflows/publish-npm.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: publish-npm - -# Publishes the WASM package to npm via trusted publishing (OIDC) — -# the txcript package on npm trusts this repo + workflow filename, so -# no registry token is involved. Runs on version tags, with manual -# dispatch (on a tag ref) as the fallback. Guards that the tag matches -# package.json before publishing. -on: - push: - tags: ["v*"] - workflow_dispatch: - -permissions: - contents: read - id-token: write - -jobs: - npm: - name: Publish to npm - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-unknown-unknown - - uses: Swatinem/rust-cache@v2 - - uses: taiki-e/install-action@v2 - with: - tool: wasm-bindgen-cli@0.2.126 - # The `build`/`prepare` scripts run through Bun; npm still invokes the - # `prepare` lifecycle during publish, so Bun must be on PATH. - - uses: oven-sh/setup-bun@v2 - - uses: actions/setup-node@v4 - with: - node-version: 24 - registry-url: https://registry.npmjs.org - # Trusted publishing needs npm >= 11.5.1; Node 24 can bundle older. - - run: npm install -g npm@latest - - name: Tag matches package.json version - run: | - tag="${GITHUB_REF_NAME#v}" - ver="$(node -p "require('./package.json').version")" - echo "tag=$tag manifest=$ver" - [ "$tag" = "$ver" ] || { echo "::error::tag $tag != package.json version $ver"; exit 1; } - # Build the wasm artifact into pkg/, then publish via OIDC. - - run: bun run build - - run: npm publish --access public --ignore-scripts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4ec41bc --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,122 @@ +name: release + +# Publishes a version tag: the library to crates.io, the WASM package to npm, +# CLI binaries for every target, and the GitHub Release with notes from the +# CHANGELOG.md section. The release is created only after every publish +# succeeded, so a release page never points at a package that isn't there. +# +# Tags come from prepare-release.yml. A hand-pushed `vX.Y.Z` tag works too, +# as long as the manifests and changelog already carry that version. +# +# npm trusted publishing is bound to this filename. crates.io uses the +# CARGO_REGISTRY_TOKEN secret. +on: + push: + tags: + - "v*" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: release-${{ github.ref_name }} + cancel-in-progress: false + +jobs: + verify: + name: Verify tag + runs-on: ubuntu-latest + outputs: + version: ${{ steps.v.outputs.version }} + steps: + - uses: actions/checkout@v5 + - name: Tag matches every manifest + id: v + run: | + set -eu + case "$GITHUB_REF" in refs/tags/v*) ;; *) echo "::error::run this on a version tag, got $GITHUB_REF"; exit 1 ;; esac + tag="${GITHUB_REF_NAME#v}" + cargo="$(grep -m1 '^version = ' Cargo.toml | sed -E 's/version = "(.*)"/\1/')" + cli="$(grep -m1 '^version = ' cli/Cargo.toml | sed -E 's/version = "(.*)"/\1/')" + npm="$(node -p "require('./package.json').version")" + echo "tag=$tag Cargo.toml=$cargo cli/Cargo.toml=$cli package.json=$npm" + for v in "$cargo" "$cli" "$npm"; do + [ "$tag" = "$v" ] || { echo "::error::tag $tag does not match every manifest"; exit 1; } + done + echo "version=$tag" >> "$GITHUB_OUTPUT" + - name: Changelog has this version + run: .github/scripts/release-notes.sh "${GITHUB_REF_NAME#v}" > notes.md && cat notes.md + - uses: actions/upload-artifact@v4 + with: + name: release-notes + path: notes.md + + crates-io: + name: Publish to crates.io + needs: verify + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + # Only the library goes to crates.io; the cli crate is publish = false. + - run: cargo publish --locked -p txcript + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + + npm: + name: Publish to npm + needs: verify + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + - uses: taiki-e/install-action@v2 + with: + tool: wasm-bindgen-cli@0.2.126 + # The build script runs through Bun; npm still invokes the prepare + # lifecycle during publish, so Bun must be on PATH. + - uses: oven-sh/setup-bun@v2 + - uses: actions/setup-node@v4 + with: + node-version: 24 + registry-url: https://registry.npmjs.org + # Trusted publishing needs npm >= 11.5.1; Node 24 can bundle older. + - run: npm install -g npm@latest + - run: bun run build + - run: npm publish --access public --ignore-scripts + + binaries: + name: Build CLI binaries + needs: verify + uses: ./.github/workflows/build-binaries.yml + + github-release: + name: Create GitHub Release + needs: [verify, crates-io, npm, binaries] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + path: artifacts + - name: Create release with notes and binaries + env: + GH_TOKEN: ${{ github.token }} + run: | + set -eu + ls -R artifacts + gh release create "$GITHUB_REF_NAME" \ + --repo "$GITHUB_REPOSITORY" \ + --verify-tag \ + --title "$GITHUB_REF_NAME" \ + --notes-file artifacts/release-notes/notes.md \ + artifacts/txcript-*/txcript-* diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5dda52d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,191 @@ +# Changelog + +All notable changes to txcript are recorded here. Entries are generated from +the pull requests merged between releases, following +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +Each release is published to [crates.io](https://crates.io/crates/txcript), +[npm](https://www.npmjs.com/package/txcript), and +[GitHub Releases](https://github.com/skillsynchq/txcript/releases). + +## [0.12.1](https://github.com/skillsynchq/txcript/compare/v0.12.0...v0.12.1) - 2026-09-01 + +### Changed + +- Search patterns match literally instead of fuzzily. +- Discovery is cheaper, and the picker's echo stays off the search path. +- Cursor Desktop discovery and loads scale with the session instead of the + whole database. + +## [0.12.0](https://github.com/skillsynchq/txcript/compare/v0.11.0...v0.12.0) - 2026-08-25 + +### Added + +- ChatGPT as a live, pull-only harness: list and continue ChatGPT + conversations in a local harness. + +## [0.11.0](https://github.com/skillsynchq/txcript/compare/v0.10.0...v0.11.0) - 2026-08-24 + +### Added + +- fx (Vercel's coding agent) harness. +- `txcript view` draws images inline on kitty-graphics terminals and ships a + built-in pager with controls over what it shows. +- Claude Chat sessions load directly by UUID. + +### Changed + +- `view` output is tuned for terminals. + +### Fixed + +- Terminal query helpers are gated to unix. + +## [0.10.0](https://github.com/skillsynchq/txcript/compare/v0.9.1...v0.10.0) - 2026-08-21 + +### Added + +- Claude Chat as a live, pull-only harness, off by default and reachable only + through an explicit `--from`. +- `txcript export` for moving sessions between machines. +- `txcript init` installs the ctrl+shift+r session picker into your shell. + +### Changed + +- Launched harnesses receive the real tty. + +### Fixed + +- The wasm bundle is built from the txcript package. + +## [0.9.1](https://github.com/skillsynchq/txcript/compare/v0.9.0...v0.9.1) - 2026-08-20 + +### Added + +- The CLI is also a library (`txcript-cli`), exposing its commands as clap + types and a `run_session` entry point. +- A persistent search cache for `query`. + +## [0.9.0](https://github.com/skillsynchq/txcript/compare/v0.8.1...v0.9.0) - 2026-08-20 + +### Added + +- Cowork, Claude desktop's local agent mode, as a harness. + +## [0.8.1](https://github.com/skillsynchq/txcript/compare/v0.8.0...v0.8.1) - 2026-08-19 + +### Fixed + +- Foreign `tool_result` blocks are flattened when writing claude_code. + +### Changed + +- README translations moved under `docs/translations/`. + +## [0.8.0](https://github.com/skillsynchq/txcript/compare/v0.7.0...v0.8.0) - 2026-08-19 + +### Added + +- Simple, an interchange pseudo-harness for agents without a native store. +- `continue` accepts a Simple document from a file or stdin. + +## [0.7.0](https://github.com/skillsynchq/txcript/compare/v0.6.0...v0.7.0) - 2026-08-18 + +### Added + +- README translations in twelve languages. + +### Changed + +- The harness list is a capability matrix. +- opencode import satisfies the stricter session and message schema. +- CLI quality gaps found against replay-cli are closed. + +## [0.6.0](https://github.com/skillsynchq/txcript/compare/v0.5.0...v0.6.0) - 2026-08-17 + +### Added + +- Cursor desktop harness for the IDE app's `state.vscdb` sessions. +- Documentation of every harness's on-disk transcript format. + +### Changed + +- Stores and the CLI are hardened against hostile session files. +- The Claude Code summary line is anchored to a real leaf. +- npm publishing uses trusted publishing and is triggered by version tags + again. + +## [0.5.0](https://github.com/skillsynchq/txcript/compare/v0.4.3...v0.5.0) - 2026-08-08 + +### Added + +- Claude Code local commands are modelled as `Tool::Command`. +- Experimental `--move` for `txcript continue`. + +## [0.4.3](https://github.com/skillsynchq/txcript/compare/v0.4.2...v0.4.3) - 2026-08-03 + +### Added + +- `Session::updated_at`. + +## [0.4.2](https://github.com/skillsynchq/txcript/compare/v0.4.1...v0.4.2) - 2026-07-30 + +### Fixed + +- Harness session stores resolve correctly on Windows. + +## [0.4.1](https://github.com/skillsynchq/txcript/compare/v0.4.0...v0.4.1) - 2026-07-20 + +### Added + +- `txcript view`, and `#range` refs for `view` and `continue`. + +### Fixed + +- Exported codex rollouts name a real `model_provider`. +- Non-`ses` session ids are re-shaped for opencode export. + +## [0.4.0](https://github.com/skillsynchq/txcript/compare/v0.3.0...v0.4.0) - 2026-07-17 + +### Added + +- amp and antigravity harnesses. +- An MCP server with a token-conscious text projection. +- `Span` for pointing into sessions with zero-copy fragment resolution. +- `completion` subcommand; harness names are advertised to completers. +- `--cwd` scopes `list` and `query` to a folder. + +### Changed + +- `query` indexes in parallel and interactive navigation is responsive. + +## [0.3.0](https://github.com/skillsynchq/txcript/compare/v0.2.0...v0.3.0) - 2026-07-06 + +### Added + +- Grok CLI harness. +- `txcript::search`: fuzzy and substring search with a hot index. +- `txcript::local` and the `query` command with an fzf-style picker. +- `Store::delete` on every harness store. + +### Changed + +- The CLI is a separate workspace crate on clap. +- Literal occurrences rank above every gapped fuzzy alignment. +- MSRV tracks the latest stable Rust. + +## [0.2.0](https://github.com/skillsynchq/txcript/compare/v0.1.0...v0.2.0) - 2026-07-01 + +### Changed + +- The public API is hierarchical. + +## [0.1.0](https://github.com/skillsynchq/txcript/releases/tag/v0.1.0) - 2026-07-01 + +### Added + +- Canonical session model and core traits. +- claude_code, codex, pi, campfire, opencode, and Cursor harnesses. +- `txcript` CLI with `list` and cross-harness `continue`. +- Composable `TextCodec` layer and WASM bindings. diff --git a/Cargo.toml b/Cargo.toml index ac22b00..8d7b66e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ edition = "2024" rust-version = "1.96" description = "Convert coding-agent session transcripts between harness formats." license = "Apache-2.0" +homepage = "https://github.com/skillsynchq/txcript" repository = "https://github.com/skillsynchq/txcript" readme = "README.md" keywords = ["transcript", "claude", "codex", "agent", "wasm"] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index e2afc2e..c13c014 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -62,3 +62,14 @@ tempfile = "3.27.0" [lints] workspace = true + +# Prebuilt binaries are attached to every GitHub Release, one archive per +# target with the executable at the archive root. Install without compiling: +# cargo binstall --git https://github.com/skillsynchq/txcript txcript-cli +[package.metadata.binstall] +pkg-url = "{ repo }/releases/download/v{ version }/txcript-{ target }{ archive-suffix }" +bin-dir = "{ bin }{ binary-ext }" +pkg-fmt = "tgz" + +[package.metadata.binstall.overrides.x86_64-pc-windows-msvc] +pkg-fmt = "zip" diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..c10054a --- /dev/null +++ b/cliff.toml @@ -0,0 +1,53 @@ +# git-cliff configuration: one CHANGELOG.md section per release, generated +# from the conventional-commit PR titles merged since the previous tag. +# The prepare-release workflow runs it; run `git-cliff --unreleased --tag +# vX.Y.Z --strip all` locally to preview the next section. + +[changelog] +trim = true +# Rendered once per release. The heading carries the compare link so the +# file stays self-contained without a reference list at the bottom. +body = """ +## [{{ version | trim_start_matches(pat="v") }}]\ +{% if previous.version %}(https://github.com/skillsynchq/txcript/compare/{{ previous.version }}...{{ version }}){% endif %} \ +- {{ timestamp | date(format="%Y-%m-%d") }} +{% for group, commits in commits | group_by(attribute="group") %} +### {{ group | striptags | trim }} +{% for commit in commits %} +- {% if commit.breaking %}**Breaking:** {% endif %}{{ commit.message | split(pat="\n") | first | upper_first }}\ +{% endfor %} +{% endfor %} +""" +# Squash merges leave "(#N)" on the subject; turn it into a PR link. +postprocessors = [ + { pattern = '\(#([0-9]+)\)', replace = "([#${1}](https://github.com/skillsynchq/txcript/pull/${1}))" }, +] + +[git] +conventional_commits = true +# A subject that escapes the PR-title check still lands under Changed +# instead of vanishing from the changelog. +filter_unconventional = false +split_commits = false +# The hidden numeric prefix fixes the section order; `striptags` removes it. +commit_parsers = [ + { message = "^chore\\(release\\)", skip = true }, + { message = "^feat", group = "Added" }, + { message = "^fix", group = "Fixed" }, + { message = "^perf", group = "Changed" }, + { message = "^refactor", group = "Changed" }, + { message = "^revert", group = "Removed" }, + { message = "^(chore|ci|docs|test|build|style)", skip = true }, + { message = ".*", group = "Changed" }, +] +filter_commits = false +tag_pattern = "v[0-9].*" +topo_order = false +sort_commits = "oldest" + +[bump] +# Pre-1.0 policy, matching the release skill: a feature is a minor bump, a +# breaking change is a minor bump too until 1.0. +features_always_bump_minor = true +breaking_always_bump_major = false +initial_tag = "v0.1.0" diff --git a/package.json b/package.json index 9399085..424067d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,18 @@ "name": "txcript", "version": "0.12.1", "description": "Convert coding-agent session transcripts between harness formats.", + "keywords": [ + "transcript", + "claude", + "codex", + "agent", + "wasm" + ], "license": "Apache-2.0", + "homepage": "https://github.com/skillsynchq/txcript#readme", + "bugs": { + "url": "https://github.com/skillsynchq/txcript/issues" + }, "repository": { "type": "git", "url": "git+https://github.com/skillsynchq/txcript.git"