faultbox 0.1.1

Production black-box recorder: structured crash, corruption, and invariant-violation reports with a flight-recorder breadcrumb trail — debuggable from a report without reproduction or shipped symbols.
Documentation
# Manual distribution workflow. Push a v* tag first; `Release Prepare` runs the
# test suite and packages the crate. Then run this workflow with that tag and
# the successful prepare run ID.
#
# NOTHING HERE COMPILES the test suite again. Every stage consumes the prepare
# run's artifact, so a failure in one stage is retried by re-dispatching with
# only that stage enabled — the tests are never re-run to fix a crates.io
# timeout or a bad release body.
#
# Stages, in dependency order:
#   publish_crate   -> crates.io
#   github_release  -> GitHub Release carrying the .crate file
#
# Each stage is idempotent: an already-published version is skipped, and the
# release is overwritten in place. Re-running everything is safe; the toggles
# exist to save time, not to protect against double-publishing.
#
# Required secrets: CARGO_REGISTRY_TOKEN.

name: Release
run-name: Release ${{ inputs.tag }}

on:
  workflow_dispatch:
    inputs:
      tag:
        description: "Release tag to publish, e.g. v0.1.0"
        required: true
        type: string
      prepare_run_id:
        description: "Successful Release Prepare run ID holding the .crate"
        required: true
        type: string
      publish_crate:
        description: "Publish to crates.io"
        type: boolean
        default: true
      github_release:
        description: "Create the GitHub Release"
        type: boolean
        default: true

concurrency:
  group: release
  cancel-in-progress: false

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always

jobs:
  # Always runs. Cheap, and it guarantees the dispatched tag still means what
  # the prepare run assumed it meant.
  validate-version:
    uses: ./.github/workflows/release-validate.yml
    with:
      ref: ${{ inputs.tag }}

  # ── crates.io ──────────────────────────────────────────────────────────────
  publish-crate:
    name: Publish to crates.io
    needs: validate-version
    if: inputs.publish_crate
    runs-on: ubuntu-latest
    environment: crates.io
    steps:
      - uses: actions/checkout@v7
        with:
          ref: ${{ inputs.tag }}

      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable

      # Must run before publishing: the manifest carries only the base version,
      # so a prerelease tag would otherwise publish the base version instead.
      - name: Set version from tag
        run: bash scripts/ci/stamp_version.sh "${{ needs.validate-version.outputs.version }}"

      - name: Publish
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
          VERSION: ${{ needs.validate-version.outputs.version }}
        run: |
          set -euo pipefail

          # Skip if this exact version is already indexed, so a re-dispatch
          # after a partial failure is a no-op rather than an error.
          if curl -sf \
            -H "User-Agent: faultbox-ci (github.com/${{ github.repository }})" \
            "https://crates.io/api/v1/crates/faultbox/${VERSION}" > /dev/null 2>&1
          then
            echo "faultbox@${VERSION} already published — skipping"
            exit 0
          fi

          # Verified here rather than trusting the prepare run's artifact: the
          # registry is what consumers get, and this is the last gate before it.
          # `--allow-dirty` because the stamp step edits Cargo.toml in the tree.
          cargo publish --all-features --allow-dirty

  # ── GitHub Release ─────────────────────────────────────────────────────────
  # Deliberately independent of publish-crate: a crates.io outage must not block
  # cutting the release, and re-running this stage alone is the fix for a bad
  # release body.
  github-release:
    name: Create GitHub Release
    needs: validate-version
    if: inputs.github_release
    runs-on: ubuntu-latest
    permissions:
      contents: write
      actions: read
    steps:
      - uses: actions/checkout@v7
        with:
          ref: ${{ inputs.tag }}
          # Needed to find the preceding tag for the compare link.
          fetch-depth: 0

      - name: Download crate artifact
        uses: actions/download-artifact@v8
        with:
          name: crate
          run-id: ${{ inputs.prepare_run_id }}
          github-token: ${{ github.token }}
          path: ./artifacts

      # The changelog is the release notes. Auto-generated notes list commit
      # subjects, which describe what was done to the code; the changelog
      # describes what changed for the person upgrading. `validate-version`
      # has already proved this section exists and is non-empty.
      - name: Build release notes from the changelog
        env:
          TAG: ${{ inputs.tag }}
          BASE: ${{ needs.validate-version.outputs.base_version }}
        run: |
          PREVIOUS_TAG=$(git tag --sort=-v:refname 'v*' | grep -v "^${TAG}$" | head -n 1 || true)

          bash scripts/ci/changelog_section.sh "$BASE" /tmp/changelog-section.md

          {
            cat /tmp/changelog-section.md
            echo
            if [[ -n "$PREVIOUS_TAG" ]]; then
              echo "**Full diff:** https://github.com/farhan-syah/faultbox/compare/${PREVIOUS_TAG}...${TAG}"
            else
              echo "**Tree:** https://github.com/farhan-syah/faultbox/tree/${TAG}"
            fi
          } > /tmp/release-notes.md

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v3
        with:
          tag_name: ${{ inputs.tag }}
          name: faultbox ${{ needs.validate-version.outputs.version }}
          body_path: /tmp/release-notes.md
          draft: false
          prerelease: ${{ needs.validate-version.outputs.is_full_release != 'true' }}
          files: artifacts/*
          fail_on_unmatched_files: true