name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions: read-all
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
jobs:
fmt:
name: fmt
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Install stable toolchain
run: |
set -euo pipefail
rustup update stable
rustup default stable
rustup component add rustfmt
- name: Check formatting
run: cargo fmt --all -- --check
clippy:
name: clippy
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Install stable toolchain
run: |
set -euo pipefail
rustup update stable
rustup default stable
rustup component add clippy
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-clippy-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-clippy-
${{ runner.os }}-cargo-
- name: Run clippy
run: cargo clippy --locked --all-targets --all-features -- -D warnings
unit-tests:
name: unit tests
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Install stable toolchain
run: rustup update stable && rustup default stable
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-unit-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-unit-
${{ runner.os }}-cargo-
- name: Run unit tests (lib + bins only)
run: cargo test --locked --lib --bins --verbose
integration-tests:
name: integration tests
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Install stable toolchain
run: rustup update stable && rustup default stable
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-integration-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-integration-
${{ runner.os }}-cargo-
- name: Run integration tests (tests/ directory)
run: cargo test --locked --tests --verbose
smoke-tests:
name: smoke tests
runs-on: ubuntu-24.04
timeout-minutes: 20
needs: [unit-tests, integration-tests, clippy]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Install stable toolchain
run: rustup update stable && rustup default stable
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-smoke-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-smoke-
${{ runner.os }}-cargo-
- name: Build release binary
run: cargo build --release --locked
- name: Smoke — sweep exits 0 on empty directory
run: |
set -euo pipefail
mkdir -p "${{ runner.temp }}/smoke-empty"
./target/release/timebomb sweep "${{ runner.temp }}/smoke-empty"
- name: Smoke — manifest always exits 0 even with detonated fuse
run: |
set -euo pipefail
mkdir -p "${{ runner.temp }}/smoke-list"
echo "// TODO[2020-01-01]: expired annotation" > "${{ runner.temp }}/smoke-list/test.rs"
./target/release/timebomb manifest "${{ runner.temp }}/smoke-list"
- name: Smoke — sweep exits 1 when detonated fuse found
run: |
set -euo pipefail
mkdir -p "${{ runner.temp }}/smoke-expired"
echo "// TODO[2020-01-01]: this is expired" > "${{ runner.temp }}/smoke-expired/main.rs"
if ./target/release/timebomb sweep "${{ runner.temp }}/smoke-expired"; then
echo "ERROR: expected exit 1 for detonated fuse" && exit 1
fi
- name: Smoke — JSON output is valid JSON
run: |
set -euo pipefail
mkdir -p "${{ runner.temp }}/smoke-json"
echo "// FIXME[2020-01-01]: old" > "${{ runner.temp }}/smoke-json/lib.rs"
(./target/release/timebomb sweep "${{ runner.temp }}/smoke-json" --format json || true) | python3 -m json.tool > /dev/null
- name: Smoke — GitHub Actions format emits workflow commands
run: |
set -euo pipefail
mkdir -p "${{ runner.temp }}/smoke-gh"
echo "// TODO[2020-01-01]: expired annotation" > "${{ runner.temp }}/smoke-gh/main.rs"
output=$(./target/release/timebomb sweep "${{ runner.temp }}/smoke-gh" --format github || true)
echo "$output" | grep -q "::error" || (echo "Expected ::error annotation" && exit 1)
- name: Checksum release binary
run: sha256sum target/release/timebomb > target/release/timebomb.sha256
- name: Upload release binary
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with:
name: timebomb-release-binary
path: |
target/release/timebomb
target/release/timebomb.sha256
retention-days: 1
self-check:
name: self check
runs-on: ubuntu-24.04
timeout-minutes: 10
needs: [smoke-tests]
continue-on-error: true
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Download release binary
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with:
name: timebomb-release-binary
path: target/release/
- name: Verify binary checksum
run: sha256sum --check target/release/timebomb.sha256
- name: Make binary executable
run: chmod +x target/release/timebomb
- name: Scan src/ with GitHub Actions format
run: ./target/release/timebomb sweep ./src --format github || true
- name: List all fuses sorted by date
run: ./target/release/timebomb manifest ./src || true
release:
name: release
runs-on: ubuntu-24.04
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
needs: [smoke-tests, self-check]
permissions:
contents: write pull-requests: write env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
steps:
- id: release
uses: googleapis/release-please-action@a02a34c4d625f9be7cb89156071d8567266a2445 with:
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
publish-crate:
name: publish crate
runs-on: ubuntu-24.04
needs: [release, publish]
if: needs.release.outputs.release_created == 'true' && needs.publish.result == 'success'
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with:
ref: ${{ needs.release.outputs.tag_name }}
- name: Install stable toolchain
run: rustup update stable && rustup default stable
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-publish-crate-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-publish-crate-
${{ runner.os }}-cargo-
- name: Publish to crates.io
shell: bash
run: |
set -euo pipefail
set +e
output="$(cargo publish --locked 2>&1)"
status=$?
set -e
printf '%s\n' "$output"
if [ "$status" -eq 0 ]; then
exit 0
fi
if printf '%s\n' "$output" | grep -Eiq 'already (been )?(uploaded|published)|version .* already (exists|uploaded|published)|crate version .* already'; then
echo "crate version is already published; treating cargo publish as a no-op"
exit 0
fi
exit "$status"
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
publish-image:
name: publish Docker image
runs-on: ubuntu-24.04
needs: release
if: needs.release.outputs.release_created == 'true'
timeout-minutes: 45
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with:
ref: ${{ needs.release.outputs.tag_name }}
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 with:
platforms: arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f
- name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
shell: bash
run: |
set -euo pipefail
tag="${{ needs.release.outputs.tag_name }}"
version="${tag#v}"
make docker-push IMAGE=pwbsladek/timebomb IMAGE_TAG="$version"
publish:
name: publish (${{ matrix.asset_name }})
needs: release
if: needs.release.outputs.release_created == 'true'
timeout-minutes: 30
permissions:
contents: write strategy:
fail-fast: true
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-24.04
asset_name: timebomb-linux-x86_64
- target: aarch64-unknown-linux-gnu
os: ubuntu-24.04
asset_name: timebomb-linux-aarch64
- target: x86_64-apple-darwin
os: macos-14
asset_name: timebomb-macos-x86_64
- target: aarch64-apple-darwin
os: macos-14
asset_name: timebomb-macos-aarch64
- target: x86_64-pc-windows-msvc
os: windows-2022
asset_name: timebomb-windows-x86_64.exe
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with:
ref: ${{ needs.release.outputs.tag_name }}
- name: Install stable toolchain
shell: bash
run: |
set -euo pipefail
rustup update stable
rustup default stable
rustup target add ${{ matrix.target }}
- name: Install aarch64 Linux cross-compiler
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
set -euo pipefail
sudo apt-get update -qq
sudo apt-get install -y gcc-aarch64-linux-gnu
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-publish-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-publish-${{ matrix.target }}-
${{ runner.os }}-cargo-
- name: Build release binary
shell: bash
run: cargo build --release --locked --target "$TARGET"
env:
TARGET: ${{ matrix.target }}
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
- name: Upload binary to GitHub release (Unix)
if: matrix.os != 'windows-2022'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.release.outputs.tag_name }}
TARGET: ${{ matrix.target }}
ASSET_NAME: ${{ matrix.asset_name }}
shell: bash
run: |
set -euo pipefail
cp "target/${TARGET}/release/timebomb" "${RUNNER_TEMP}/${ASSET_NAME}"
gh release upload "${TAG}" "${RUNNER_TEMP}/${ASSET_NAME}" --clobber
- name: Upload binary to GitHub release (Windows)
if: matrix.os == 'windows-2022'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.release.outputs.tag_name }}
TARGET: ${{ matrix.target }}
ASSET_NAME: ${{ matrix.asset_name }}
run: |
Copy-Item "target/$env:TARGET/release/timebomb.exe" "$env:RUNNER_TEMP/$env:ASSET_NAME"
gh release upload "$env:TAG" "$env:RUNNER_TEMP/$env:ASSET_NAME" --clobber