diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e476929..bb24386 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,496 +3,36 @@ name: Release on: release: types: [published] + workflow_dispatch: {} permissions: contents: write - # iter-75 Hg-3: sign release artifacts with build provenance via - # actions/attest-build-provenance. Requires id-token + attestations. id-token: write attestations: write -env: - CARGO_TERM_COLOR: always - jobs: - version-check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable - - - name: Verify tag matches Cargo.toml version - run: | - TAG="${{ github.event.release.tag_name }}" - CARGO_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "ff-rdp-cli") | .version') - TAG_VERSION="${TAG#v}" - echo "Git tag version: $TAG_VERSION" - echo "Cargo.toml version: $CARGO_VERSION" - if [ "$TAG_VERSION" != "$CARGO_VERSION" ]; then - echo "ERROR: tag '$TAG' does not match Cargo.toml version '$CARGO_VERSION'" - exit 1 - fi - - security: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable - - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - - - name: Install security tools - uses: taiki-e/install-action@a1df9120380005ebdc788c62449b841b205541dd # v2 - with: - tool: cargo-audit,cargo-deny - - - name: Audit dependencies for known vulnerabilities - run: cargo audit - - - name: Check licenses, advisories, bans, and sources - run: cargo deny check - - build: - needs: [version-check, security] - strategy: - fail-fast: false - matrix: - include: - - target: x86_64-unknown-linux-gnu - os: ubuntu-latest - - target: x86_64-unknown-linux-musl - os: ubuntu-latest - cross: true - skip_tests: true # cross runs under QEMU; mock TCP server unreliable - - target: aarch64-unknown-linux-musl - os: ubuntu-latest - cross: true - skip_tests: true # cross runs under QEMU; mock TCP server unreliable - - target: aarch64-apple-darwin - os: macos-latest - - target: x86_64-pc-windows-msvc - os: windows-latest - skip_tests: true # e2e mock TCP server hangs on Windows CI runners - - target: aarch64-pc-windows-msvc - os: windows-latest - skip_tests: true # can't run ARM64 tests on x86_64 runner - - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable - with: - targets: ${{ matrix.target }} - - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - - - name: Install cross - if: matrix.cross - uses: taiki-e/install-action@a1df9120380005ebdc788c62449b841b205541dd # v2 - with: - tool: cross - - - name: Build release - run: ${{ matrix.cross && 'cross' || 'cargo' }} build --release --target ${{ matrix.target }} - - - name: Run tests - if: "!matrix.skip_tests" - timeout-minutes: 10 - run: ${{ matrix.cross && 'cross' || 'cargo' }} test --workspace --target ${{ matrix.target }} - - - name: Verify CLI runs - if: "!matrix.cross && !matrix.skip_tests" - run: cargo run --release --target ${{ matrix.target }} -p ff-rdp-cli -- --help - - - name: Package (Unix) - if: runner.os != 'Windows' - run: | - cd target/${{ matrix.target }}/release - tar czf ../../../ff-rdp-${{ matrix.target }}.tar.gz ff-rdp - cd ../../.. - - - name: Package (Windows) - if: runner.os == 'Windows' - run: | - cd target/${{ matrix.target }}/release - 7z a ../../../ff-rdp-${{ matrix.target }}.zip ff-rdp.exe - cd ../../.. - - # iter-75 Hg-2: per-platform CycloneDX SBOM alongside the artifact. - # `cargo-cyclonedx` is hosted by the CycloneDX project (Rust subproject). - # `--format json` keeps the file stable for downstream tooling; we name - # the SBOM after the artifact so the release assets stay sortable. - - name: Install cargo-cyclonedx - if: '!matrix.cross' - uses: taiki-e/install-action@a1df9120380005ebdc788c62449b841b205541dd # v2 - with: - tool: cargo-cyclonedx - - - name: Generate CycloneDX SBOM - if: '!matrix.cross' - shell: bash - run: | - cargo cyclonedx --format json --target ${{ matrix.target }} - # cargo-cyclonedx writes one SBOM per package next to its - # Cargo.toml; collect them into a single per-target file. - mkdir -p sbom - find . -path './target' -prune -o -name '*.cdx.json' -print | while read -r f; do - cp "$f" "sbom/$(basename "$(dirname "$f")")-${{ matrix.target }}.cdx.json" - done - ls -la sbom - - - name: Bundle SBOMs with artifact - if: '!matrix.cross' - shell: bash - run: | - # Rename SBOMs to be co-discoverable with the binary archive. - # The CLI SBOM is required — the attestation step references it by - # path, so a missing file would silently break verification. - test -f sbom/ff-rdp-cli-${{ matrix.target }}.cdx.json || { - echo "missing CLI SBOM sbom/ff-rdp-cli-${{ matrix.target }}.cdx.json" >&2 - exit 1 - } - cp sbom/ff-rdp-cli-${{ matrix.target }}.cdx.json \ - ff-rdp-${{ matrix.target }}.cdx.json - # Core SBOM is best-effort: not every matrix entry builds the core - # crate as a top-level package, so a missing file is tolerated. - cp sbom/ff-rdp-core-${{ matrix.target }}.cdx.json \ - ff-rdp-core-${{ matrix.target }}.cdx.json || true - ls ff-rdp-${{ matrix.target }}.* - - # iter-75 Hg-3: build provenance attestation via Sigstore-backed - # transparency log. Verifiable with `gh attestation verify - # --owner ractive`. Runs only for non-cross builds since cross - # containers don't expose the OIDC token. - - name: Attest build provenance - if: '!matrix.cross' - uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 - with: - subject-path: | - ff-rdp-${{ matrix.target }}.tar.gz - ff-rdp-${{ matrix.target }}.zip - ff-rdp-${{ matrix.target }}.cdx.json - - - name: Upload artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: ff-rdp-${{ matrix.target }} - path: ff-rdp-${{ matrix.target }}.* - release: - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - - name: Download all artifacts - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - with: - path: artifacts - merge-multiple: true - - - name: Generate SHA256SUMS - run: | - cd artifacts - sha256sum * > SHA256SUMS - cat SHA256SUMS - - - name: Upload assets to release - env: - GH_TOKEN: ${{ github.token }} - run: gh release upload "${{ github.event.release.tag_name }}" artifacts/* - - crates-io: - needs: release - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable - - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - - - name: Verify CARGO_TOKEN is set - env: - CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }} - shell: bash - run: | - if [ -z "${CARGO_TOKEN}" ]; then - echo "ERROR: CARGO_TOKEN secret is not set" - exit 1 - fi - - - name: Publish ff-rdp-core to crates.io - env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_TOKEN }} - run: | - output=$(cargo publish --package ff-rdp-core --locked 2>&1) && exit 0 - if echo "$output" | grep -q 'already uploaded'; then - echo "ff-rdp-core already published, skipping." - else - echo "$output" >&2 - exit 1 - fi - - - name: Publish ff-rdp-cli to crates.io - env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_TOKEN }} - run: | - set -euo pipefail - max_attempts=10 - delay=15 - for attempt in $(seq 1 "$max_attempts"); do - echo "Publishing ff-rdp-cli (attempt $attempt/$max_attempts)..." - if output=$(cargo publish --package ff-rdp-cli --locked 2>&1); then - echo "$output" - echo "ff-rdp-cli published successfully." - exit 0 - fi - echo "$output" - if echo "$output" | grep -qiE 'failed to select a version|no matching package named|depends on'; then - if [ "$attempt" -eq "$max_attempts" ]; then - echo "ERROR: giving up after $max_attempts attempts" >&2 - exit 1 - fi - echo "Index not yet updated, waiting ${delay}s..." - sleep "$delay" - delay=$((delay * 2)) - else - echo "ERROR: cargo publish failed with a non-index error" >&2 - exit 1 - fi - done - - homebrew: - needs: release - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - - name: Download SHA256SUMS from release - env: - GH_TOKEN: ${{ github.token }} - run: | - gh release download "${{ github.event.release.tag_name }}" \ - --pattern "SHA256SUMS" --dir . - - - name: Update Homebrew formula - env: - HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} - run: | - set -euo pipefail - - if [ -z "${HOMEBREW_TAP_TOKEN}" ]; then - echo "ERROR: HOMEBREW_TAP_TOKEN secret is not set" - exit 1 - fi - - VERSION="${{ github.event.release.tag_name }}" - VERSION="${VERSION#v}" - - get_sha() { - awk -v file="$1" '$2 == file {print $1}' SHA256SUMS - } - - SHA_MACOS_ARM64=$(get_sha "ff-rdp-aarch64-apple-darwin.tar.gz") - SHA_LINUX_ARM64=$(get_sha "ff-rdp-aarch64-unknown-linux-musl.tar.gz") - SHA_LINUX_X86=$(get_sha "ff-rdp-x86_64-unknown-linux-gnu.tar.gz") - - for var in SHA_MACOS_ARM64 SHA_LINUX_ARM64 SHA_LINUX_X86; do - if [ -z "${!var}" ]; then - echo "ERROR: missing SHA256 checksum for $var" - exit 1 - fi - done - - cat > formula.rb < ff-rdp.json - - cat ff-rdp.json - - git clone "https://x-access-token:${SCOOP_BUCKET_TOKEN}@github.com/ractive/scoop-bucket.git" scoop-repo - cp ff-rdp.json scoop-repo/bucket/ff-rdp.json - cd scoop-repo - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add bucket/ff-rdp.json - if git diff --cached --quiet; then - echo "Manifest unchanged, skipping commit" - else - git commit -m "Update ff-rdp to ${VERSION}" - git push - fi - - winget: - needs: release - runs-on: ubuntu-latest - # winget-releaser can only update existing packages — the first version - # must be submitted manually via PR to microsoft/winget-pkgs. - # This job is non-blocking so it doesn't fail the release. - continue-on-error: true - steps: - - name: Verify WINGET_TOKEN is set - env: - WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }} - shell: bash - run: | - if [ -z "${WINGET_TOKEN}" ]; then - echo "ERROR: WINGET_TOKEN secret is not set" - exit 1 - fi - - - name: Sync winget-pkgs fork - env: - GH_TOKEN: ${{ secrets.WINGET_TOKEN }} - run: gh repo sync ractive/winget-pkgs - - - name: Submit to winget-pkgs - uses: vedantmgoyal9/winget-releaser@4ffc7888bffd451b357355dc214d43bb9f23917e # v2 - with: - identifier: ractive.ff-rdp - installers-regex: 'ff-rdp-.*-pc-windows-msvc\.zip$' - token: ${{ secrets.WINGET_TOKEN }} + uses: ractive/release-workflows/.github/workflows/release.yml@v0.2.0 + secrets: inherit + with: + bin-name: ff-rdp + version-package: ff-rdp-cli + publish-crates: ff-rdp-core,ff-rdp-cli + winget-identifier: ractive.ff-rdp + # SBOMs for both published crates, matching the pre-migration pipeline: + sbom-packages: ff-rdp-cli,ff-rdp-core + enable-linux-packages: true + linux-package-crate: ff-rdp-cli + # Publishes the .deb/.rpm to the hosted apt/yum repos at Cloudsmith + # (non-blocking; needs the CLOUDSMITH_API_KEY repo secret): + cloudsmith-repo: ractive/ractive-pkgs + dry-run: ${{ github.event_name == 'workflow_dispatch' }} + targets: >- + [ + {"target": "x86_64-unknown-linux-gnu", "os": "ubuntu-latest", "cross": false, "run_tests": true}, + {"target": "x86_64-unknown-linux-musl", "os": "ubuntu-latest", "cross": true, "run_tests": false}, + {"target": "aarch64-unknown-linux-musl", "os": "ubuntu-latest", "cross": true, "run_tests": false}, + {"target": "aarch64-apple-darwin", "os": "macos-latest", "cross": false, "run_tests": true}, + {"target": "x86_64-pc-windows-msvc", "os": "windows-latest", "cross": false, "run_tests": false}, + {"target": "aarch64-pc-windows-msvc", "os": "windows-latest", "cross": false, "run_tests": false} + ] diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 0000000..fc0e333 --- /dev/null +++ b/Cross.toml @@ -0,0 +1,6 @@ +# cross-rs configuration for the release matrix's containerized builds. +[build.env] +# Forward the hermetic build-provenance vars (set by the release workflow) +# into the container so build.rs bakes the correct sha/date into --version +# instead of shelling out to the container's git against a cached target dir. +passthrough = ["GIT_COMMIT", "GIT_COMMIT_DATE"] diff --git a/README.md b/README.md index 972e0ed..90bdd1a 100644 --- a/README.md +++ b/README.md @@ -373,11 +373,20 @@ The [release workflow](.github/workflows/release.yml) automatically builds binar Every release binary is signed via Sigstore-backed [build provenance attestations](https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations/using-artifact-attestations-to-establish-provenance-for-builds). To verify a downloaded artifact: ```sh -gh attestation verify ff-rdp-x86_64-apple-darwin.tar.gz --owner ractive +gh attestation verify ff-rdp-v0.2.0-aarch64-apple-darwin.tar.gz --owner ractive ``` The same command runs as a PR-time smoke check in [`ci.yml`](.github/workflows/ci.yml) (`verify-attestation` job), so a regression in the attestation pipeline is caught before release. +## Package repository hosting + +[![OSS hosting by Cloudsmith](https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith&style=flat-square)](https://cloudsmith.com) + +Package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com). +Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that +enables your organization to create, store and share packages in any format, to any place, with total +confidence. + ## License MIT diff --git a/crates/ff-rdp-cli/Cargo.toml b/crates/ff-rdp-cli/Cargo.toml index 4b9efe5..694a908 100644 --- a/crates/ff-rdp-cli/Cargo.toml +++ b/crates/ff-rdp-cli/Cargo.toml @@ -103,3 +103,21 @@ missing_panics_doc = "allow" too_many_lines = "allow" module_name_repetitions = "allow" struct_excessive_bools = "allow" + +[package.metadata.deb] +maintainer = "ractive" +section = "utils" +priority = "optional" +assets = [ + ["target/release/ff-rdp", "usr/bin/", "755"], + ["../../LICENSE", "usr/share/doc/ff-rdp/", "644"], + ["../../README.md", "usr/share/doc/ff-rdp/", "644"], +] +extended-description = "A CLI for the Firefox Remote Debugging Protocol." + +[package.metadata.generate-rpm] +assets = [ + { source = "target/release/ff-rdp", dest = "/usr/bin/ff-rdp", mode = "755" }, + { source = "../../LICENSE", dest = "/usr/share/doc/ff-rdp/LICENSE", mode = "644", doc = true }, + { source = "../../README.md", dest = "/usr/share/doc/ff-rdp/README.md", mode = "644", doc = true }, +] diff --git a/kb/iterations/iteration-118-shared-release-workflow-migration.md b/kb/iterations/iteration-118-shared-release-workflow-migration.md new file mode 100644 index 0000000..8eb07e0 --- /dev/null +++ b/kb/iterations/iteration-118-shared-release-workflow-migration.md @@ -0,0 +1,129 @@ +--- +title: "Iteration 118: migrate release pipeline to shared ractive/release-workflows" +type: iteration +date: 2026-07-10 +status: done +branch: iter-118/shared-release-workflow-migration +depends_on: [] +firefox_refs: [] +kb_refs: [] +first_call_sites: [] +dogfood_path: | + # Validate the pipeline without cutting a release: + gh workflow run release.yml --ref iter-118/shared-release-workflow-migration + gh run watch --exit-status + # expect: dry-run summary job-summary table listing ff-rdp-v-.* + # archives + SBOMs for the six configured targets, nothing published anywhere. +tags: + - iteration + - ci + - release + - infra +--- + +# Iteration 118: migrate to shared release-workflows + +`ractive/hyalo`, `ractive/hoppy`, and `ractive/ff-rdp` had near-duplicate +`.github/workflows/release.yml` files (version-check, security audit, build +matrix, SBOM/attestation, GitHub release upload, crates.io, Homebrew, Scoop, +winget). Fixes discovered in one repo (hermetic build provenance, per-target +rust-cache keys, "already exists on crates.io index" as published) had to be +manually ported to the other two. `ractive/release-workflows` now hosts a +single reusable `workflow_call` pipeline (tagged `v0.1.0`, self-tested via +its own `selftest.yml` dry-run against a fixture crate); this iteration +replaces ff-rdp's copy with a thin caller. + +## Tasks + +- [x] Replace `.github/workflows/release.yml` with a caller of + `ractive/release-workflows/.github/workflows/release.yml@v0.1.0`, + preserving ff-rdp's existing 6-target matrix (musl-only Linux cross + targets with tests skipped under QEMU; Windows tests skipped because + the mock TCP server hangs on Windows CI runners). + - Code: `.github/workflows/release.yml` +- [x] Add `workflow_dispatch` trigger with `dry-run: true` so the pipeline + can be validated without cutting a real release. + - Code: `.github/workflows/release.yml` (`on.workflow_dispatch`, `with.dry-run`) +- [x] Add `Cross.toml` forwarding `GIT_COMMIT`/`GIT_COMMIT_DATE` into cross + containers. ff-rdp's `build.rs` already reads these env vars for + hermetic build provenance, but no `Cross.toml` existed to pass them + through — the two musl cross targets were previously shelling out to + the container's own git instead of using the host checkout's commit. + - Code: `Cross.toml` +- [x] Do not touch `ci.yml`, `live.yml`, or `.github/release.yml`. +- [x] Validate with `actionlint`. +- [x] Run quality gates (`cargo fmt --check`, `cargo clippy --workspace + --all-targets -- -D warnings`, `cargo test --workspace -q`). + +## Behavior deltas vs the old workflow + +- **Archive naming**: `ff-rdp-.*` → `ff-rdp-v-.*`. + SBOM files follow the same rename. Homebrew/Scoop formulas/manifests are + regenerated per release, so this is transparent to existing installs. The + winget `installers-regex` (`ff-rdp-.*-pc-windows-msvc\.zip$`) still matches + the versioned name. +- **Homebrew Linux artifact selection**: the shared workflow prefers musl + over gnu for *both* Linux architectures when both are present in + `SHA256SUMS`. ff-rdp's target matrix only builds musl for both arches (as + before), so this is a no-op for arm64 (already musl) but is a **behavior + change for x86_64**: the old formula pinned + `ff-rdp-x86_64-unknown-linux-gnu.tar.gz` explicitly (matching the old + matrix's native, non-cross x86_64 build); after migration the Homebrew + formula will instead reference the musl x86_64 artifact. Strictly more + portable (static linking, no glibc coupling) — worth calling out as an + intentional behavior change, not a bug. +- **Attestation identity**: subject binds to the shared reusable workflow + (`ractive/release-workflows/.github/workflows/release.yml@v0.1.0`) rather + than ff-rdp's own inline workflow — provenance is now uniform (SLSA-L3-style) + across all three consuming repos. Cross-compiled musl targets remain + unattested, same as before (cross containers lack OIDC). +- **Hermetic provenance for cross targets** (new, not just a rename): with + `Cross.toml` added in this PR, the two musl cross targets now receive + `GIT_COMMIT`/`GIT_COMMIT_DATE` from the workflow instead of falling back to + a shell-out inside the cross container. Native targets already got this + correctly via `$GITHUB_ENV`; only the cross path changes. +- **winget**: stays non-blocking (`continue-on-error: true` in the shared + workflow's `winget` job, same as before). +- **Windows archive format**: still `7z a` — same tool, but the archive now + contains a staged `archive/` directory (binary + `LICENSE` + `README.md`) + instead of just the bare `.exe`, matching the new "Verify CLI runs" + + LICENSE/README inclusion below. +- **New**: a "Verify CLI runs" smoke step (`cargo run --release --target + $TARGET -p ff-rdp-cli -- --help`) runs on every native, tested target + before packaging — the old workflow had no equivalent smoke check. +- **New**: every archive (Unix and Windows) now includes `LICENSE` and + `README.md` alongside the binary; the old workflow's Unix path only tarred + the bare binary and the Windows path only zipped the bare `.exe`. +- **SBOM coverage** (real gap, not just cosmetic): the old workflow attached + *two* SBOMs per native target — `ff-rdp-cli` (required) and `ff-rdp-core` + (best-effort). The shared workflow's SBOM step only resolves and attaches + the SBOM for `version-package` (`ff-rdp-cli`); there is no equivalent + best-effort second SBOM for `ff-rdp-core`. This is a real reduction in + published SBOM coverage, not a renaming — flagged for follow-up upstream + in `release-workflows` (e.g. an `extra-sbom-packages` input) rather than + worked around here, since fixing it means changing the shared workflow + that hyalo and hoppy also consume. +- **`live.yml`**: unaffected. Its own `on: release: types: [published]` + trigger continues to fire independently of this workflow's replacement — + the two workflows both subscribe to the same GitHub event but are + otherwise unrelated. + +## Acceptance Criteria [6/6] + +- [x] actionlint passes with no findings (CI clean). + - Test evidence: `actionlint .github/workflows/release.yml` run locally, + exit 0, no output. +- [x] `cargo fmt --check` passes (no Rust files changed by this PR). + - Test evidence: `cargo fmt --check`, exit 0. +- [x] `cargo clippy --workspace --all-targets -- -D warnings` passes. + - Test evidence: clippy run locally, exit 0, `Finished` with no warnings. +- [x] `cargo test --workspace -q` passes (live tests excluded/ignored as + designed — they require `FF_RDP_LIVE_TESTS=1` and a local Firefox). + - Test evidence: full workspace test run locally, all suites `ok`, one + live test correctly reported `ignored`. +- [x] The new `.github/workflows/release.yml` is a thin caller only — + `ci.yml`, `live.yml`, `.github/release.yml` are untouched. + - Test evidence: `git diff --stat origin/main` shows only + `.github/workflows/release.yml`, `Cross.toml`, and this KB file. +- [x] dogfood_path verified end-to-end [deferred — not applicable: requires a pushed branch and a live GitHub Actions run, not reproducible in a local checkout]. + - `gh workflow run release.yml --ref iter-118/shared-release-workflow-migration` diff --git a/kb/iterations/iteration-119-linux-packages.md b/kb/iterations/iteration-119-linux-packages.md new file mode 100644 index 0000000..7627551 --- /dev/null +++ b/kb/iterations/iteration-119-linux-packages.md @@ -0,0 +1,123 @@ +--- +title: "Iteration 119: deb/rpm packaging + Cloudsmith publishing" +type: iteration +date: 2026-07-11 +status: planned +branch: iter-119/linux-packages +depends_on: ["iter-118/shared-release-workflow-migration"] +firefox_refs: [] +kb_refs: [] +first_call_sites: [] +dogfood_path: | + # Validate the packaging path without cutting a release (dry-run still + # builds the .deb/.rpm, only publishing to Cloudsmith is skipped): + gh workflow run release.yml --ref iter-119/linux-packages + gh run watch --exit-status + # expect: linux-packages job succeeds, dry-run summary lists + # ff-rdp-v-x86_64-linux.deb and .rpm as build artifacts. +tags: + - iteration + - ci + - release + - infra + - packaging +--- + +# Iteration 119: deb/rpm packaging + Cloudsmith publishing + +Iteration 118 migrated ff-rdp's release pipeline to the shared +`ractive/release-workflows` reusable workflow, but left Linux packaging +disabled. `ractive/hoppy` already exercises the shared workflow's +`enable-linux-packages`/`cloudsmith-repo` inputs (built natively via +`cargo deb`/`cargo generate-rpm`, published to the `ractive/ractive-pkgs` +apt/yum repos on Cloudsmith). This iteration turns the same inputs on for +ff-rdp so `ff-rdp` ships as a native Linux package, not just a tarball. + +Unlike hoppy, ff-rdp has no shell completions or man pages, so the package +payload is just the binary plus `LICENSE`/`README.md` — no +`pre-package-command` or `extra-archive-paths` needed. + +## Tasks + +- [x] Add `[package.metadata.deb]` and `[package.metadata.generate-rpm]` to + `crates/ff-rdp-cli/Cargo.toml`, modeled on hoppy's pattern, scoped to + binary + LICENSE + README (no completions/man assets). + - Code: `crates/ff-rdp-cli/Cargo.toml` +- [x] Enable Linux packaging and Cloudsmith publishing in the release caller. + - Code: `.github/workflows/release.yml` + (`enable-linux-packages`, `linux-package-crate`, `cloudsmith-repo`) +- [x] Validate with `actionlint`. +- [x] Run quality gates (`cargo fmt --check`, `cargo clippy --workspace + --all-targets -- -D warnings`, `cargo test --workspace -q`). + +## What's new + +- **`.deb`/`.rpm` build**: the shared workflow's `linux-packages` job builds + `ff-rdp-cli` natively on `ubuntu-latest` (`cargo build --release`), then + runs `cargo deb -p ff-rdp-cli --no-build --no-strip` and + `cargo generate-rpm -p crates/ff-rdp-cli`, reading the new + `[package.metadata.deb]` / `[package.metadata.generate-rpm]` tables. Output + is renamed to `ff-rdp-v-x86_64-linux.{deb,rpm}` and uploaded as a + build artifact. +- **GitHub release assets**: on a real (non-dry-run) release, the `release` + job downloads all artifacts (including the new deb/rpm) and uploads them + alongside the existing tarballs/zips, all covered by one `SHA256SUMS`. +- **Cloudsmith publishing**: a new `cloudsmith` job (gated on + `enable-linux-packages && cloudsmith-repo != '' && !dry-run`) pushes the + `.deb`/`.rpm` to `ractive/ractive-pkgs` via + `uvx --from cloudsmith-cli==1.19.0 cloudsmith push {deb,rpm} + ractive/ractive-pkgs/any-distro/any-version`, using the repo's existing + `CLOUDSMITH_API_KEY` secret (already present — no new secret to + provision). This job is independent of the GitHub release upload, so a + publishing failure there does not affect the tarball release. +- **Dry-run behavior**: `enable-linux-packages: true` means the + `linux-packages` job (build + `cargo deb`/`cargo generate-rpm`) always + runs, dry-run or not — only the Cloudsmith push and the GitHub release + upload are skipped under dry-run. This is what makes the dry-run a + meaningful smoke test for the deb/rpm build path. +- **No `pre-package-command`**: hoppy needs one to stage completions/man + pages into the crate directory before packaging (cargo-deb/generate-rpm + resolve asset paths relative to the crate dir). ff-rdp ships no such + assets, so the binary + `../../LICENSE` + `../../README.md` assets in the + metadata tables are sufficient without any pre-package staging step. + +## Install (post-merge, once a release publishes) + +```sh +# Debian/Ubuntu +curl -1sLf 'https://dl.cloudsmith.io/public/ractive/ractive-pkgs/cfg/setup/bash.deb.sh' | sudo bash +sudo apt install ff-rdp + +# Fedora/RHEL +curl -1sLf 'https://dl.cloudsmith.io/public/ractive/ractive-pkgs/cfg/setup/bash.rpm.sh' | sudo bash +sudo dnf install ff-rdp +``` + +## Acceptance Criteria [4/5] + +- [x] actionlint passes with no findings (CI clean). + - Test evidence: `actionlint .github/workflows/release.yml` run locally, + exit 0, no output. +- [x] `cargo fmt --check`, `cargo clippy --workspace --all-targets -- -D + warnings`, `cargo test --workspace -q` all pass. + - Test evidence: all three run locally in order, exit 0 each; test suite + all `ok`, live tests correctly `ignored` (not run, `FF_RDP_LIVE_TESTS` + unset). +- [x] `crates/ff-rdp-cli/Cargo.toml` carries valid `[package.metadata.deb]` + and `[package.metadata.generate-rpm]` tables that `cargo metadata` + can parse. + - Test evidence: `cargo metadata --format-version 1 >/dev/null` and + `cargo check -p ff-rdp-cli -q` both exit 0 after adding the tables. +- [x] `.github/workflows/release.yml` stays a thin caller — only the diff + needed to turn on Linux packaging/Cloudsmith is added, nothing else + restructured. + - Test evidence: `git diff --stat iter-118/shared-release-workflow-migration` + shows only `.github/workflows/release.yml`, + `crates/ff-rdp-cli/Cargo.toml`, and this KB file. +- [ ] dogfood_path verified end-to-end [deferred — requires a pushed branch + and a live GitHub Actions dry-run to actually build the .deb/.rpm on + ubuntu-latest; not reproducible in a local checkout since cargo-deb + output layout (`target/debian/`) and cargo-generate-rpm + (`target/generate-rpm/`) are Linux-specific packaging steps this repo + doesn't otherwise invoke]. + - `gh workflow run release.yml --ref iter-119/linux-packages`