Skip to content

ci: workflow hardening + macOS build & release binaries - #1

Closed
plusky wants to merge 17 commits into
masterfrom
ci/workflow-hardening
Closed

ci: workflow hardening + macOS build & release binaries#1
plusky wants to merge 17 commits into
masterfrom
ci/workflow-hardening

Conversation

@plusky

@plusky plusky commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Summary

Integration branch for CI/workflow hardening, plus this round's cross-platform
work: repose now builds & tests on macOS in CI and publishes macOS binaries
on release.

This session's changes (top of branch)

  • ci: add macOS (Apple Silicon) build/test leg — new rust-macos job on
    macos-latest (aarch64-apple-darwin): clippy + full test suite. Validated
    green in CI.
  • test(cli): gate Linux-coupled stderr-color tests to Linux — two
    stderr-color tests assert on a DEBUG record only the Linux/BSD CA-file path
    of rustls-platform-verifier emits ("Loaded N CA root certificates"); it
    is absent on macOS (Security.framework) and Windows (CryptoAPI). Gated to
    target_os = "linux"; Linux coverage unchanged.
  • ci(release): build and publish macOS (Apple Silicon) binaries — release
    workflow refactored into a build matrix (x86_64-unknown-linux-gnu +
    aarch64-apple-darwin) that uploads per-triple artifacts, plus a publish job
    that gathers them into one GitHub release. Builds stay hermetic (no cargo
    cache), every leg re-verifies the tag matches the workspace version, and the
    release publishes only if all legs succeed (no partial artifact sets).
    Checksums use shasum -a 256 for ubuntu/macOS parity.

Also on this branch

Earlier hardening/perf work already present over master: workflow credential
hardening + caching + a beta early-warning leg, ClusterFuzzLite fuzzing, action
SHA pinning, dependency bumps, and the P1 resource-limit / bounded-fan-out
work.

Testing

  • Linux CI: green.
  • macOS CI (rust-macos): green — clippy + all tests pass on Apple Silicon.
  • release.yml is tag-triggered; not exercised end-to-end (that cuts a real
    release). Statically validated; its cargo build --release is the same
    command proven to compile on the macOS runner. First runs on the next tag.

mimi1vx and others added 17 commits July 24, 2026 14:04
Why this change was needed:
repose has no way to represent a validated, non-zero concurrency cap or
a phase deadline/byte limit in one place: `CommandOptions` hard-coded a
per-host probe cap, SSH phases had no timeout budgets at all beyond the
final command-completion timeout, and there was no typed error for
"this operation exceeded its configured bound" — every future P1 cap
would otherwise invent its own ad hoc representation.

Problem solved:
Establishes one immutable, core-owned policy surface (`ConnectionConfig`)
that CLI, mocks, and the SSH transport all read from, with `NonZeroUsize`
making a zero concurrency cap unrepresentable, and one typed `SshError`
family (`Timeout`, `OutputTooLarge`, `SftpFileTooLarge`,
`DirectoryTooLarge`, `KnownHostsPersistFailed`) that later changesets map
real overflow/timeout conditions onto instead of string-matching.

What changed:
- `ConnectionConfig`: host/probe/SFTP concurrency limits, SFTP/output byte
  caps, and per-phase SSH deadlines, all with reviewed defaults
- `SshError`: typed timeout/overflow/persistence-failure variants
- `HostGroup::host_operation_limit()` trait accessor (object-safe)
- CLI wiring: every invocation now constructs the reviewed defaults
…l probe budget

Why this change was needed:
Every host-group phase and every mutation command (add/install/reset/
remove/clear/uninstall) fanned out with plain `join_all`, so a 100-host
command started 100 simultaneous connections/commands with no ceiling,
and URL-liveness probing was capped at 16 *per host* — a 100-host `add`
could attempt roughly 1,600 concurrent probes. Neither scales safely to
a real fleet, and a slow host under a naive ordered `.buffered(limit)`
would block admission of unrelated, already-ready work behind it.

Problem solved:
`MockHostGroup`'s phases and every mutation worker now fan out through
`buffer_unordered`/a semaphore-guarded `join_all` bounded by the reviewed
`host_operation_limit`, with results tagged by their original index and
restored to key order before aggregation — so scheduling changes, but
every host still runs exactly once, in its original local command order,
with the same per-host failure isolation and BTreeMap aggregation order
as before. A new fleet-wide `ProbeBudget` semaphore replaces the per-host
probe cap, bounding total concurrent probes across the whole command
instead of multiplying by host count.

What changed:
- `mock.rs`: bounded connect/prune, reads, parse, run-all, and close
- `commands/mod.rs`: `ProbeBudget`, permit acquired per `Probe::is_live`
- `add.rs`/`install.rs`/`reset.rs`: bounded workers + a probe budget
- `remove.rs`/`clear.rs`/`uninstall.rs`: bounded workers (no probing)
- `Cargo.toml`: tokio's `sync` feature (already an in-tree transitive
  dependency via reqwest) made directly usable for the worker semaphore
…new TOFU gap

Why this change was needed:
The SSH transport had no bound on host-group concurrency, and only
command completion had a timeout — connection, authentication, channel
open, exec dispatch, SFTP subsystem setup, SFTP reads, product-directory
listings, and command stdout/stderr were all unbounded, so a single
stalled phase or a pathological/oversized remote response could pin a
host slot or exhaust memory forever. Separately, `accept-new`'s
trust-on-first-use flow trusted an unknown host key *before* confirming
it was durably recorded: if the `known_hosts` write failed (read-only or
full filesystem), the session proceeded anyway, degrading every future
connection to trust-without-verification with no durable pin — a TOFU
guarantee that only held as long as the disk write happened to succeed.
Private-key discovery/parsing and `accept-new` persistence also ran
synchronously on the CLI's current-thread runtime, stalling every other
host's progress during file/crypto work.

Problem solved:
- `RusshHostGroup` bounds every phase to `host_operation_limit`, the same
  indexed-unordered pattern as the mock implementation.
- Every SSH phase (connect+handshake, authentication, channel open,
  dispatch/subsystem, SFTP operations) now has its own typed deadline;
  a phase that completes on time is unaffected, one that stalls returns
  a typed `SshError::Timeout` instead of hanging the host forever.
- SFTP reads are incrementally bounded (checked before buffer growth, not
  after a full read), `/etc/products.d` listings are capped before any
  addon path is built, and stdout/stderr each have an independent byte
  cap with bounded cleanup (channel close) on overflow.
- Private-key discovery/loading and `accept-new` persistence now run via
  `spawn_blocking`, so a slow disk/crypto operation no longer stalls
  sibling hosts on the current-thread runtime.
- `accept-new` first contact is now fail-closed: `check_server_key` only
  trusts an unknown key *after* a successful durable `known_hosts` write;
  a persistence failure rejects the session instead of silently trusting
  an unrecorded key. Matching/changed/revoked-key decisions are
  unaffected — this is the one intentional, approved behavior change.

What changed:
- `session.rs`: `with_deadline` phase-budget helper; bounded incremental
  SFTP reads and stdout/stderr accumulation; `spawn_blocking` offload for
  key discovery/loading; `ClientHandler::check_server_key` now awaits
  persistence before trusting a first-contact key
- `hostkey.rs`: `HostKeyVerifier::decide` is now a pure decision (no I/O);
  persistence moved to a separate blocking `persist_first_contact`
- `host.rs`: bounded `RusshHostGroup` phases; typed timeout mapped onto
  the existing synthetic out-entry contract; products.d cap enforced
  before addon-path construction
- `README.md` / `crates/README.md`: document the fail-closed correction
- `tests/ssh_integration.rs`: live-gated coverage for the byte/entry caps
  and the accept-new fail-closed path (require a Docker/SSH fixture)
Why this change was needed:
Deciding real host/probe concurrency caps for P1 required measured
evidence, but the existing mock workload's operations mostly complete in
a single poll under plain `join_all`, so the harness could report a
misleading peak concurrency of one even when a cap was actually binding.
There was no workload that kept operations pending long enough to expose
real admission behavior across a range of candidate caps.

Problem solved:
Adds deterministic, gated 20/100-host fleet scenarios whose futures stay
pending until explicitly released, so sweeping a candidate cap shows the
fleet actually reach the expected width and lets `tests/performance/p1-limit-decision.md`'s
concurrency-cap table cite real measured peaks instead of a harness
artifact.

What changed:
- `benches/support/mod.rs`: gated host-operation helpers for the sweep
- `benches/fleet.rs`: `fleet_gated_admission` and
  `fleet_concurrency_cap_sweep_100h` benchmark groups
Why this change was needed:
Every numeric default in the P1 changesets (host/probe/SFTP concurrency,
byte caps, phase deadlines) had to trace to measured evidence and receive
explicit review before implementation, and the finished work needed a
single before/after record tying the implementation back to that
evidence — otherwise the caps landed in the earlier commits would read as
unexplained magic numbers.

Problem solved:
`p1-limit-decision.md` is the reviewed decision table (mock concurrency-
cap sweeps, a real multi-session SSH measurement via a local container
substitute, and refhost-fixture-derived byte/count bounds) every P1
default traces back to. `p1-review-packet.md` closes the loop: it reruns
every mock workload after all changesets, compares against the committed
P0 baseline under the project's own regression thresholds, and records
per-changeset equivalence proofs, fail-closed host-key test evidence, and
a rollback boundary per changeset.

What changed:
- `tests/performance/p1-limit-decision.md`: the approved limit table
- `tests/performance/p1-review-packet.md`: final remeasurement + sign-off
- `scripts/measure-ssh-concurrency.sh`,
  `scripts/measure-ssh-concurrency-container.sh`,
  `scripts/ssh-fixture-container.sh`: local-dev-only measurement scripts
  used to gather the SSH evidence (the committed, CI-facing
  `tests/ssh/run.sh` remains Docker-only and unmodified)
Why this change was needed:
tests/performance/p1-review-packet.md's sign-off checkbox is the explicit
review gate P1's plan requires before the phase is considered mergeable.

What changed:
- Checked the review sign-off box.
…udit

Why this change was needed:
A cargo-hawk 0.1.9 audit against the two shipped binaries (repose,
repose-gen) surfaced 107 findings: 6 provably dead public items with
no callers anywhere in the workspace, plus ~101 items marked `pub`
that are only ever reached from within their own crate.

Problem solved:
Unused public API and over-broad visibility make the true surface
area of repose-core/repose-ssh hard to see and refactor safely.
Removing genuinely dead code and narrowing visibility to what's
actually reachable makes the crate boundaries honest.

What changed:
- hawk.toml: declare repose-gen as a second production target
- Delete 6 dead public items: HostKeyPolicy::parse/as_str,
  Repositories::get/len/is_empty, RusshHost::key_str
- Narrow ~101 pub declarations to pub(crate)/private via
  `cargo hawk --fix`, run to a fixpoint (visibility narrowing
  cascades and exposes further narrowing opportunities)
- Split compound `pub use` re-exports that mixed still-public and
  now-crate-only items, and drop re-exports that had no callers
  through any path
- Mark test-only helpers `#[cfg(test)]` where narrowing visibility
  turned them into rustc dead_code in non-test builds
- cargo hawk check -D warnings now exits 0 (zero findings)
chore(hawk): remove dead code and tighten visibility via cargo-hawk audit
All other actions in the workflows were already pinned by SHA; pin the
remaining tag/branch refs flagged by OSSF Scorecard Pinned-Dependencies:

- dtolnay/rust-toolchain (@stable x5, @1.96 x1): pin the master-branch
  HEAD commit — toolchain-branch commits are garbage-collected upstream —
  and select the toolchain via the toolchain: input, which the action
  documents for SHA-pinned usage. The MSRV job keeps installing 1.96.
- EmbarkStudios/cargo-deny-action@v2 and
  github/codeql-action/{init,analyze}@v4: pin the commits the annotated
  tags resolve to.

Dependabot keeps the newly pinned codeql-action and cargo-deny-action
SHAs current (as it already does for the previously pinned actions);
dtolnay/rust-toolchain stays ignored since the action publishes no
semver releases.
init_logging only consulted --no-color/NO_COLOR, so the stderr tracing
subscriber kept emitting ANSI escapes with --color=never (making the
documented alias pair --no-color/--color=never diverge) and colorized
piped stderr under the default auto mode. The subscriber now shares the
same resolve_color decision as stdout, computed against stderr's own TTY
bit: never/no-color force off, always forces on, auto colors only on a
TTY (honoring NO_COLOR/COLOR).

One deliberate semantic alignment: --color=always + NO_COLOR now colors
stderr logs, matching how stdout already resolved that combination.

Subprocess tests pin both modes against a real debug log record, with
cert-store env scrubbed for hermeticity.
Bumps the actions-minor-patch group with 3 updates: [taiki-e/install-action](https://github.com/taiki-e/install-action), [ossf/scorecard-action](https://github.com/ossf/scorecard-action) and [github/codeql-action/upload-sarif](https://github.com/github/codeql-action).


Updates `taiki-e/install-action` from 2.84.0 to 2.85.0
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](taiki-e/install-action@a6b2e2d...7572810)

Updates `ossf/scorecard-action` from 2.4.3 to 2.4.4
- [Release notes](https://github.com/ossf/scorecard-action/releases)
- [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md)
- [Commits](ossf/scorecard-action@4eaacf0...2d11466)

Updates `github/codeql-action/upload-sarif` from 4.37.1 to 4.37.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@7188fc3...e4fba86)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.85.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-minor-patch
- dependency-name: ossf/scorecard-action
  dependency-version: 2.4.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-minor-patch
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.37.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps the cargo-minor-patch group with 2 updates: [tokio](https://github.com/tokio-rs/tokio) and [russh](https://github.com/warp-tech/russh).


Updates `tokio` from 1.53.0 to 1.53.1
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](tokio-rs/tokio@tokio-1.53.0...tokio-1.53.1)

Updates `russh` from 0.62.2 to 0.62.4
- [Release notes](https://github.com/warp-tech/russh/releases)
- [Commits](Eugeny/russh@v0.62.2...v0.62.4)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.53.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-patch
- dependency-name: russh
  dependency-version: 0.62.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Add a cargo-fuzz harness (fuzz/, detached from the workspace so the
nightly-only tooling cannot affect the MSRV, Cargo.lock, cargo-deny, or
stable CI) with four targets over the host-controlled parsers: repodata
XML, products.d/os-release parsing, host-spec parsing, and version/repo
name transforms.

The pure parsing helpers the targets exercise become public in
repose-core (parse_prod_xml, parse_os_release, product_from_repo_name,
transform_version_partialy + the transform module); they were already
documented.

Run them under ClusterFuzzLite on every PR and on master pushes (address
sanitizer, 300s, code-change mode for PRs), with crashes uploaded as
artifacts. Project files live in .clusterfuzzlite/ (external-project
contract: Dockerfile + build.sh + project.yaml); the base builder image
is digest-pinned, with a dated nightly selected via RUSTUP_TOOLCHAIN
because the image's bundled nightly predates the workspace MSRV and the
-Zsanitizer flags rule out the repo's stable pin.

.dockerignore previously whitelisted only test vectors (for the SSH
fixture), which would have left the build context empty; un-ignore the
sources the fuzz build needs while keeping rust-toolchain.toml out.
- persist-credentials: false on every actions/checkout: no workflow
  needs git credentials on disk after checkout (the release workflow
  authenticates gh via GH_TOKEN, not stored git credentials).
- release.yml: drop the cargo cache so published artifacts are always
  built from a clean slate; ci.yml: skip the cache on tag runs, which
  re-verify a release. PR and master runs keep the cache, so CI speed
  is unchanged where it matters.
- ci.yml: new non-blocking rust-beta job (clippy + test on the beta
  toolchain) to surface upcoming compiler/lint breakage before beta
  becomes stable.
Portability early-warning on macos-latest (aarch64-apple-darwin): clippy +
test the workspace and SSH transport so the platform stays viable before we
publish macOS release binaries.
Both stderr-log color tests assert on a DEBUG record that only the Linux/BSD
CA-file path of rustls-platform-verifier emits ("Loaded N CA root
certificates"). On macOS (Security.framework) and Windows (CryptoAPI) that
line is absent, so the commands emit no DEBUG output and both assertions fail.
Gate the two tests to target_os="linux" where the coupling holds; correct the
prior "unix-only" comment. Linux color coverage is unchanged.
Refactor the release workflow into a build matrix (x86_64-unknown-linux-gnu +
aarch64-apple-darwin) that uploads per-triple artifacts, plus a publish job
that gathers them and creates the GitHub release. Builds stay hermetic (no
cargo cache) and every leg re-verifies the tag matches the workspace version;
the release is published only if all legs succeed, so a partial artifact set
can never ship. sha256 generation switches to shasum -a 256 for parity across
the ubuntu and macOS runners.
@plusky
plusky force-pushed the ci/workflow-hardening branch from 6d53e19 to c461013 Compare July 24, 2026 19:58
@plusky plusky added the ai-assisted Contains AI-assisted changes label Jul 24, 2026
@plusky plusky closed this Jul 24, 2026
@plusky
plusky deleted the ci/workflow-hardening branch July 25, 2026 18:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-assisted Contains AI-assisted changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants