Run fmt, clippy, tests, and rustdoc on both hosts

Two runners on purpose. The CSR tests verify our output through openssl rather than through our own parser, and the hosts ship different ones: ubuntu has OpenSSL 3, macOS has LibreSSL, and they disagree about which stream the verify banner goes to. That difference already cost a debugging round locally, so a single-host matrix would let the same class of bug through unnoticed. macOS is also where this crate's signing work has to run eventually, since keychain access exists nowhere else. fail-fast is off because when a test fails on exactly one host, that asymmetry is the most useful part of the report. The rustdoc job pays for itself immediately: it found a link to a `KeyPair` type that does not exist, in the paragraph explaining that a lost private key is unrecoverable. The intra-doc links carry real weight here, since the certificate module's warnings point at the csr module that explains them, and a broken one drops that thread silently. Also fixes a pre-existing unclosed `<key-id>` HTML tag in the CLI's help text, which rustdoc reads as a tag rather than a placeholder. openssl version is printed before the tests because it decides how the CSR verification behaves and changes under us whenever GitHub bumps runner images. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PGeEFM4hBGSZYXjFWguaWa

Seto Elkahfi committed Aug 6, 2026 at 07:23 UTC 985d18c8dbfff062d0bda7fdc4b3f710d012bb08
3 files changed +68 -2
.github/workflows/ci.yml new
+66
@@ -0,0 +1,66 @@
1 +name: Tests
2 +
3 +# Runs on every push.
4 +# Covers the Rust workspace: formatting, clippy, tests, and the rustdoc
5 +# build (which catches broken intra-doc links, of which this crate has
6 +# many).
7 +on: push
8 +
9 +env:
10 + CARGO_TERM_COLOR: always
11 +
12 +jobs:
13 + test:
14 + # Both hosts on purpose. The CSR tests shell out to `openssl` to verify
15 + # our output against an independent implementation, and the two runners
16 + # ship different ones: ubuntu has OpenSSL 3, macOS has LibreSSL. They
17 + # disagree on which stream the `verify OK` banner goes to, so a
18 + # single-host matrix would let a real portability bug through. macOS is
19 + # also where this crate's signing work ultimately has to run, since
20 + # keychain access exists nowhere else.
21 + name: Rust (${{ matrix.os }})
22 + runs-on: ${{ matrix.os }}
23 + strategy:
24 + # Don't cancel the other host on first failure: when a test fails on
25 + # exactly one of them, that difference is the most useful part of the
26 + # report.
27 + fail-fast: false
28 + matrix:
29 + os: [ubuntu-latest, macos-latest]
30 +
31 + steps:
32 + - name: Checkout
33 + uses: actions/checkout@v6
34 +
35 + - name: Install toolchain
36 + uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
37 + with:
38 + toolchain: stable
39 + components: rustfmt, clippy
40 +
41 + - name: Setup Rust cache
42 + uses: Swatinem/rust-cache@v2
43 + with:
44 + key: test-${{ matrix.os }}
45 +
46 + - name: Report the openssl in use
47 + # Recorded because it decides how the CSR verification tests
48 + # behave, and it changes under us when GitHub bumps runner images.
49 + run: openssl version
50 +
51 + - name: Check formatting
52 + run: cargo fmt --all -- --check
53 +
54 + - name: Clippy
55 + run: cargo clippy --workspace --all-targets --locked -- -D warnings
56 +
57 + - name: Test
58 + run: cargo test --workspace --locked
59 +
60 + - name: Build docs
61 + # Intra-doc links are load-bearing in this crate: the certificate
62 + # module's warnings about lost private keys point at the csr module
63 + # that explains them. A broken link silently drops that thread.
64 + env:
65 + RUSTDOCFLAGS: -D warnings
66 + run: cargo doc --no-deps --workspace --locked
crates/ascapi/src/csr.rs
+1 -1
@@ -7,7 +7,7 @@
7 //! binding your public key to your team identity.
8 //!
9 //! The private half never leaves this process except through
10 -//! [`KeyPair::private_key_pem`], and it is the part that actually matters:
10 +//! [`CertificateRequest::private_key_pem`], and it is the part that actually matters:
11 //! a certificate whose private key is lost is dead, permanently, because
12 //! Apple never had a copy to give back.
13 //!
crates/cli/src/main.rs
+1 -1
@@ -32,7 +32,7 @@ struct Cli {
32 issuer_id: String,
33
34 /// Path to the key's .p8 private key file. Defaults to
35 - /// ~/.appstoreconnect/private_keys/AuthKey_<key-id>.p8, matching
35 + /// `~/.appstoreconnect/private_keys/AuthKey_<key-id>.p8`, matching
36 /// Xcode's own convention.
37 #[arg(long, env = "ASC_PRIVATE_KEY_PATH", global = true)]
38 private_key_path: Option<PathBuf>,