Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
120 changes: 116 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
name: CI

on:
pull_request:
push:
branches: [main]
pull_request:

permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

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
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Install Rust toolchain (from rust-toolchain.toml)
run: rustup show
Expand All @@ -37,3 +47,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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-compile targets installed on overridden toolchain

High Severity

dtolnay/rust-toolchain@stable installs cross-compile targets (aarch64-apple-darwin, x86_64-apple-darwin) on the stable toolchain via rustup default, but rust-toolchain.toml (with channel = "1.93") takes precedence per rustup's override order. When cargo build --target x86_64-apple-darwin runs, it uses the 1.93 toolchain, which doesn't have that target installed. On ARM64 macos-latest runners, the aarch64 build would succeed (host target), but the x86_64 cross-compile build would fail. By that point, semantic-release has already created the tag and GitHub Release, leaving a half-published release with no binary assets.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4278e44. Configure here.


- 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 }}
50 changes: 0 additions & 50 deletions .github/workflows/release.yml

This file was deleted.

32 changes: 32 additions & 0 deletions .releaserc.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
22 changes: 22 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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): <version> [skip ci]` (the `[skip ci]` keeps the bump from re-triggering the pipeline).
4. **`@semantic-release/github`** creates the `v<version>` 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.
Expand Down
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
30 changes: 30 additions & 0 deletions scripts/release-prepare.sh
Original file line number Diff line number Diff line change
@@ -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 <version>" >&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"
Loading