ci: workflow hardening + macOS build & release binaries - #1
Closed
plusky wants to merge 17 commits into
Closed
Conversation
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
force-pushed
the
ci/workflow-hardening
branch
from
July 24, 2026 19:58
6d53e19 to
c461013
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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— newrust-macosjob onmacos-latest(aarch64-apple-darwin): clippy + full test suite. Validatedgreen in CI.
test(cli): gate Linux-coupled stderr-color tests to Linux— twostderr-color tests assert on a DEBUG record only the Linux/BSD CA-file path
of
rustls-platform-verifieremits ("Loaded N CA root certificates"); itis 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— releaseworkflow refactored into a build matrix (
x86_64-unknown-linux-gnu+aarch64-apple-darwin) that uploads per-triple artifacts, plus a publish jobthat 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 256for ubuntu/macOS parity.Also on this branch
Earlier hardening/perf work already present over
master: workflow credentialhardening + caching + a beta early-warning leg, ClusterFuzzLite fuzzing, action
SHA pinning, dependency bumps, and the P1 resource-limit / bounded-fan-out
work.
Testing
rust-macos): green — clippy + all tests pass on Apple Silicon.release.ymlis tag-triggered; not exercised end-to-end (that cuts a realrelease). Statically validated; its
cargo build --releaseis the samecommand proven to compile on the macOS runner. First runs on the next tag.