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 diff --git a/.github/workflows/release_please.yml b/.github/workflows/release_please.yml new file mode 100644 index 0000000..4aeddd9 --- /dev/null +++ b/.github/workflows/release_please.yml @@ -0,0 +1,197 @@ +name: Release Please + +on: + push: + branches: + - main + workflow_dispatch: + inputs: + tag_name: + description: Existing release tag to rebuild artifacts for; run the workflow on the same tag + 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: 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: + 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 + + - name: Publish release + env: + GH_TOKEN: ${{ github.token }} + run: gh release edit "$TAG_NAME" --draft=false 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/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..85a0dec --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,71 @@ +# 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`, +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. + +To rebuild the assets for an existing tag, run the workflow on that tag so the +attestation records the tagged commit: + +```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/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/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). 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..6b89ee6 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,15 @@ +{ + "$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, + "draft": true, + "force-tag-creation": true + } + } +}