fix: guard stale and downgrade deployments (#8186) #5094
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
| # Continuous release pipeline for Homeboy. | |
| # | |
| # Triggers on push to main (and manual dispatch). Checks for releasable | |
| # conventional commits since the last tag. If found: | |
| # 1. Quality gate (audit, lint, test) | |
| # 2. Version bump + changelog generation from conventional commits | |
| # 3. Cross-platform binary builds via cargo-dist | |
| # 4. Publish to GitHub Releases, crates.io, and Homebrew | |
| # | |
| # No human input needed — version is computed from commit types: | |
| # fix: → patch, feat: → minor, BREAKING CHANGE → major | |
| # chore:/ci:/docs:/test: → no release | |
| name: Release | |
| permissions: | |
| actions: write | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| on: | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| inputs: | |
| dry-run: | |
| description: 'Preview the release without making changes' | |
| type: boolean | |
| default: false | |
| release_tag: | |
| description: 'Existing tag to publish/recover without preparing a new release' | |
| type: string | |
| default: '' | |
| release_blocking_commands: | |
| description: 'Comma-separated quality commands that may block release preparation' | |
| type: string | |
| default: 'review lint,review test' | |
| env: | |
| RELEASE_BLOCKING_COMMANDS: ${{ inputs.release_blocking_commands || 'review lint,review test' }} | |
| HOMEBOY_NO_UPDATE_CHECK: '1' | |
| RELEASE_MIN_FREE_KB: '5242880' | |
| # Only one release pipeline at a time. If a push arrives while a release | |
| # is already running, it queues (never cancels). The queued run starts | |
| # after the first finishes and its check job exits in seconds because | |
| # HEAD is already tagged — zero wasted work. | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| # ── Step 1: Check for releasable commits ── | |
| # Fast exit if nothing to release (e.g. chore-only commits). | |
| check: | |
| name: Check for releasable commits | |
| runs-on: ubuntu-latest | |
| outputs: | |
| should-release: ${{ steps.check.outputs.should-release }} | |
| bump-type: ${{ steps.check.outputs['release-bump-type'] }} | |
| recovery-release: ${{ steps.check.outputs.recovery-release }} | |
| release-version: ${{ steps.check.outputs['release-version'] }} | |
| release-tag: ${{ steps.check.outputs['release-tag'] }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ inputs.release_tag || github.ref }} | |
| fetch-depth: 0 | |
| - name: Restore failed release marker | |
| id: failure-cache | |
| if: inputs.release_tag == '' | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: ${{ runner.temp }}/homeboy-release-last-failed | |
| key: release-last-failed-${{ github.ref_name }}-${{ github.sha }} | |
| restore-keys: | | |
| release-last-failed-${{ github.ref_name }}- | |
| - name: Check failed release marker | |
| id: failed-release | |
| if: inputs.release_tag == '' | |
| run: | | |
| HEAD_SHA="$(git rev-parse HEAD)" | |
| FAILURE_MARKER="${RUNNER_TEMP}/homeboy-release-last-failed" | |
| if [ -f "${FAILURE_MARKER}" ]; then | |
| LAST_FAILED="$(tr -d '[:space:]' < "${FAILURE_MARKER}")" | |
| if [ "${HEAD_SHA}" = "${LAST_FAILED}" ]; then | |
| echo "::notice::HEAD ${HEAD_SHA:0:8} matches last failed release attempt — skipping until new commits" | |
| echo "blocked=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| fi | |
| echo "blocked=false" >> "$GITHUB_OUTPUT" | |
| - name: Dry-run release check | |
| id: release-check | |
| if: inputs.release_tag == '' && steps.failed-release.outputs.blocked != 'true' | |
| uses: Extra-Chill/homeboy-action@v2 | |
| with: | |
| commands: release | |
| expected-commands: review audit,review lint,review test | |
| args: --skip-checks=audit,lint,test | |
| release-dry-run: 'true' | |
| - name: Validate existing release tag | |
| id: recovery | |
| if: inputs.release_tag != '' | |
| run: | | |
| TAG="${{ inputs.release_tag }}" | |
| if ! git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then | |
| echo "::error::Release tag ${TAG} does not exist" | |
| exit 1 | |
| fi | |
| if [[ ! "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "::error::Release tag ${TAG} must look like v1.2.3" | |
| exit 1 | |
| fi | |
| VERSION="${TAG#v}" | |
| echo "release-version=${VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "release-tag=${TAG}" >> "$GITHUB_OUTPUT" | |
| echo "release-bump-type=recovery" >> "$GITHUB_OUTPUT" | |
| echo "::notice::Recovering publish pipeline for existing tag ${TAG}" | |
| - name: Decide whether to release | |
| id: check | |
| run: | | |
| HEAD_SHA="$(git rev-parse HEAD)" | |
| RECOVERY_TAG="${{ steps.recovery.outputs['release-tag'] }}" | |
| RELEASE_VERSION="${{ steps.recovery.outputs['release-version'] || steps.release-check.outputs['release-version'] }}" | |
| RELEASE_TAG="${{ steps.recovery.outputs['release-tag'] || steps.release-check.outputs['release-tag'] }}" | |
| BUMP_TYPE="${{ steps.recovery.outputs['release-bump-type'] || steps.release-check.outputs['release-bump-type'] }}" | |
| if [ "${{ steps.failed-release.outputs.blocked }}" = "true" ]; then | |
| echo "should-release=false" >> "$GITHUB_OUTPUT" | |
| elif [ -n "${RECOVERY_TAG}" ]; then | |
| echo "should-release=true" >> "$GITHUB_OUTPUT" | |
| echo "release-bump-type=${BUMP_TYPE}" >> "$GITHUB_OUTPUT" | |
| echo "release-version=${RELEASE_VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "release-tag=${RECOVERY_TAG}" >> "$GITHUB_OUTPUT" | |
| echo "recovery-release=true" >> "$GITHUB_OUTPUT" | |
| echo "::notice::Release recovery will publish ${RECOVERY_TAG}" | |
| elif [ -z "${RELEASE_VERSION}" ]; then | |
| echo "should-release=false" >> "$GITHUB_OUTPUT" | |
| echo "::notice::No releasable commits at HEAD ${HEAD_SHA:0:8}" | |
| else | |
| echo "should-release=true" >> "$GITHUB_OUTPUT" | |
| echo "release-bump-type=${BUMP_TYPE}" >> "$GITHUB_OUTPUT" | |
| echo "release-version=${RELEASE_VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "release-tag=${RELEASE_TAG}" >> "$GITHUB_OUTPUT" | |
| if [ "${BUMP_TYPE}" = "recovery" ]; then | |
| echo "recovery-release=true" >> "$GITHUB_OUTPUT" | |
| echo "::notice::Recovered prepared release tag ${RELEASE_TAG}; bypassing quality gates and publishing artifacts" | |
| else | |
| echo "recovery-release=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| echo "::notice::Release dry-run predicts v${RELEASE_VERSION} (${BUMP_TYPE})" | |
| fi | |
| # ── Step 2: Build once ── | |
| # Compile homeboy from source once and share the binary with all | |
| # quality gate jobs. Eliminates 3× redundant cargo builds. | |
| gate-build: | |
| name: Build | |
| needs: check | |
| if: needs.check.outputs.should-release == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ inputs.release_tag || github.ref }} | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache cargo | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: ${{ runner.os }}-cargo-release-gate-${{ hashFiles('Cargo.lock') }} | |
| restore-keys: ${{ runner.os }}-cargo-release-gate- | |
| - name: Build homeboy | |
| run: cargo build --release | |
| - name: Upload binary | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: homeboy-binary | |
| path: target/release/homeboy | |
| retention-days: 1 | |
| # ── Step 3: Quality checks (parallel read-only checks + advisory repair) ── | |
| # Release preparation is gated by release-quality-policy, not by raw job | |
| # failures. Commands opt in to release blocking through RELEASE_BLOCKING_COMMANDS | |
| # (default: lint,test). Audit still runs and files issues, but stale/full-tree | |
| # audit debt does not block releases unless audit is explicitly listed. | |
| gate-audit: | |
| name: Audit | |
| needs: | |
| - check | |
| - gate-build | |
| if: needs.check.outputs.should-release == 'true' && needs.check.outputs.recovery-release != 'true' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| audit-result: ${{ steps.audit.outcome }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ inputs.release_tag || github.ref }} | |
| fetch-depth: 0 | |
| - name: Download homeboy binary | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: homeboy-binary | |
| path: .homeboy-bin | |
| - name: Verify homeboy binary present | |
| run: | | |
| if [ ! -f .homeboy-bin/homeboy ]; then | |
| echo "::error::Build artifact missing from upstream Build job: .homeboy-bin/homeboy was not produced/uploaded by gate-build. This is a CI artifact-handoff problem, not a code finding in this change. Re-run the failed Build job or investigate the homeboy-binary upload step." >&2 | |
| exit 1 | |
| fi | |
| chmod +x .homeboy-bin/homeboy | |
| - name: Generate GitHub App token | |
| id: app-token | |
| uses: actions/create-github-app-token@v1 | |
| continue-on-error: true | |
| with: | |
| app-id: ${{ secrets.HOMEBOY_APP_ID }} | |
| private-key: ${{ secrets.HOMEBOY_APP_PRIVATE_KEY }} | |
| - name: Run advisory audit | |
| id: audit | |
| uses: Extra-Chill/homeboy-action@v2 | |
| continue-on-error: true | |
| with: | |
| binary-path: .homeboy-bin/homeboy | |
| commands: review audit | |
| expected-commands: review audit,review lint,review test | |
| args: ${{ github.event.before && format('--profile=pr --changed-since {0}', github.event.before) || '--profile=pr' }} | |
| autofix: 'false' | |
| app-token: ${{ steps.app-token.outputs.token || '' }} | |
| gate-lint: | |
| name: Lint | |
| needs: | |
| - check | |
| - gate-build | |
| if: needs.check.outputs.should-release == 'true' && needs.check.outputs.recovery-release != 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ inputs.release_tag || github.ref }} | |
| fetch-depth: 0 | |
| - name: Download homeboy binary | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: homeboy-binary | |
| path: .homeboy-bin | |
| - name: Verify homeboy binary present | |
| run: | | |
| if [ ! -f .homeboy-bin/homeboy ]; then | |
| echo "::error::Build artifact missing from upstream Build job: .homeboy-bin/homeboy was not produced/uploaded by gate-build. This is a CI artifact-handoff problem, not a lint finding in this change. Re-run the failed Build job or investigate the homeboy-binary upload step." >&2 | |
| exit 1 | |
| fi | |
| chmod +x .homeboy-bin/homeboy | |
| - name: Generate GitHub App token | |
| id: app-token | |
| uses: actions/create-github-app-token@v1 | |
| continue-on-error: true | |
| with: | |
| app-id: ${{ secrets.HOMEBOY_APP_ID }} | |
| private-key: ${{ secrets.HOMEBOY_APP_PRIVATE_KEY }} | |
| - uses: Extra-Chill/homeboy-action@v2 | |
| with: | |
| binary-path: .homeboy-bin/homeboy | |
| commands: review lint | |
| expected-commands: review audit,review lint,review test | |
| args: ${{ github.event.before && format('--changed-since {0}', github.event.before) || '' }} | |
| autofix: 'false' | |
| app-token: ${{ steps.app-token.outputs.token || '' }} | |
| gate-test: | |
| name: Test | |
| needs: | |
| - check | |
| - gate-build | |
| if: needs.check.outputs.should-release == 'true' && needs.check.outputs.recovery-release != 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ inputs.release_tag || github.ref }} | |
| fetch-depth: 0 | |
| - name: Download homeboy binary | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: homeboy-binary | |
| path: .homeboy-bin | |
| - name: Verify homeboy binary present | |
| run: | | |
| if [ ! -f .homeboy-bin/homeboy ]; then | |
| echo "::error::Build artifact missing from upstream Build job: .homeboy-bin/homeboy was not produced/uploaded by gate-build. This is a CI artifact-handoff problem, not a test finding in this change. Re-run the failed Build job or investigate the homeboy-binary upload step." >&2 | |
| exit 1 | |
| fi | |
| chmod +x .homeboy-bin/homeboy | |
| - name: Generate GitHub App token | |
| id: app-token | |
| uses: actions/create-github-app-token@v1 | |
| continue-on-error: true | |
| with: | |
| app-id: ${{ secrets.HOMEBOY_APP_ID }} | |
| private-key: ${{ secrets.HOMEBOY_APP_PRIVATE_KEY }} | |
| - uses: Extra-Chill/homeboy-action@v2 | |
| env: | |
| RELEASE_BLOCKING_COMMANDS: ${{ env.RELEASE_BLOCKING_COMMANDS }} | |
| with: | |
| binary-path: .homeboy-bin/homeboy | |
| commands: review test | |
| expected-commands: review audit,review lint,review test | |
| args: ${{ github.event.before && format('--skip-lint --changed-since {0}', github.event.before) || '--skip-lint' }} | |
| autofix: 'false' | |
| app-token: ${{ steps.app-token.outputs.token || '' }} | |
| # ── Step 3b: Release-blocking quality policy ── | |
| # Release preparation is gated by release-quality-policy, not by raw job | |
| # failures or generic source auto-refactor. Generic release auto-refactor / | |
| # autofix was removed (#8046): the release pipeline no longer mutates source, | |
| # opens autofix branches, or creates autofix PRs. Audit baseline consumption | |
| # (--changed-since against homeboy.json baselines.audit.known_fingerprints) | |
| # and automated categorized issue filing (app-token) are preserved in the | |
| # read-only gate jobs. Extension-declared generated-drift maintenance is | |
| # preserved as a narrow allowlisted transaction in core | |
| # (changes_are_only_drift / drift_file_paths), not as a release-workflow | |
| # source-mutation step. | |
| release-quality-policy: | |
| name: Release Quality Policy | |
| needs: | |
| - check | |
| - gate-build | |
| - gate-audit | |
| - gate-lint | |
| - gate-test | |
| if: ${{ always() && needs.check.outputs.should-release == 'true' && needs.check.outputs.recovery-release != 'true' && needs.gate-build.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout workflow event commit | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.sha }} | |
| - name: Enforce release-blocking commands | |
| env: | |
| BLOCKING_COMMANDS: ${{ env.RELEASE_BLOCKING_COMMANDS }} | |
| AUDIT_RESULT: ${{ needs.gate-audit.outputs.audit-result || needs.gate-audit.result }} | |
| LINT_RESULT: ${{ needs.gate-lint.result }} | |
| TEST_RESULT: ${{ needs.gate-test.result }} | |
| run: | | |
| bash .github/release-quality-policy.sh | |
| # ── Step 4: Version bump + changelog + tag ── | |
| prepare: | |
| name: Prepare Release | |
| needs: | |
| - check | |
| - gate-build | |
| - release-quality-policy | |
| if: ${{ always() && needs.check.outputs.should-release == 'true' && needs.gate-build.result == 'success' && (needs.check.outputs.recovery-release == 'true' || inputs.release_tag != '' || needs.release-quality-policy.result == 'success') }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| release-version: ${{ steps.outputs.outputs['release-version'] }} | |
| release-tag: ${{ steps.outputs.outputs['release-tag'] }} | |
| prepared: ${{ steps.outputs.outputs.prepared }} | |
| released: ${{ steps.outputs.outputs.released }} | |
| steps: | |
| - name: Generate GitHub App token | |
| id: app-token | |
| uses: actions/create-github-app-token@v1 | |
| continue-on-error: true | |
| with: | |
| app-id: ${{ secrets.HOMEBOY_APP_ID }} | |
| private-key: ${{ secrets.HOMEBOY_APP_PRIVATE_KEY }} | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ inputs.release_tag || github.ref }} | |
| fetch-depth: 0 | |
| persist-credentials: true | |
| token: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }} | |
| - name: Preflight release runner disk | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| report_disk() { | |
| echo "::group::Release runner disk usage" | |
| df -h . "$RUNNER_TEMP" "$HOME" || true | |
| echo "::endgroup::" | |
| } | |
| available_kb() { | |
| df -Pk . | awk 'NR==2 {print $4}' | |
| } | |
| report_disk | |
| before_kb="$(available_kb)" | |
| if [ "$before_kb" -lt "$RELEASE_MIN_FREE_KB" ]; then | |
| echo "::warning::Release runner has ${before_kb} KiB free before prepare; cleaning reconstructable release artifacts and caches" | |
| rm -rf target/distrib target/package .homeboy-bin artifacts "$HOME/.cache/cargo-dist" "$HOME/.cache/sccache" | |
| fi | |
| report_disk | |
| after_kb="$(available_kb)" | |
| if [ "$after_kb" -lt "$RELEASE_MIN_FREE_KB" ]; then | |
| echo "::error::Release runner has ${after_kb} KiB free after cleanup; refusing prepare before the runner exhausts disk while writing diagnostics" | |
| exit 1 | |
| fi | |
| # Rust toolchain needed so run-release.sh can regenerate Cargo.lock | |
| # after bumping Cargo.toml version | |
| - name: Install Rust toolchain | |
| if: inputs.release_tag == '' | |
| uses: dtolnay/rust-toolchain@stable | |
| - uses: Extra-Chill/homeboy-action@v2 | |
| id: release | |
| if: inputs.release_tag == '' && needs.check.outputs.recovery-release != 'true' | |
| with: | |
| source: '.' | |
| commands: release | |
| expected-commands: review audit,review lint,review test | |
| args: --skip-checks=audit,lint,test | |
| release-dry-run: ${{ inputs.dry-run || 'false' }} | |
| release-skip-publish: 'true' | |
| release-skip-github-release: 'true' | |
| app-token: ${{ steps.app-token.outputs.token || '' }} | |
| - name: Mark prepared release for downstream publish | |
| id: prepared | |
| if: inputs.release_tag == '' && needs.check.outputs.recovery-release != 'true' && steps.release.outputs['release-tag'] != '' | |
| run: | | |
| echo "prepared=true" >> "$GITHUB_OUTPUT" | |
| echo "::notice::Prepared ${{ steps.release.outputs['release-tag'] }}; downstream jobs will publish it in this run" | |
| - name: Use existing release tag | |
| id: recovery | |
| if: inputs.release_tag != '' || needs.check.outputs.recovery-release == 'true' | |
| run: | | |
| TAG="${{ inputs.release_tag || needs.check.outputs['release-tag'] }}" | |
| VERSION="${{ needs.check.outputs['release-version'] }}" | |
| if [ -z "${VERSION}" ]; then | |
| VERSION="${TAG#v}" | |
| fi | |
| echo "release-version=${VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "release-tag=${TAG}" >> "$GITHUB_OUTPUT" | |
| echo "prepared=true" >> "$GITHUB_OUTPUT" | |
| echo "released=true" >> "$GITHUB_OUTPUT" | |
| echo "::notice::Skipping release preparation; downstream jobs will publish existing tag ${TAG}" | |
| - name: Resolve release outputs | |
| id: outputs | |
| run: | | |
| RELEASE_VERSION="${{ steps.recovery.outputs['release-version'] || steps.release.outputs['release-version'] }}" | |
| RELEASE_TAG="${{ steps.recovery.outputs['release-tag'] || steps.release.outputs['release-tag'] }}" | |
| PREPARED="${{ steps.recovery.outputs.prepared || steps.prepared.outputs.prepared }}" | |
| RELEASED="${{ steps.recovery.outputs.released || steps.release.outputs.released }}" | |
| echo "release-version=${RELEASE_VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "release-tag=${RELEASE_TAG}" >> "$GITHUB_OUTPUT" | |
| echo "prepared=${PREPARED}" >> "$GITHUB_OUTPUT" | |
| echo "released=${RELEASED}" >> "$GITHUB_OUTPUT" | |
| # ── Step 4: Build cross-platform binaries ── | |
| plan: | |
| name: Plan Build Matrix | |
| needs: prepare | |
| if: needs.prepare.outputs.prepared == 'true' && needs.prepare.outputs['release-tag'] != '' | |
| runs-on: ubuntu-22.04 | |
| outputs: | |
| val: ${{ steps.plan.outputs.manifest }} | |
| tag-flag: ${{ format('--tag={0}', needs.prepare.outputs['release-tag']) }} | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ needs.prepare.outputs['release-tag'] }} | |
| persist-credentials: false | |
| submodules: recursive | |
| - name: Install dist | |
| shell: bash | |
| run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.31.0/cargo-dist-installer.sh | sh" | |
| - name: Cache dist | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: cargo-dist-cache | |
| path: ~/.cargo/bin/dist | |
| - id: plan | |
| run: | | |
| dist host --steps=create --tag=${{ needs.prepare.outputs['release-tag'] }} --output-format=json > plan-dist-manifest.json | |
| echo "dist ran successfully" | |
| cat plan-dist-manifest.json | |
| echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" | |
| - name: Upload dist-manifest.json | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: artifacts-plan-dist-manifest | |
| path: plan-dist-manifest.json | |
| build-local-artifacts: | |
| name: build (${{ join(matrix.targets, ', ') }}) | |
| needs: | |
| - prepare | |
| - plan | |
| if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null }} | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} | |
| runs-on: ${{ matrix.runner }} | |
| container: ${{ matrix.container && matrix.container.image || null }} | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json | |
| steps: | |
| - name: enable windows longpaths | |
| run: | | |
| git config --global core.longpaths true | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ needs.prepare.outputs['release-tag'] }} | |
| persist-credentials: false | |
| submodules: recursive | |
| - name: Install Rust non-interactively if not already installed | |
| if: ${{ matrix.container }} | |
| run: | | |
| if ! command -v cargo > /dev/null 2>&1; then | |
| curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y | |
| echo "$HOME/.cargo/bin" >> $GITHUB_PATH | |
| fi | |
| - name: Install dist | |
| run: ${{ matrix.install_dist.run }} | |
| - name: Fetch local artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: artifacts-* | |
| path: target/distrib/ | |
| merge-multiple: true | |
| - name: Install dependencies | |
| run: | | |
| ${{ matrix.packages_install }} | |
| - name: Build artifacts | |
| run: | | |
| dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json | |
| echo "dist ran successfully" | |
| - id: cargo-dist | |
| name: Post-build | |
| shell: bash | |
| run: | | |
| echo "paths<<EOF" >> "$GITHUB_OUTPUT" | |
| dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT" | |
| echo "EOF" >> "$GITHUB_OUTPUT" | |
| cp dist-manifest.json "$BUILD_MANIFEST_NAME" | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: artifacts-build-local-${{ join(matrix.targets, '_') }} | |
| path: | | |
| ${{ steps.cargo-dist.outputs.paths }} | |
| ${{ env.BUILD_MANIFEST_NAME }} | |
| build-global-artifacts: | |
| needs: | |
| - prepare | |
| - plan | |
| - build-local-artifacts | |
| runs-on: ubuntu-22.04 | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ needs.prepare.outputs['release-tag'] }} | |
| persist-credentials: false | |
| submodules: recursive | |
| - name: Preflight release publisher disk | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| report_disk() { | |
| echo "::group::Release publisher disk usage" | |
| df -h . "$RUNNER_TEMP" "$HOME" || true | |
| echo "::endgroup::" | |
| } | |
| available_kb() { | |
| df -Pk . | awk 'NR==2 {print $4}' | |
| } | |
| report_disk | |
| before_kb="$(available_kb)" | |
| if [ "$before_kb" -lt "$RELEASE_MIN_FREE_KB" ]; then | |
| echo "::warning::Release publisher has ${before_kb} KiB free before publish; cleaning reconstructable release artifacts and caches" | |
| rm -rf target/distrib target/package .homeboy-bin artifacts "$HOME/.cache/cargo-dist" "$HOME/.cache/sccache" | |
| fi | |
| report_disk | |
| after_kb="$(available_kb)" | |
| if [ "$after_kb" -lt "$RELEASE_MIN_FREE_KB" ]; then | |
| echo "::error::Release publisher has ${after_kb} KiB free after cleanup; refusing publish before the runner exhausts disk while writing diagnostics" | |
| exit 1 | |
| fi | |
| - name: Install cached dist | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: cargo-dist-cache | |
| path: ~/.cargo/bin/ | |
| - run: chmod +x ~/.cargo/bin/dist | |
| - name: Fetch local artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: artifacts-* | |
| path: target/distrib/ | |
| merge-multiple: true | |
| - id: cargo-dist | |
| shell: bash | |
| run: | | |
| dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json | |
| echo "dist ran successfully" | |
| echo "paths<<EOF" >> "$GITHUB_OUTPUT" | |
| jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" | |
| echo "EOF" >> "$GITHUB_OUTPUT" | |
| cp dist-manifest.json "$BUILD_MANIFEST_NAME" | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: artifacts-build-global | |
| path: | | |
| ${{ steps.cargo-dist.outputs.paths }} | |
| ${{ env.BUILD_MANIFEST_NAME }} | |
| # ── Step 5: Publish ── | |
| host: | |
| name: Create GitHub Release | |
| needs: | |
| - prepare | |
| - plan | |
| - build-local-artifacts | |
| - build-global-artifacts | |
| if: ${{ always() && needs.plan.result == 'success' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} | |
| HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} | |
| runs-on: ubuntu-22.04 | |
| outputs: | |
| val: ${{ steps.host.outputs.manifest }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ needs.prepare.outputs['release-tag'] }} | |
| persist-credentials: false | |
| submodules: recursive | |
| - name: Install cached dist | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: cargo-dist-cache | |
| path: ~/.cargo/bin/ | |
| - run: chmod +x ~/.cargo/bin/dist | |
| - name: Fetch artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: artifacts-* | |
| path: target/distrib/ | |
| merge-multiple: true | |
| - id: host | |
| shell: bash | |
| run: | | |
| dist host --tag=${{ needs.prepare.outputs['release-tag'] }} --steps=upload --output-format=json > dist-manifest.json | |
| echo "artifacts uploaded successfully" | |
| cat dist-manifest.json | |
| echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" | |
| - name: Upload dist-manifest.json | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: artifacts-dist-manifest | |
| path: dist-manifest.json | |
| - name: Download GitHub Artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: artifacts-* | |
| path: artifacts | |
| merge-multiple: true | |
| - name: Cleanup | |
| run: | | |
| rm -f artifacts/*-dist-manifest.json | |
| - name: Finish Homeboy release pipeline at tag | |
| uses: Extra-Chill/homeboy-action@v2 | |
| with: | |
| source: '.' | |
| component: homeboy | |
| commands: release | |
| release-head: 'true' | |
| release-from-artifacts: artifacts | |
| announce: | |
| needs: | |
| - plan | |
| - host | |
| if: ${{ always() && needs.host.result == 'success' }} | |
| runs-on: ubuntu-22.04 | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false | |
| submodules: recursive | |
| record-failure: | |
| name: Record failed release SHA | |
| needs: | |
| - check | |
| - gate-build | |
| - gate-audit | |
| - gate-lint | |
| - gate-test | |
| - release-quality-policy | |
| - prepare | |
| - plan | |
| - build-local-artifacts | |
| - build-global-artifacts | |
| - host | |
| - announce | |
| if: ${{ always() && github.event_name == 'push' && needs.check.outputs.should-release == 'true' && contains(toJson(needs), '"result":"failure"') }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Save failed SHA | |
| run: git rev-parse HEAD > "${RUNNER_TEMP}/homeboy-release-last-failed" | |
| - name: Cache failed SHA | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: ${{ runner.temp }}/homeboy-release-last-failed | |
| key: release-last-failed-${{ github.ref_name }}-${{ github.sha }} | |
| clear-failure: | |
| name: Clear failed release SHA cache | |
| needs: | |
| - check | |
| - gate-build | |
| - gate-audit | |
| - gate-lint | |
| - gate-test | |
| - release-quality-policy | |
| - prepare | |
| - plan | |
| - build-local-artifacts | |
| - build-global-artifacts | |
| - host | |
| - announce | |
| if: ${{ always() && github.event_name == 'push' && needs.check.outputs.should-release == 'true' && !contains(toJson(needs), '"result":"failure"') && !contains(toJson(needs), '"result":"cancelled"') }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Clear failed SHA cache | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh cache list --json id,key --jq '.[] | select(.key | startswith("release-last-failed-${{ github.ref_name }}-")) | .id' | while read -r id; do | |
| gh cache delete "$id" 2>/dev/null || true | |
| done |