diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c6aa1d..1892a49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,18 +1,34 @@ name: CI on: + pull_request: push: branches: [main] - pull_request: -permissions: - contents: read +# Concurrency is intentionally job-level, not workflow-level. A workflow- +# level cancellable group would also cancel the `release` job mid-flight if +# a second push to `main` arrived — semantic-release would have created the +# tag and GitHub Release, but binary assets and the Homebrew bump would +# never run, leaving a half-published release. Job-level groups let verify +# stay cancellable while release queues. jobs: - check: + verify: + # Skip the bot-authored bump commit's `[skip ci]` push. PR runs always go + # through (no `head_commit` on pull_request events; gating on event_name + # avoids the null-deref that would otherwise prevent the job scheduling). + if: ${{ github.event_name != 'push' || !contains(github.event.head_commit.message, '[skip ci]') }} + name: Verify runs-on: macos-latest + permissions: + contents: read + concurrency: + group: verify-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Install Rust toolchain (from rust-toolchain.toml) run: rustup show @@ -37,3 +53,105 @@ jobs: - name: Build release run: cargo build --release + + # Push-to-main release. semantic-release decides the next version from + # Conventional Commits since the last `v*` tag, runs scripts/release- + # prepare.sh to bump Cargo.toml + Cargo.lock, commits the bump back to + # main with [skip ci], creates the GitHub Release, then we build dual-arch + # macOS tarballs and bump the Homebrew formula on uinaf/homebrew-tap. + # + # Mirrors the shape used by uinaf/react-json-logic. + release: + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, '[skip ci]') }} + name: Release + needs: [verify] + runs-on: macos-latest + permissions: + contents: write + issues: write + pull-requests: write + concurrency: + group: release-${{ github.repository }}-main + cancel-in-progress: false + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: true + + - name: Install Rust toolchain (with darwin cross-compile targets) + uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-apple-darwin,x86_64-apple-darwin + + - name: Setup Node (for semantic-release plugins) + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Run semantic-release + id: release + uses: cycjimmy/semantic-release-action@v6 + with: + extra_plugins: | + @semantic-release/commit-analyzer + @semantic-release/release-notes-generator + @semantic-release/exec + @semantic-release/git + @semantic-release/github + conventional-changelog-conventionalcommits + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_AUTHOR_NAME: glitch418x + GIT_AUTHOR_EMAIL: 189487110+glitch418x@users.noreply.github.com + GIT_COMMITTER_NAME: glitch418x + GIT_COMMITTER_EMAIL: 189487110+glitch418x@users.noreply.github.com + + - name: Build dual-arch macOS binaries + if: steps.release.outputs.new_release_published == 'true' + run: | + cargo build --release --target aarch64-apple-darwin + cargo build --release --target x86_64-apple-darwin + + - name: Package release artifacts + if: steps.release.outputs.new_release_published == 'true' + run: | + VERSION="${{ steps.release.outputs.new_release_version }}" + mkdir -p dist/arm64 dist/amd64 + cp target/aarch64-apple-darwin/release/tccutil-rs dist/arm64/tccutil-rs + cp target/x86_64-apple-darwin/release/tccutil-rs dist/amd64/tccutil-rs + chmod +x dist/arm64/tccutil-rs dist/amd64/tccutil-rs + + tar -C dist/arm64 -czf "tccutil-rs_v${VERSION}_darwin-arm64.tar.gz" tccutil-rs + tar -C dist/amd64 -czf "tccutil-rs_v${VERSION}_darwin-amd64.tar.gz" tccutil-rs + + shasum -a 256 \ + "tccutil-rs_v${VERSION}_darwin-arm64.tar.gz" \ + "tccutil-rs_v${VERSION}_darwin-amd64.tar.gz" \ + > checksums.txt + + - name: Attach assets to GitHub Release + # semantic-release already created the Release with changelog notes; + # this step appends the binaries and checksums to the same Release. + if: steps.release.outputs.new_release_published == 'true' + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.release.outputs.new_release_git_tag }} + generate_release_notes: false + files: | + tccutil-rs_v*_darwin-arm64.tar.gz + tccutil-rs_v*_darwin-amd64.tar.gz + checksums.txt + + - name: Bump Homebrew formula on uinaf/homebrew-tap + # Computes the tarball sha256 from the GitHub-hosted release archive, + # rewrites Formula/tccutil-rs.rb on the tap, and opens a PR. Runs + # after the assets are attached so the action can fetch them. + if: steps.release.outputs.new_release_published == 'true' + uses: dawidd6/action-homebrew-bump-formula@v5 + with: + token: ${{ secrets.TAP_GITHUB_TOKEN }} + tap: uinaf/homebrew-tap + formula: tccutil-rs + tag: ${{ steps.release.outputs.new_release_git_tag }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 245202c..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Release - -on: - push: - tags: ["v*"] - -permissions: - contents: write - -jobs: - build: - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - targets: aarch64-apple-darwin,x86_64-apple-darwin - - - name: Build aarch64 - run: cargo build --release --target aarch64-apple-darwin - - - name: Build x86_64 - run: cargo build --release --target x86_64-apple-darwin - - - name: Package release artifacts - run: | - VERSION="${GITHUB_REF_NAME#v}" - mkdir -p dist/arm64 dist/amd64 - cp target/aarch64-apple-darwin/release/tccutil-rs dist/arm64/tccutil-rs - cp target/x86_64-apple-darwin/release/tccutil-rs dist/amd64/tccutil-rs - chmod +x dist/arm64/tccutil-rs dist/amd64/tccutil-rs - - tar -C dist/arm64 -czf "tccutil-rs_v${VERSION}_darwin-arm64.tar.gz" tccutil-rs - tar -C dist/amd64 -czf "tccutil-rs_v${VERSION}_darwin-amd64.tar.gz" tccutil-rs - - shasum -a 256 \ - "tccutil-rs_v${VERSION}_darwin-arm64.tar.gz" \ - "tccutil-rs_v${VERSION}_darwin-amd64.tar.gz" \ - > checksums.txt - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - generate_release_notes: true - files: | - tccutil-rs_v*_darwin-arm64.tar.gz - tccutil-rs_v*_darwin-amd64.tar.gz - checksums.txt diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 0000000..ad20fe6 --- /dev/null +++ b/.releaserc.json @@ -0,0 +1,32 @@ +{ + "branches": ["main"], + "tagFormat": "v${version}", + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits" + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits" + } + ], + [ + "@semantic-release/exec", + { + "prepareCmd": "scripts/release-prepare.sh ${nextRelease.version}" + } + ], + [ + "@semantic-release/git", + { + "assets": ["Cargo.toml", "Cargo.lock"], + "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" + } + ], + "@semantic-release/github" + ] +} diff --git a/AGENTS.md b/AGENTS.md index 515276c..74983f6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,3 +31,6 @@ Single binary, two source files. Reads both user (`~/Library/Application Support - `scripts/verify.sh` — Single canonical gate. CI calls it; the pre-push hook calls it; run it locally before opening a PR - `Cargo.toml` — Dependencies and package metadata - `rust-toolchain.toml` — Pinned toolchain channel +- `.releaserc.json` — semantic-release config (Conventional Commits → version + tag + GitHub Release). See [CONTRIBUTING.md → Releases](CONTRIBUTING.md#releases) +- `scripts/release-prepare.sh` — Bumps `Cargo.toml` + `Cargo.lock` during the release pipeline (invoked by `@semantic-release/exec`) +- `.github/workflows/ci.yml` — Single workflow with `verify` (PR + push) and `release` jobs (push to `main`, runs semantic-release + dual-arch macOS build + Homebrew tap bump) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 70111fd..5964c96 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,6 +49,28 @@ After install, every `git push` runs `scripts/verify.sh` and fails the push if a - Table output in `src/main.rs` does manual ANSI-aware padding. If you touch it, run `tccutil-rs list` against a real TCC.db to eyeball alignment. - Integration tests in `tests/integration.rs` exec the real binary via `CARGO_BIN_EXE_tccutil-rs`. Unit tests in `src/tcc.rs` round-trip real SQLite via `tempfile`. No mocks. +## Releases + +Push-to-main, semantic-release driven. Mirrors the [`uinaf/react-json-logic`](https://github.com/uinaf/react-json-logic) setup. + +When a `feat:` or `fix:` lands on `main`, the `release` job in [`.github/workflows/ci.yml`](.github/workflows/ci.yml) runs after `verify` passes and: + +1. **`semantic-release`** analyzes commits since the last `v*` tag and decides the next version. +2. **`scripts/release-prepare.sh`** bumps `Cargo.toml` + `Cargo.lock` to the new version (via `@semantic-release/exec`). +3. **`@semantic-release/git`** commits those files back to `main` as `chore(release): [skip ci]` (the `[skip ci]` keeps the bump from re-triggering the pipeline). +4. **`@semantic-release/github`** creates the `v` tag and the GitHub Release with the changelog as the body. +5. **macOS dual-arch build** runs in the same job, attaching tarballs + `checksums.txt` to the new Release. +6. **`dawidd6/action-homebrew-bump-formula`** opens a PR against [`uinaf/homebrew-tap`](https://github.com/uinaf/homebrew-tap) bumping `Formula/tccutil-rs.rb`. + +Bot identity is `glitch418x` (set inside the semantic-release step's `env:`). + +Required secrets on this repo: + +- `GITHUB_TOKEN` — provided automatically. Used by semantic-release for the bump-back commit, tag, and Release. +- `TAP_GITHUB_TOKEN` — fine-grained PAT for `glitch418x` with `contents: write` and `pull-requests: write` on `uinaf/homebrew-tap`. The default `GITHUB_TOKEN` only has scope on this repo. + +`chore:` / `docs:` / `refactor:` commits do not bump the version on their own — land them alongside a `feat:` or `fix:` if you want them in a release. `feat!:` / `BREAKING CHANGE:` bumps the major. + ## Pull requests - Keep changes focused — a single concern per PR. diff --git a/Cargo.toml b/Cargo.toml index 21cbf56..98088ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,12 @@ name = "tccutil-rs" version = "0.2.0" edition = "2024" description = "CLI tool for managing macOS TCC permissions" +license = "MIT" +repository = "https://github.com/uinaf/tccutil" +homepage = "https://github.com/uinaf/tccutil" +readme = "README.md" +# Binary crate, not intended for crates.io. +publish = false [[bin]] name = "tccutil-rs" diff --git a/scripts/release-prepare.sh b/scripts/release-prepare.sh new file mode 100755 index 0000000..d93a0ff --- /dev/null +++ b/scripts/release-prepare.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Bumps the version in Cargo.toml + Cargo.lock to the version semantic-release +# computed for the upcoming release. Invoked by @semantic-release/exec via +# `prepareCmd` in .releaserc.json. +# +# Runs in CI only — you should not need to run this locally. +set -euo pipefail + +if [ "$#" -ne 1 ]; then + echo "usage: $0 " >&2 + exit 2 +fi + +version="$1" + +# Bump only the [package] version line, not any dependency version specs. +# awk replaces the first matching `^version = ` line and leaves the rest of +# the file alone; this is portable across BSD awk (macOS) and GNU awk. +tmp="$(mktemp)" +awk -v v="$version" ' + /^version = / && !done { print "version = \"" v "\""; done=1; next } + { print } +' Cargo.toml > "$tmp" +mv "$tmp" Cargo.toml + +# Refresh Cargo.lock so the local-crate entry matches the new version. +# `cargo check` updates Cargo.lock when Cargo.toml's version changes. +cargo check --quiet + +echo "Bumped Cargo.toml + Cargo.lock to version $version"