Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5486eea
ci: add a script that verifies a release run step by step
mlevkov Sep 23, 2026
92cfb38
docs(tech-debt): record the silent Codecov upload failure
mlevkov Sep 23, 2026
d073caa
docs(tech-debt): record the crates.io job that cannot succeed
mlevkov Sep 23, 2026
9eb245f
docs(tech-debt): record rust-cache's floating pin
mlevkov Sep 23, 2026
771f1d9
docs(tech-debt): record main's non-strict required checks
mlevkov Sep 23, 2026
6d7c45c
docs(tech-debt): record why Dependabot proposes no iggy 0.11
mlevkov Sep 23, 2026
dd1cc22
ci(verify-release): run the binary isolated and time-bounded
mlevkov Sep 23, 2026
1886bc9
ci(verify-release): tie the run to its tag and fail closed on fetches
mlevkov Sep 23, 2026
ce208a1
ci(verify-release): check every asset, the notes, Pages and crates.io
mlevkov Sep 23, 2026
ab3a6e7
ci: run the release verifier after every green Release run
mlevkov Sep 23, 2026
dc9fdc4
ci: lint shell scripts with shellcheck
mlevkov Sep 23, 2026
3eadd8c
ci: mark the lines the verifier and two TD records depend on
mlevkov Sep 23, 2026
0f4577e
docs: say what exercising release.yml leaves behind
mlevkov Sep 23, 2026
e148351
docs(tech-debt): correct the counts and wording review found
mlevkov Sep 23, 2026
5bb70c5
docs(code-reviews): release verifier review round 1
mlevkov Sep 23, 2026
4d56993
ci(verify-release): fail closed where round 2 found new silent paths
mlevkov Sep 24, 2026
ceab671
ci: let Verify Release run by hand and keep the token off disk
mlevkov Sep 24, 2026
deaa245
ci(verify-release): verify the run and attempt that triggered it
mlevkov Sep 24, 2026
3d1ecf7
docs: say when the Pages restore expires and how it can fail
mlevkov Sep 24, 2026
f32abc0
ci: correct the verifier's changelog comment and mark two more sites
mlevkov Sep 24, 2026
c1d6545
docs: say how the publish job fails, and name both tag patterns
mlevkov Sep 24, 2026
7439d29
docs(code-reviews): correct round 1 where round 2 found it wrong
mlevkov Sep 24, 2026
e5fefd2
ci: close the gaps the remediation check found
mlevkov Sep 24, 2026
0566d29
docs: correct the restore steps and round 1's attribution
mlevkov Sep 24, 2026
39280b9
docs: bring the changelog entry up to round 2's changes
mlevkov Sep 24, 2026
18c1a44
docs(code-reviews): release verifier review round 2
mlevkov Sep 24, 2026
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
30 changes: 26 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Continuous Integration workflow for iggy-sample
#
# Runs on every push to main and on all pull requests.
# Includes: formatting, linting, testing, coverage, documentation, and security audit.
# Runs on pushes to main and develop, on pull requests to main, and weekly.
# Includes: formatting, linting, testing, coverage, documentation, security
# audit, dependency policy, and shell-script linting.

name: CI

Expand Down Expand Up @@ -55,6 +56,8 @@ jobs:
with:
toolchain: stable
components: clippy
# TD-2026-09-05: this file's rust-cache pins are a branch head, not
# the release their comment names; re-pin every workflow's together.
- uses: Swatinem/rust-cache@f0d9c3887740aee45f6153b24b3a6b815192ec16 # v2
- name: Run Clippy
run: cargo clippy --locked --all-targets --all-features -- -D warnings
Expand Down Expand Up @@ -136,6 +139,9 @@ jobs:
- name: Generate coverage report
run: cargo llvm-cov --locked --all-features --workspace --lcov --output-path lcov.info

# TD-2026-09-03: without a CODECOV_TOKEN secret Codecov rejects
# this upload, and fail_ci_if_error: false keeps the job green.
# Resolve the record before editing this step.
- name: Upload coverage to Codecov
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with:
Expand Down Expand Up @@ -246,13 +252,28 @@ jobs:
fi
echo "Dockerfile rust $image is at or above rust-version $msrv"

# ==========================================================================
# Shell scripts (scripts/verify-release.sh runs after every release)
# ==========================================================================
shellcheck:
name: Shellcheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# Preinstalled on GitHub's ubuntu runners; -s bash because the
# scripts are bash, and they must stay clean under it.
- name: Lint shell scripts
run: |
shellcheck --version
shellcheck -s bash scripts/*.sh

# ==========================================================================
# Final status check (for branch protection)
# ==========================================================================
ci-success:
name: CI Success
runs-on: ubuntu-latest
needs: [fmt, clippy, test, integration, docs, audit, licenses]
needs: [fmt, clippy, test, integration, docs, audit, licenses, shellcheck]
if: always()
steps:
- name: Check all jobs passed
Expand All @@ -263,7 +284,8 @@ jobs:
[[ "${{ needs.integration.result }}" != "success" ]] || \
[[ "${{ needs.docs.result }}" != "success" ]] || \
[[ "${{ needs.audit.result }}" != "success" ]] || \
[[ "${{ needs.licenses.result }}" != "success" ]]; then
[[ "${{ needs.licenses.result }}" != "success" ]] || \
[[ "${{ needs.shellcheck.result }}" != "success" ]]; then
echo "One or more jobs failed"
exit 1
fi
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/extended-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ jobs:
- uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de # master
with:
toolchain: stable
# TD-2026-09-05: this file's rust-cache pins are a branch head, not
# the release their comment names; re-pin every workflow's together.
- uses: Swatinem/rust-cache@f0d9c3887740aee45f6153b24b3a6b815192ec16 # v2

- name: Install criterion dependencies
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ jobs:
- uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de # master
with:
toolchain: stable
# TD-2026-09-05: this file's rust-cache pins are a branch head, not
# the release their comment names; re-pin every workflow's together.
- uses: Swatinem/rust-cache@f0d9c3887740aee45f6153b24b3a6b815192ec16 # v2

- name: Check for missing documentation
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
# Triggered when a version tag is pushed (e.g., v0.1.0).
# Builds release binaries for multiple platforms and creates a GitHub release.

# .github/workflows/verify-release.yml runs after this workflow by this name
# (workflow_run); rename both together.
name: Release

on:
Expand Down Expand Up @@ -46,6 +48,7 @@ jobs:
echo "is_prerelease=false" >> $GITHUB_OUTPUT
fi

# scripts/verify-release.sh matches this line exactly
echo "Version: $VERSION"

- name: Verify Cargo.toml version matches tag
Expand All @@ -61,6 +64,7 @@ jobs:
exit 1
fi

# scripts/verify-release.sh matches this line exactly
echo "Version verified: $CARGO_VERSION"

# ==========================================================================
Expand Down Expand Up @@ -104,6 +108,8 @@ jobs:
toolchain: stable
targets: ${{ matrix.target }}

# TD-2026-09-05: this file's rust-cache pins are a branch head, not
# the release their comment names; re-pin every workflow's together.
- uses: Swatinem/rust-cache@f0d9c3887740aee45f6153b24b3a6b815192ec16 # v2
with:
key: release-${{ matrix.target }}
Expand Down Expand Up @@ -176,6 +182,9 @@ jobs:
# Get the previous tag
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")

# scripts/verify-release.sh matches the first echo line below
# exactly, which also rules out the fallback, and counts the
# "- %s (%h)" entries; change them together.
if [ -n "$PREV_TAG" ]; then
echo "Generating changelog since $PREV_TAG"
CHANGELOG=$(git log --pretty=format:"- %s (%h)" $PREV_TAG..HEAD)
Expand All @@ -200,6 +209,9 @@ jobs:
# ==========================================================================
# Publish to crates.io (optional)
# ==========================================================================
# TD-2026-09-04: this job cannot succeed (there is no CRATES_IO_TOKEN
# secret, and Cargo.toml sets publish = false), and continue-on-error hides
# that. Resolve the record before editing the job.
publish:
name: Publish to crates.io
runs-on: ubuntu-latest
Expand Down
66 changes: 66 additions & 0 deletions .github/workflows/verify-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Verify every green Release run step by step.
#
# A Release run can conclude success while a step failed under
# continue-on-error, and nothing else inspects what a run produced, so this
# runs scripts/verify-release.sh after each one: a problem shows up as a
# failed run here instead of waiting for someone to remember the script.
# Re-running jobs of a Release run makes a new attempt of it, which runs
# this again once it succeeds. Until TD-2026-09-04 is resolved it fails on
# every stable release, by design: the crates.io publish step fails
# silently there. It can also be run by hand for any tag (Actions > Verify
# Release > Run workflow).

name: Verify Release

on:
workflow_run:
workflows: [Release]
types: [completed]
workflow_dispatch:
inputs:
tag:
description: "Tag whose Release run to verify, e.g. v0.4.1"
required: true
type: string

permissions:
actions: read
contents: read
deployments: read

jobs:
verify:
name: Verify release run
# By hand, or after a green run from a tag push: a failed run is already
# visible.
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.event == 'push' && github.event.workflow_run.conclusion == 'success')
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
# The job runs a downloaded binary; keep the token off the disk
# (git fetch and ls-remote work anonymously on this public repo).
persist-credentials: false

- name: Verify the release run
env:
GH_TOKEN: ${{ github.token }}
# Through the environment, not interpolated into the script, so a
# crafted tag name cannot inject shell.
TAG: ${{ inputs.tag || github.event.workflow_run.head_branch }}
# The run and attempt that triggered this one, not the tag's
# newest; both are empty when run by hand, which verifies the
# newest run at its latest attempt.
VERIFY_RUN_ID: ${{ github.event.workflow_run.id }}
VERIFY_RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt }}
run: |
set +e
scripts/verify-release.sh "$TAG" 2>&1 | tee verify.out
status=${PIPESTATUS[0]}
# The PASS/FAIL lines on the run's summary page, not only its log.
{ echo '```'; cat verify.out; echo '```'; } >>"$GITHUB_STEP_SUMMARY"
exit "$status"
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `scripts/verify-release.sh` checks a release.yml run step by step instead
of trusting its conclusion: that the tag names the commit the run built;
what each step printed; the release notes; that every asset is
byte-identical to its build job's artifact and holds a binary for its
target; the Pages deployment and the served docs; crates.io against
Cargo.toml's `publish`; and that the host platform's binary runs, isolated
and time-bounded. It fails on a step that failed under `continue-on-error`,
which the Actions API reports as a success: the crates.io publish step has
failed that way on every stable release (TD-2026-09-04). It handles a run
whose jobs were re-run, where the jobs an attempt did not re-run carry
over with their logs, and needs gh 2.75 or later to fetch those
- The Verify Release workflow runs it after every green Release run,
re-runs included, on exactly the run and attempt that triggered it, so a
hidden failure fails a run instead of waiting for someone to look; it can
also be run by hand for any tag. Until TD-2026-09-04 is resolved it fails
on every stable release, by design
- CI lints shell scripts with shellcheck, as part of the required CI Success
check
- CONTRIBUTING.md describes releasing, and exercising `release.yml` with a
pre-release tag, including the two things that outlive the exercise: the
Pages deployment (with how to restore it, and the limits of doing so) and
the tag itself

### Fixed

- The README no longer says coverage is uploaded to Codecov: every upload
whose log is still retained was rejected for want of a token
(TD-2026-09-03)

## [0.4.1] - 2026-09-23

Security patch for four RustSec advisories: one reachable on the public API
Expand Down
50 changes: 50 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,56 @@ src/
└── services/ # Business logic
```

## Releasing

Pushing a `vX.Y.Z` tag (or `vX.Y.Z-<suffix>` for a pre-release) runs
`.github/workflows/release.yml`, which builds the binaries, creates the GitHub
Release and deploys the docs to GitHub Pages; only admins can create tags.
Before tagging, set `version` in `Cargo.toml` (the validate job requires the
tag to match it), move the `[Unreleased]` changelog entries under the new
version, and resolve or re-bind every record in `docs/tech-debt/` whose
trigger is the next release.

A green run does not mean every step worked, since a step under
`continue-on-error` fails silently. So `.github/workflows/verify-release.yml`
runs `scripts/verify-release.sh` after every green Release run; until
TD-2026-09-04 is resolved it fails on every stable release, by design. To
check a run by hand:

```bash
scripts/verify-release.sh v0.4.2
```

It checks the tag's newest run at its latest attempt; `VERIFY_RUN_ID` and
`VERIFY_RUN_ATTEMPT` pick another run of the tag or an earlier attempt.

To exercise a `release.yml` change before a real release, push a pre-release
tag of the current `Cargo.toml` version, such as `v0.4.1-ci.1` (the validate
job compares only the part before the hyphen), and verify its run the same
way. Two things outlive the exercise:

- The docs job deploys the tagged commit's docs to Pages even for a
pre-release, and deleting the tag does not undo that. To restore the latest
stable release's docs, re-run the Deploy Documentation job of its Release
run (a new attempt that succeeds runs Verify Release again), with two
limits:
- GitHub re-runs jobs only within 30 days of a run. Past that, tag the
stable release's commit as a pre-release instead
(`git tag v0.4.1-docs.1 'v0.4.1^{}'`) and push it: its run deploys that
commit's docs. Delete the tag afterwards like any exercise tag.
- deploy-pages refuses a run that holds more than one `github-pages`
artifact: v0.2.0's second re-run failed with `Multiple artifacts named
"github-pages"` beside the first attempt's, which had not expired yet
(whether an expired one also counts is untested). If that happens,
delete the run's `github-pages` artifacts and re-run the job again. `gh api repos/mlevkov/iggy_sample/actions/runs/<run-id>/artifacts`
lists them, and
`gh api -X DELETE repos/mlevkov/iggy_sample/actions/artifacts/<id>`
deletes one.
- The tag. Delete it with `gh release delete v0.4.1-ci.1 --cleanup-tag --yes`
from your clone (this also deletes the local tag). Left behind, it becomes
where `release.yml` starts the changelog it writes for the next release
(`git describe --tags`), which drops every commit before it.

## Questions?

- Open a [Discussion](https://github.com/mlevkov/iggy_sample/discussions) for questions
Expand Down
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ rust-version = "1.93.0"
description = "A comprehensive demonstration of Apache Iggy message streaming with Axum"
license = "MIT"
# Repo-level releases only - never published to crates.io (enforced here,
# not just in CI: cargo refuses to publish with this set)
# not just in CI: cargo refuses to publish with this set). release.yml still
# runs a publish job, which fails silently: with no token it stops before
# reaching this setting, and with one this would stop it. TD-2026-09-04.
publish = false
repository = "https://github.com/mlevkov/iggy_sample"
homepage = "https://github.com/mlevkov/iggy_sample"
Expand Down
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,8 @@ iggy_sample/
│ ├── workflows/
│ │ ├── ci.yml # Main CI (tests, lint, coverage)
│ │ ├── pr.yml # PR checks (size, commits, docs)
│ │ ├── release.yml # Multi-platform release builds
│ │ ├── release.yml # Release builds, GitHub release, docs deploy
│ │ ├── verify-release.yml # Checks each green release run
│ │ └── extended-tests.yml # Weekly stress/benchmark tests
│ ├── dependabot.yml # Automated dependency updates
│ └── pull_request_template.md
Expand Down Expand Up @@ -423,6 +424,8 @@ iggy_sample/
│ ├── streams.rs # Stream management
│ ├── topics.rs # Topic management
│ └── util.rs # Shared handler utilities
├── scripts/
│ └── verify-release.sh # Checks a release run step by step
├── tests/
│ ├── integration_tests.rs # End-to-end API tests
│ ├── metrics_smoke_test.rs # Prometheus exporter smoke test
Expand Down Expand Up @@ -683,22 +686,25 @@ This project uses GitHub Actions for continuous integration and deployment:
|----------|---------|-------------|
| `ci.yml` | Push, PR, weekly | Tests, linting, coverage, dependency policy, security audit |
| `pr.yml` | PR | Size checks, conventional commits, semver |
| `release.yml` | Tag `v*` | Multi-platform builds, GitHub release |
| `release.yml` | Tag `vX.Y.Z` or `vX.Y.Z-<suffix>` | Multi-platform builds, GitHub release, docs deploy |
| `verify-release.yml` | Green Release run, or by hand | Checks the run with `scripts/verify-release.sh` |
| `extended-tests.yml` | Weekly | Benchmarks, stress tests, memory checks |

### Automated Checks
- **Formatting**: `cargo fmt --check`
- **Linting**: `cargo clippy -- -D warnings`
- **Linting**: `cargo clippy -- -D warnings`, and `shellcheck` for `scripts/`
- **Tests**: stable and beta on Linux, macOS and Windows, plus the MSRV on Linux
- **Coverage**: Uploaded to Codecov
- **Coverage**: `cargo llvm-cov`; the Codecov upload is not authenticated
yet (TD-2026-09-03)
- **Dependency policy**: `cargo deny --locked check` (advisories, bans,
licenses, sources)
- **Security**: `cargo-audit` vulnerability scanning; blocking on pushes and
PRs, and the weekly run files an issue per new advisory

### Dependabot
Automatically creates PRs for:
- Cargo dependency updates, direct and transitive (weekly)
- Cargo dependency updates, direct and transitive (weekly), except a
version that needs a newer Rust than the MSRV (TD-2026-07-02)
- GitHub Actions updates (weekly)

## Documentation
Expand Down
4 changes: 3 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ Dependencies are monitored via:
- Dependabot version updates every Monday, for direct and transitive Cargo
dependencies alike: one grouped PR for minor and patch updates, a
separate PR per major bump (a pre-1.0 minor bump such as 0.8 to 0.9
counts as major), and one grouped PR for GitHub Actions
counts as major), and one grouped PR for GitHub Actions. Dependabot
silently skips a version that needs a newer Rust than this crate's
`rust-version` (TD-2026-07-02); the checks below still flag it
- `cargo deny --locked check` in CI, gating every pull request through the
required `CI Success` check and also run weekly: vulnerability,
unmaintained and unsound advisories in any crate fail it, and so does a
Expand Down
Loading
Loading