From d084c7b9cfc917a6881fb0a244b70ddcdf162de1 Mon Sep 17 00:00:00 2001 From: furedea <132188853+furedea@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:38:22 +0900 Subject: [PATCH 1/4] ci: align action pins and toolchain with agent-harness Pin actions/checkout to v6.1.0, setup-rust-toolchain to v1.17.0, and Rust to 1.98.1, and run tests with --locked. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 30 ++++++++++++------------- .github/workflows/codeql.yml | 2 +- .github/workflows/dependency_review.yml | 2 +- .github/workflows/gha_lint.yml | 4 ++-- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88e92dc..fb489f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: contents: read steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: persist-credentials: false @@ -49,14 +49,14 @@ jobs: contents: read steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: persist-credentials: false - name: Install Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 + uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 with: - toolchain: stable + toolchain: 1.98.1 components: clippy rustflags: "" @@ -70,14 +70,14 @@ jobs: contents: read steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: persist-credentials: false - name: Install Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 + uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 with: - toolchain: stable + toolchain: 1.98.1 components: rustfmt rustflags: "" @@ -91,7 +91,7 @@ jobs: contents: read steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: persist-credentials: false @@ -117,7 +117,7 @@ jobs: contents: read steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: persist-credentials: false @@ -143,7 +143,7 @@ jobs: contents: read steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: persist-credentials: false @@ -169,18 +169,18 @@ jobs: contents: read steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: persist-credentials: false - name: Install Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0 + uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 with: - toolchain: stable + toolchain: 1.98.1 rustflags: "" - name: Run tests - run: cargo test + run: cargo test --locked audit: runs-on: ubuntu-latest @@ -189,7 +189,7 @@ jobs: contents: read steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: persist-credentials: false diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c697b66..411940a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -32,7 +32,7 @@ jobs: language: [rust, javascript-typescript, actions] steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: persist-credentials: false diff --git a/.github/workflows/dependency_review.yml b/.github/workflows/dependency_review.yml index f815cd5..9346820 100644 --- a/.github/workflows/dependency_review.yml +++ b/.github/workflows/dependency_review.yml @@ -21,7 +21,7 @@ jobs: contents: read steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: persist-credentials: false diff --git a/.github/workflows/gha_lint.yml b/.github/workflows/gha_lint.yml index d1b8462..f951725 100644 --- a/.github/workflows/gha_lint.yml +++ b/.github/workflows/gha_lint.yml @@ -30,7 +30,7 @@ jobs: contents: read steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: persist-credentials: false @@ -49,7 +49,7 @@ jobs: contents: read steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: persist-credentials: false From 687fd49a53a4cff92a38264a55694646674fec3d Mon Sep 17 00:00:00 2001 From: furedea <132188853+furedea@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:38:22 +0900 Subject: [PATCH 2/4] ci: release aarch64-apple-darwin binaries with release-please Add Release Please config and a workflow that builds the cargo-dist archive on a macOS runner, smoke-tests it, attests its provenance, and uploads the archive, checksums, and shell installer to the release. The flake now reads its version from Cargo.toml. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release_please.yml | 175 +++++++++++++++++++++++++++ .ls-lint.yml | 3 + .release-please-manifest.json | 3 + Cargo.toml | 23 ++++ flake.nix | 2 +- release-please-config.json | 13 ++ 6 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release_please.yml create mode 100644 .release-please-manifest.json create mode 100644 release-please-config.json diff --git a/.github/workflows/release_please.yml b/.github/workflows/release_please.yml new file mode 100644 index 0000000..ce29c27 --- /dev/null +++ b/.github/workflows/release_please.yml @@ -0,0 +1,175 @@ +name: Release Please + +on: + push: + branches: + - main + workflow_dispatch: + inputs: + tag_name: + description: Existing release tag to rebuild artifacts for + required: true + type: string + +permissions: {} + +defaults: + run: + shell: bash + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + release-please: + if: ${{ github.event_name == 'push' }} + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + issues: write + pull-requests: write + outputs: + release_created: ${{ steps.release.outputs.release_created }} + tag_name: ${{ steps.release.outputs.tag_name }} + steps: + - name: Check release token + env: + RELEASE_PLEASE_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }} + run: | + set -euo pipefail + + if [[ -z "$RELEASE_PLEASE_TOKEN" ]]; then + echo "RELEASE_PLEASE_TOKEN repository secret is required." + exit 1 + fi + + - name: Create or update release PR + id: release + uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1 + with: + token: ${{ secrets.RELEASE_PLEASE_TOKEN }} + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + + release-artifacts: + needs: release-please + if: ${{ always() && (needs.release-please.outputs.release_created == 'true' || github.event_name == 'workflow_dispatch') }} + runs-on: macos-15 + timeout-minutes: 30 + permissions: + contents: write + id-token: write + attestations: write + env: + CARGO_DIST_VERSION: 0.30.2 + TARGET: aarch64-apple-darwin + TAG_NAME: ${{ github.event_name == 'workflow_dispatch' && inputs.tag_name || needs.release-please.outputs.tag_name }} + steps: + - name: Checkout release tag + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + persist-credentials: false + ref: ${{ env.TAG_NAME }} + + - name: Install Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 + with: + toolchain: 1.98.1 + target: ${{ env.TARGET }} + rustflags: "" + + - name: Setup pnpm + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 22 + + - name: Install cargo-dist + run: | + curl --proto '=https' --tlsv1.2 -LsSf \ + "https://github.com/axodotdev/cargo-dist/releases/download/v${CARGO_DIST_VERSION}/cargo-dist-installer.sh" \ + | sh + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + + - name: Run tests + run: cargo test --locked + + - name: Install JXA check dependencies + run: pnpm install --frozen-lockfile + + - name: Check embedded JXA + run: pnpm run check + + - name: Build release artifacts + run: | + set -euo pipefail + + dist build \ + --tag "$TAG_NAME" \ + --target "$TARGET" \ + --artifacts=all \ + --output-format=json \ + > dist-manifest.json + dist print-upload-files-from-manifest \ + --manifest dist-manifest.json \ + > dist-upload-files.txt + + - name: Smoke test packaged archive + run: | + set -euo pipefail + + archive_path="$( + find target/distrib -maxdepth 1 -type f \ + \( -name "apple-mail-cli-${TARGET}.tar.xz" -o -name "apple-mail-cli-${TARGET}.tar.gz" \) \ + | head -n 1 + )" + + if [[ -z "$archive_path" ]]; then + echo "apple-mail-cli archive for ${TARGET} was not produced." + exit 1 + fi + + tmpdir="$(mktemp -d)" + + case "$archive_path" in + *.tar.xz) + tar -xJf "$archive_path" -C "$tmpdir" --strip-components=1 + ;; + *.tar.gz) + tar -xzf "$archive_path" -C "$tmpdir" --strip-components=1 + ;; + *) + echo "unsupported archive format: $archive_path" + exit 1 + ;; + esac + + "$tmpdir/apple-mail" --version + "$tmpdir/apple-mail" --help + + - name: Attest release artifacts + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: target/distrib/* + + - name: Upload release artifacts + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + + # macOS runners ship Bash 3.2, which has no mapfile. + artifact_paths=() + while IFS= read -r artifact_path; do + artifact_paths+=("$artifact_path") + done < dist-upload-files.txt + if ((${#artifact_paths[@]} == 0)); then + echo "cargo-dist did not report uploadable files." + exit 1 + fi + + gh release upload "$TAG_NAME" "${artifact_paths[@]}" --clobber diff --git a/.ls-lint.yml b/.ls-lint.yml index b828958..539a097 100644 --- a/.ls-lint.yml +++ b/.ls-lint.yml @@ -11,6 +11,9 @@ ignore: - docs/logs - README.md + - CONTRIBUTING.md + - CHANGELOG.md + - release-please-config.json - Cargo.toml - Cargo.lock diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..466df71 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.1.0" +} diff --git a/Cargo.toml b/Cargo.toml index 04a71dd..ed8acee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,3 +29,26 @@ pedantic = { level = "warn", priority = -1 } dbg_macro = "deny" todo = "deny" unimplemented = "deny" + +[profile.dist] +inherits = "release" +lto = "thin" + +# Config for 'dist' +[workspace.metadata.dist] +# The preferred dist version to use in CI (Cargo.toml SemVer syntax) +cargo-dist-version = "0.30.2" +# CI backends to support +ci = "github" +# This repository keeps a custom Release Please workflow and uses dist only for +# artifact and installer generation inside that workflow. +allow-dirty = ["ci"] +# The installers to generate for each app +installers = ["shell"] +# Mail automation needs Mail.app and /usr/bin/osascript, so only Apple Silicon +# macOS is a meaningful target. +targets = ["aarch64-apple-darwin"] +# Path that installers should place binaries in +install-path = "CARGO_HOME" +# Whether to install an updater program +install-updater = false diff --git a/flake.nix b/flake.nix index 1f7b28e..825c618 100644 --- a/flake.nix +++ b/flake.nix @@ -13,7 +13,7 @@ pkgs = import nixpkgs { inherit system; }; package = pkgs.rustPlatform.buildRustPackage { pname = "apple-mail-cli"; - version = "0.1.0"; + inherit ((builtins.fromTOML (builtins.readFile ./Cargo.toml)).package) version; src = ./.; cargoLock.lockFile = ./Cargo.lock; meta = { diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..aa33038 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "packages": { + ".": { + "release-type": "rust", + "package-name": "apple-mail-cli", + "changelog-path": "CHANGELOG.md", + "include-v-in-tag": true, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true + } + } +} From c6f2cc74e788638a48054799da942b07da260104 Mon Sep 17 00:00:00 2001 From: furedea <132188853+furedea@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:38:23 +0900 Subject: [PATCH 3/4] docs: document release installs and contributing Co-Authored-By: Claude Opus 5.5 --- CONTRIBUTING.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 43 +++++++++++++++++++++++++++++--- 2 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4997bbf --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,65 @@ +# Contributing + +## Development environment + +Clone the repository and enter the Nix development shell on Apple Silicon +macOS: + +```bash +git clone https://github.com/furedea/apple-mail-cli +cd apple-mail-cli +nix develop +pnpm install --frozen-lockfile +lefthook install +``` + +## Running local changes + +An `apple-mail` command on `PATH` may refer to a previously installed release +build. Run the current checkout through Cargo when developing or reviewing local +changes: + +```bash +cargo run -- accounts +cargo run -- unread --limit 5 +``` + +Use the local flake when the Nix package itself must be verified: + +```bash +nix run . -- accounts +``` + +## Quality gates + +Run the Rust and Nix gates: + +```bash +cargo fmt --check +cargo clippy --all-targets -- -D warnings +cargo test --locked +nix flake check +``` + +Run the embedded JXA format, lint, and type checks: + +```bash +pnpm run check +``` + +CI additionally runs Nix linting, dependency audits, GitHub Actions linting, +dependency review, and CodeQL. + +## Release + +Releases are managed by Release Please. Merging the release PR updates +`Cargo.toml`, `Cargo.lock`, `.release-please-manifest.json`, and `CHANGELOG.md`, +then publishes a GitHub Release with the `aarch64-apple-darwin` cargo-dist +archive, its checksum, a shell installer, and build provenance attestations. + +If the release assets need to be rebuilt for an existing tag, run the +`Release Please` workflow manually with the tag name, for example: + +```text +apple-mail-cli-v0.1.1 +``` diff --git a/README.md b/README.md index 5fa990b..b7fd65c 100644 --- a/README.md +++ b/README.md @@ -9,20 +9,51 @@ tokens, or a separate account configuration. Requirements: -- macOS with the target accounts configured in Mail -- Rust and Cargo +- Apple Silicon macOS with the target accounts configured in Mail + +Release binaries are built for `aarch64-apple-darwin` only, because the CLI +needs Mail.app and `/usr/bin/osascript`. The quickest install uses the shell +installer generated by cargo-dist: ```console -cargo install --path . +curl --proto '=https' --tlsv1.2 -LsSf \ + https://github.com/furedea/apple-mail-cli/releases/latest/download/apple-mail-cli-installer.sh \ + | sh apple-mail accounts ``` -The repository also exposes a Nix package and app: +For a source-based Cargo install, which requires Rust and Cargo: + +```console +cargo install --locked --git https://github.com/furedea/apple-mail-cli apple-mail-cli +``` + +The shell and Cargo installers place the binary under Cargo's bin directory, +normally `$HOME/.cargo/bin`. Make sure that directory is on `PATH`. + +For Nix: ```console +nix profile install github:furedea/apple-mail-cli nix run github:furedea/apple-mail-cli -- accounts ``` +Release binaries are not signed with a Developer ID or notarized. The curl +installer does not set the quarantine attribute, but an archive downloaded with +a browser does. Remove it from the extracted binary before running it: + +```console +xattr -d com.apple.quarantine ./apple-mail +``` + +Each release archive has a `.sha256` file and a GitHub build provenance +attestation: + +```console +shasum -a 256 -c apple-mail-cli-aarch64-apple-darwin.tar.xz.sha256 +gh attestation verify apple-mail-cli-aarch64-apple-darwin.tar.xz --repo furedea/apple-mail-cli +``` + The first Mail operation can trigger a macOS Automation prompt. Allow the calling terminal under System Settings > Privacy & Security > Automation. @@ -86,3 +117,7 @@ Apple documents Mail's scripting terminology but may change it between macOS releases. Large inbox queries depend on Mail's Apple Event performance and stop after 30 seconds. Search covers sender and subject in the selected mailbox, or the aggregate inbox when no mailbox is supplied. + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md). From eee07479fcde9db688e188fd16f7a0c3f26f0c98 Mon Sep 17 00:00:00 2001 From: furedea <132188853+furedea@users.noreply.github.com> Date: Tue, 20 Oct 2026 11:16:43 +0900 Subject: [PATCH 4/4] ci: publish releases only after verified artifacts upload Release Please now creates draft releases with forced tag creation, and the artifact job publishes the release only after tests, build, smoke test, attestation, and uploads succeed, so the latest installer URL never points at missing files. Manual rebuilds must run on the tag itself, and the job fails unless the checked-out commit equals the run's commit, so attestations record the tagged commit instead of main. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release_please.yml | 24 +++++++++++++++++++++++- CONTRIBUTING.md | 18 ++++++++++++------ release-please-config.json | 4 +++- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release_please.yml b/.github/workflows/release_please.yml index ce29c27..4aeddd9 100644 --- a/.github/workflows/release_please.yml +++ b/.github/workflows/release_please.yml @@ -7,7 +7,7 @@ on: workflow_dispatch: inputs: tag_name: - description: Existing release tag to rebuild artifacts for + description: Existing release tag to rebuild artifacts for; run the workflow on the same tag required: true type: string @@ -73,6 +73,23 @@ jobs: persist-credentials: false ref: ${{ env.TAG_NAME }} + - name: Verify release ref + run: | + set -euo pipefail + + # Provenance must name the tagged commit, so manual rebuilds run on the tag. + if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" && "$GITHUB_REF" != "refs/tags/$TAG_NAME" ]]; then + echo "Run manual rebuilds on the tag itself:" + echo " gh workflow run release_please.yml --ref $TAG_NAME -f tag_name=$TAG_NAME" + exit 1 + fi + + head_sha="$(git rev-parse HEAD)" + if [[ "$head_sha" != "$GITHUB_SHA" ]]; then + echo "$TAG_NAME points to $head_sha, but this run is for $GITHUB_SHA." + exit 1 + fi + - name: Install Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 with: @@ -173,3 +190,8 @@ jobs: fi gh release upload "$TAG_NAME" "${artifact_paths[@]}" --clobber + + - name: Publish release + env: + GH_TOKEN: ${{ github.token }} + run: gh release edit "$TAG_NAME" --draft=false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4997bbf..85a0dec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,12 +54,18 @@ dependency review, and CodeQL. Releases are managed by Release Please. Merging the release PR updates `Cargo.toml`, `Cargo.lock`, `.release-please-manifest.json`, and `CHANGELOG.md`, -then publishes a GitHub Release with the `aarch64-apple-darwin` cargo-dist -archive, its checksum, a shell installer, and build provenance attestations. +tags the merge commit, and creates a draft GitHub Release. The release workflow +then tests and builds the `aarch64-apple-darwin` cargo-dist archive, attests its +provenance, and uploads the archive, its checksum, and a shell installer. The +release is published only after every step succeeds; if one fails, it stays a +draft. -If the release assets need to be rebuilt for an existing tag, run the -`Release Please` workflow manually with the tag name, for example: +To rebuild the assets for an existing tag, run the workflow on that tag so the +attestation records the tagged commit: -```text -apple-mail-cli-v0.1.1 +```bash +tag=apple-mail-cli-v0.1.1 +gh workflow run release_please.yml --ref "$tag" -f tag_name="$tag" ``` + +The workflow fails before building when it is started from any other ref. diff --git a/release-please-config.json b/release-please-config.json index aa33038..6b89ee6 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -7,7 +7,9 @@ "changelog-path": "CHANGELOG.md", "include-v-in-tag": true, "bump-minor-pre-major": true, - "bump-patch-for-minor-pre-major": true + "bump-patch-for-minor-pre-major": true, + "draft": true, + "force-tag-creation": true } } }