Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,21 @@ jobs:
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2.0.0
with:
toolchain: 1.92.0
components: rustfmt, clippy
rustflags: ""
cache-shared-key: ci-msrv

- name: Check
run: cargo check --all-targets --all-features

# The MSRV toolchain lints differently from stable; lint here too so a
# pull request is held to every toolchain the crate supports.
- name: Format check (MSRV)
run: cargo fmt --all -- --check

- name: Clippy (MSRV)
run: cargo clippy --all-targets --all-features -- -D warnings

- name: Test
run: cargo test --all-features

Expand Down
221 changes: 96 additions & 125 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,152 +1,126 @@
name: Release Gate

name: Release

# Publishes guard-agent-rs to crates.io for a release tag. Nothing is linted
# or tested here: the tagged commit must already have passed the push
# workflows listed in REQUIRED_WORKFLOWS (ci.yml runs fmt and clippy on
# stable and on the MSRV, the tests with Redis, and coverage). A release can
# therefore never fail on a check its pull request did not run.
#
# Re-drive: run this workflow by hand (Actions > Release > Run workflow) with
# the tag, for example v3.2.4. It checks out that tag, so a fixed workflow on
# master can publish an existing tag. A version already on crates.io is
# skipped. guard-core-rs at the required version must be on crates.io first.
on:
push:
tags:
- 'v*'
release:
types: [created]
workflow_dispatch:
inputs:
tag:
description: 'Release tag to publish, for example v3.2.4'
required: true

permissions: {}

concurrency:
group: release-${{ inputs.tag || github.event.release.tag_name }}
cancel-in-progress: false

env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
REQUIRED_WORKFLOWS: >-
.github/workflows/ci.yml

jobs:
test:
name: Test at tag (${{ matrix.rust }})
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
matrix:
rust: ['stable', '1.92.0']

services:
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- name: Checkout tag
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

# The workspace's examples/basic_usage carries the guard-core-rs path
# dependency (the bridge feature's sibling), so every job that loads
# the workspace metadata checks the sibling out first (the ci.yml
# shape). The packaged crate itself ships the registry dependency:
# packaging strips the path and keeps the version pin.
- name: Checkout guard-core-rs (path dependency)
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: Guard-Core/guard-core-rs
ref: master
path: core-checkout
persist-credentials: false

- name: Place guard-core-rs at the path dependency location
run: mv core-checkout ../guard-core-rs


- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2.0.0
with:
toolchain: ${{ matrix.rust }}
components: rustfmt, clippy
rustflags: ""
cache-shared-key: release-${{ matrix.rust }}

- name: Format check
run: cargo fmt --all -- --check

- name: Clippy
run: cargo clippy --all-targets --all-features -- -D warnings

- name: Test (default features)
run: cargo test

- name: Test (all features, including Redis persistence)
run: cargo test --all-features -- --include-ignored

tag-consistency:
name: Tag matches crate version
needs: [test]
verify:
name: Tagged commit passed CI
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
outputs:
tag: ${{ steps.tag.outputs.tag }}
steps:
- name: Checkout tag
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Resolve the tag
id: tag
env:
INPUT_TAG: ${{ inputs.tag }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
tag="${INPUT_TAG:-$RELEASE_TAG}"
if ! printf '%s' "$tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "::error::tag '$tag' is not of the form v1.2.3"
exit 1
fi
echo "tag=$tag" >> "$GITHUB_OUTPUT"

# The workspace's examples/basic_usage carries the guard-core-rs path
# dependency (the bridge feature's sibling), so every job that loads
# the workspace metadata checks the sibling out first (the ci.yml
# shape). The packaged crate itself ships the registry dependency:
# packaging strips the path and keeps the version pin.
- name: Checkout guard-core-rs (path dependency)
- name: Checkout the tag
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: Guard-Core/guard-core-rs
ref: master
path: core-checkout
ref: refs/tags/${{ steps.tag.outputs.tag }}
persist-credentials: false

- name: Place guard-core-rs at the path dependency location
run: mv core-checkout ../guard-core-rs


- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2.0.0
with:
rustflags: ""

- name: Verify tag equals the crate version
- name: Verify the tag equals the crate version
env:
TAG: ${{ steps.tag.outputs.tag }}
run: |
tag="${GITHUB_REF_NAME#v}"
version=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "guard-agent-rs") | .version')
if [ "$tag" != "$version" ]; then
echo "tag v$tag does not match crate version $version"
version=$(sed -n 's/^version *= *"\(.*\)"/\1/p' Cargo.toml | head -1)
if [ "${TAG#v}" != "$version" ]; then
echo "::error::tag $TAG does not match guard-agent-rs version $version"
exit 1
fi
echo "ok: tag v$tag matches guard-agent-rs version $version"
echo "ok: $TAG matches guard-agent-rs $version"

# The newest push run of each required workflow on the tagged commit
# must have succeeded. A release created right after a merge waits for
# them (up to an hour) instead of failing.
- name: Require green CI on the tagged commit
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: |
sha=$(git rev-parse HEAD)
for _ in $(seq 1 60); do
runs=$(gh api "repos/$REPO/actions/runs?head_sha=$sha&event=push&per_page=100" \
--jq '[.workflow_runs | group_by(.path)[] | max_by(.run_number) | {path, status, conclusion}]')
waiting=0
for wf in $REQUIRED_WORKFLOWS; do
run=$(jq -c --arg p "$wf" '.[] | select(.path == $p)' <<< "$runs")
if [ -z "$run" ]; then
echo "$wf: no push run on $sha yet"; waiting=1; continue
fi
status=$(jq -r .status <<< "$run"); conclusion=$(jq -r .conclusion <<< "$run")
if [ "$status" != "completed" ]; then
echo "$wf: $status"; waiting=1
elif [ "$conclusion" != "success" ]; then
echo "::error::$wf concluded $conclusion on $sha; fix master and release a new tag"
exit 1
else
echo "ok: $wf success on $sha"
fi
done
[ "$waiting" = "0" ] && exit 0
sleep 60
done
echo "::error::required workflows did not finish on $sha within an hour"
exit 1

publish:
name: Publish to crates.io
needs: tag-consistency
# Trusted publishing (crates.io enforces it for this crate): the job
# exchanges the Actions OIDC token via id-token: write. workflow_dispatch
# is allowed so a publish can be re-driven after a partial failure
# (master's crate version must match the intended release).
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
needs: verify
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Checkout release tag
- name: Checkout the tag
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: refs/tags/${{ needs.verify.outputs.tag }}
persist-credentials: false

# The workspace's examples/basic_usage carries the guard-core-rs path
# dependency (the bridge feature's sibling), so every job that loads
# the workspace metadata checks the sibling out first (the ci.yml
# shape). The packaged crate itself ships the registry dependency:
# packaging strips the path and keeps the version pin.
# dependency, so loading the workspace needs the sibling checkout. The
# packaged crate itself ships the registry dependency.
- name: Checkout guard-core-rs (path dependency)
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand All @@ -158,8 +132,7 @@ jobs:
- name: Place guard-core-rs at the path dependency location
run: mv core-checkout ../guard-core-rs


- name: Setup Rust toolchain
- name: Install the Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2.0.0
with:
rustflags: ""
Expand All @@ -170,26 +143,24 @@ jobs:
uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5
id: auth

# Retried up to 3 times (crates.io transient 5xx). A version already
# on crates.io (the partial-failure rerun) skips via the registry
# check, so a re-drive never blocks on the identical-version rejection.
- name: Publish guard-agent-rs
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
TAG: ${{ needs.verify.outputs.tag }}
run: |
set -o pipefail
tag="${GITHUB_REF_NAME#v}"
published=$(curl -fsSL -A "guard-agent-rs release (https://github.com/Guard-Core/guard-agent-rs)" "https://crates.io/api/v1/crates/guard-agent-rs" | jq -r .crate.max_version || true)
if [ "$published" = "$tag" ]; then
echo "guard-agent-rs $tag is already on crates.io; skipping"
version="${TAG#v}"
code=$(curl -s -o /dev/null -w '%{http_code}' -A "guard-agent-rs release (https://github.com/Guard-Core/guard-agent-rs)" \
"https://crates.io/api/v1/crates/guard-agent-rs/$version")
if [ "$code" = "200" ]; then
echo "guard-agent-rs $version is already on crates.io; skipping"
exit 0
fi
for attempt in 1 2 3; do
if cargo publish; then
if cargo publish -p guard-agent-rs --locked; then
exit 0
fi
echo "attempt $attempt failed; retrying in 30s"
sleep 30
done
echo "guard-agent-rs publish failed after 3 attempts"
echo "::error::guard-agent-rs publish failed after 3 attempts"
exit 1
Loading