Skip to main content

Release pipeline

Release-pipeline config keys: release.*, changelog.*, announce.*, blobs[], publishers[].

Release pipeline

The keys that drive the release itself: GitHub/GitLab/Gitea release surface, changelog generation, announcers, cloud uploads, and custom publishers.

Release and changelog

KeyStatusNotes
release.github✅ Verifiedanodizer releases. Header/footer/draft/prerelease/make_latest all exercised
release.metadata✅ Verifiedv0.1.1 metadata.json · artifacts.json
release.name_template / tag_template✅ Verifiedcfgd .anodizer.yaml (tag_template: "core-v{{ Version }}" / "v{{ Version }}" / "operator-v{{ Version }}" / "csi-v{{ Version }}")
release.header / footer✅ Verifiedcfgd v0.3.5 release body (What's new header + Released with anodizer footer)
changelog.groups✅ Verified"Features" / "Bug Fixes" / "Others" sections in the v0.1.1 release body
changelog.filters.include / exclude✅ Verifiedanodizer .anodizer.yaml (changelog.filters.include / exclude patterns)
changelog.use: git✅ Verifiedcrates/stage-changelog/src/lib.rs (use: git branch)
changelog.use: github-native✅ Verifiedcrates/stage-changelog/src/lib.rs (use: github-native branch)
changelog.use: github✅ Verifiedcrates/stage-changelog/src/lib.rs (use: github branch)
changelog.use: gitlab / gitea✅ Verifiedcrates/stage-changelog/src/lib.rs (gitlab / gitea branches)
changelog.use: ai🤝 Help wantedanthropic / openai / ollama implemented; no live release uses it
release.gitlab🤝 Help wantedWe dogfood on GitHub only
release.gitea🤝 Help wantedWe dogfood on GitHub only
milestones[]✅ Verifiedcrates/core/src/config/milestone.rs

Release resilience

These features shipped 2026-05-14 in response to the anodize v0.2.0 cascade failure (Run 25754442852 and four siblings on 2026-05-12, all failing in the publish stage). They form three-group publisher dispatch (Assets, Manager, Submitter), a Submitter gate that aborts the Submitter group when required Assets or Manager publishers fail, opt-in rollback per-publisher, and a --rollback-only --from-run=<id> replay path. Several behaviors have unit/integration test coverage today (rows marked ✅ Verified (tests) below); rows that need a live v0.2.x+ tag to exercise the codepath stay 🤝 Help wanted.

KeyStatusNotes
Three-group Submitter gate (default-on)✅ Verified (tests)crates/stage-publish/src/dispatch.rs — Assets / Manager / Submitter groups wired and gate verified via dispatch::tests::submitter_gate_skips_submitter_when_required_manager_fails; first v0.2.x release confirms end-to-end
--no-gate-submitter override✅ Verified (tests)crates/stage-publish/src/dispatch.rs (dispatch::tests::no_gate_submitter_runs_submitter_anyway) + CLI parse (crates/cli/src/main.rs::tests::release_parses_no_gate_submitter_flag); awaits a live release that flips the override
--rollback=best-effort✅ Verified (tests)crates/stage-publish/src/rollback.rs — per-publisher rollback path verified via preflight::tests::preflight_bails_when_required_publisher_missing_scope_and_rollback_best_effort + CLI parse (crates/cli/src/main.rs::tests::release_parses_rollback_best_effort); no live release has rolled back yet
--rollback-only --from-run=<id> replay✅ Verified (tests)crates/stage-publish/src/rollback_only.rs — idempotency + read/dispatch covered by rollback_only::tests::rollback_only_reads_report_and_dispatches, rollback_only_second_invocation_is_noop_for_already_rolled_back_entries, plus path-traversal guard at the binary surface (crates/cli/tests/integration.rs::release_from_run_rejects_path_traversal_at_binary_surface, release_rollback_only_invokes_replay_from_disk) and crates/stage-publish/tests/run_report_persistence.rs::publish_stage_writes_report_and_rollback_only_can_read_it
--fail-fast✅ Verified (tests)anodize .anodizer.yaml plus release command wiring (fail_fast opts); dispatcher coverage via dispatch::tests::fail_fast_aborts_at_first_error — pre-resilience-work flag, exercised in v0.1.x runs but no live v0.2.x release has tripped it
--summary-json=<path> audit-trail✅ Verified (tests)crates/stage-publish/src/run_summary.rs — JSON schema v1 round-trip + writer covered by run_summary::tests::run_summary_schema_v1_roundtrips_through_json, run_summary_rejects_unknown_fields, write_summary_json_creates_parent_dir; CLI parse at crates/cli/src/main.rs::tests::release_parses_summary_json; no v0.2.x release has emitted one yet
announce.gate_on config (default required_publishers)✅ Verified (tests)crates/stage-announce/src/run.rs — gate evaluation covered by run::tests::announce_skips_when_gate_required_and_required_failure, announce_skips_when_gate_all_and_any_failure, announce_gate_serializes_as_snake_case; no post-merge release has gated an announce on publisher health
Preflight rollback-scope checks✅ Verified (tests)crates/stage-publish/src/preflight.rs — warn / strict-block / best-effort-bail paths covered by preflight::tests::preflight_warns_on_missing_rollback_scope, preflight_blocks_on_missing_rollback_scope_when_strict, preflight_bails_when_required_publisher_missing_scope_and_rollback_best_effort; no live release has tripped them
AnnounceStage emit-summary-on-skip✅ Verified (tests)crates/stage-announce/src/run.rs — emit-on-gate-skip + emit-when-stage-not-called covered by run::tests::emit_summary_writes_when_gate_would_fire, emit_summary_writes_when_announce_stage_was_not_called, emit_summary_writes_summary_when_path_set, plus integration test crates/cli/tests/integration.rs::test_release_skip_announce_still_writes_summary_json; no v0.2.x release has skipped an announce yet
BlobStage writes to ctx.publish_report✅ Verified (tests)crates/stage-blob/src/run.rs — publish-report append + BlobConfig.required gating covered by tests::blob_stage_appends_succeeded_to_publish_report, blob_stage_appends_failed_to_publish_report, blob_stage_initializes_publish_report_when_none, record_blob_result_required_false_by_default, record_blob_result_failed_required_blob_trips_assets_required_gate; awaits a release with cloud blob credentials configured
Snapcraft double-publish fix (SnapcraftPublisher unregistered unconditionally)✅ Verified (tests)crates/stage-publish/src/registry.rsregistry::tests::snapcraft_unconditionally_unregistered_regardless_of_publish_flag asserts the registry never re-registers SnapcraftPublisher alongside the load-bearing SnapcraftPublishStage (crates/stage-snapcraft/src/publish_stage.rs) regardless of the publish: flag, preventing the v0.2.0 double-upload regression (commit b3791cf)
Required-failure → non-zero exit gate✅ Verified (tests)crates/cli/src/commands/release/mod.rs (gate_required_failures) — short-circuit / snapshot / dry-run / rollback-failed / optional-failure / missing-report branches covered by unit tests in the same module; commit 1d9a13e. No live release has emitted a non-zero exit yet
--strict / --strict-preflight✅ Verified (tests)crates/cli/src/commands/release/mod.rs (strict_preflight opt + opts.strict || opts.strict_preflight combiner) — promotion of PublisherState::Unknown preflight results to blockers covered by strict_or_strict_preflight_promotes_unknown_to_blocker; mutex with --allow-nondeterministic covered by crates/cli/tests/integration.rs::release_strict_conflicts_with_allow_nondeterministic
--allow-rerun + end-of-pipeline rerun guard✅ Verified (tests)crates/stage-publish/src/lib.rs (refuse_rerun_if_report_exists) — first-run / re-run / allow-rerun-override / snapshot / dry-run / rollback-only / local-run-id branches covered by sibling unit tests; mutual exclusion with --rollback-only asserted by crates/cli/tests/integration.rs::test_release_allow_rerun_conflicts_with_rollback_only
Real DELETE rollback for blobs[]✅ Verified (tests)crates/stage-blob/src/publisher.rs — structured BlobTarget evidence drives rollback_via_object_store (real ObjectStore::delete); evidence-carry + delete dispatch covered by tests::blob_publisher_records_only_uploaded_keys, blob_publisher_rollback_decodes_structured_targets_and_attempts_delete, blob_publisher_rollback_falls_back_to_warn_for_legacy_evidence, and the serde round-trips at blob_target_serde_roundtrip / blob_target_decode_* (commit 1195ce5)
Real DELETE rollback for cloudsmiths[]✅ Verified (tests)crates/stage-publish/src/cloudsmith.rs — slug captured at upload time round-trips through PublishEvidence.extra so rollback can issue real DELETE against the Cloudsmith API; coverage via cloudsmith_target_serde_roundtrip_with_slug, cloudsmith_target_decode_tolerates_missing_slug_field, cloudsmith_target_decode_tolerates_null_slug, cloudsmith_rollback_falls_back_to_warn_when_slug_missing, cloudsmith_rollback_warns_when_no_targets_recorded (commit 8a79bf1)
RunSummary dynamic-width status table✅ Verified (tests)crates/stage-publish/src/run_summary.rs (print_status_table) — width adapts to the longest publisher name (capped at 40 chars with UTF-8-safe ellipsis truncation); covered by print_status_table_renders_human_readable, print_status_table_widens_for_long_publisher_names, print_status_table_truncates_extremely_long_names (commit 52c51da)

Test-harness-only flags (--simulate-failure, --inject-drift) are intentionally omitted from this matrix — they exist for regression coverage only and require ANODIZE_TEST_HARNESS=1 to be honored. Operators will never run them in production.

Build determinism

Byte-stability contract plus a check determinism harness, an operator --allow-nondeterministic <name>=<reason> escape, and a release-body "Non-deterministic exemptions:" block that lists any waived artifacts. Merged 2026-05-14; rows fill in as v0.2.x+ releases exercise each surface.

KeyStatusNotes
anodize check determinism --runs=N harness🤝 Help wantedcrates/cli/src/commands/check/determinism.rs - N-run harness wired; not yet invoked from a tagged release run
anodize check config (post-restructure)🤝 Help wantedcrates/cli/src/commands/check/config.rs - post-restructure config validator; no release has exercised the new surface yet
--allow-nondeterministic <name>=<reason>🤝 Help wantedOperator escape parsed and threaded through the build stage; rejection paths covered by crates/cli/tests/integration.rs::release_allow_nondeterministic_rejects_no_eq, release_allow_nondeterministic_rejects_empty_reason, release_strict_conflicts_with_allow_nondeterministic; no live release has waived an artifact yet
"Non-deterministic exemptions:" block in release body🤝 Help wantedcrates/stage-release/src/release_body.rs - emitter wired; release body fragment unused until an exemption ships
--inject-drift=archive|sbom test seam (ANODIZE_TEST_HARNESS=1 gated)✅ Verified (tests)crates/cli/src/determinism_harness.rs (inject_drift_byte) + env-gate in crates/cli/src/commands/check/determinism.rs — end-to-end drift detection covered by crates/cli/tests/check_determinism.rs::inject_drift_archive_reports_drift_on_minimal_workspace and the unit-level mutation seam at determinism_harness::tests::inject_drift_byte_mutates_file_so_hash_differs
Snapshot SOURCE_DATE_EPOCH resolver✅ Verified (tests)crates/core/src/git/snapshot_sde.rs (resolve_snapshot_sde) — env-override / clean-tree-HEAD / dirty-tree-hash / stability branches covered by snapshot_sde_uses_env_var_when_set, snapshot_sde_uses_head_when_tree_clean, snapshot_sde_uses_dirty_tree_hash_when_tree_dirty, snapshot_sde_is_stable_for_unchanged_dirty_tree (commit 5ad6a76)
SBOM byte-stability under SOURCE_DATE_EPOCH✅ Verified (tests)crates/stage-sbom/src/lib.rs — CycloneDX output byte-stable for the same timestamp + honors / varies with SDE; coverage via cyclonedx_output_byte_stable_for_same_timestamp, sbom_metadata_timestamp_honors_sde, sbom_metadata_timestamp_varies_with_sde (commit 4a34d1a)
GPG --faked-system-time preflight probe✅ Verified (tests)crates/core/src/signing.rs (gpg_supports_faked_system_time + _with injection seam) routes through crates/core/src/tool_detect.rs (tool_runs_with_args) to satisfy module-boundaries; coverage in both modules' unit tests (commits c649b3e, 8803c68)

Announcers

13 channels implemented. Two are exercised by live cfgd releases; the others have full test coverage but no live secrets configured.

KeyStatusNotes
announce.webhook✅ Verifiedcfgd .anodizer.yaml (announce.webhook.endpoint_url: https://tj.jarvispro.io/webhooks/anodizer)
announce.smtp✅ Verifiedcfgd .anodizer.yaml (announce.smtp.host: smtp.gmail.com)
announce.discord🤝 Help wantedNo live workflow has the secrets
announce.slack🤝 Help wantedNo live workflow has the secrets
announce.telegram🤝 Help wantedNo live workflow has the secrets
announce.teams🤝 Help wantedNo live workflow has the secrets
announce.mattermost🤝 Help wantedNo live workflow has the secrets
announce.reddit🤝 Help wantedNo live workflow has the secrets
announce.twitter🤝 Help wantedNo live workflow has the secrets
announce.mastodon🤝 Help wantedNo live workflow has the secrets
announce.bluesky🤝 Help wantedNo live workflow has the secrets
announce.linkedin🤝 Help wantedNo live workflow has the secrets
announce.opencollective🤝 Help wantedNo live workflow has the secrets
announce.discourse🤝 Help wantedNo live workflow has the secrets

Blob and artifactory uploads

KeyStatusNotes
blobs[] (S3 / GCS / Azure)🤝 Help wantedobject_store SDK wired. No release configures cloud credentials
artifactories[]🤝 Help wantedTarget, mode, TLS, headers wired; no live deployment
uploads[]🤝 Help wantedGeneric HTTP upload wired; no live deployment
furies[]🤝 Help wantedImplemented; no live credentials
cloudsmiths[]🤝 Help wantedWired in cfgd's config with a live CLOUDSMITH_TOKEN; uploads currently fail at HTTP layer so no package has landed in the jarvispro/cfgd repo. Awaiting endpoint debug

Custom publishers

KeyStatusNotes
publishers[]✅ Verifiedcrates/cli/src/commands/publisher.rs (custom command per artifact)

MCP registry

Publishes an MCP server manifest to https://registry.modelcontextprotocol.io.

Implementation is feature-complete with unit-test coverage of every branch (auth providers, retry policy, dry-run, repository inference). Dogfooding is held: anodizer's own .anodizer.yaml declares packages[0].registry_type: oci with identifier: ghcr.io/tj-smith47/anodizer, but the project ships binary archives and does not yet have a dockers: block. Publishing this manifest today would point MCP clients at a 404, so the mcp: block is marked skip: true until anodizer ships an OCI image (via a dockers: block) or the package is pivoted to a registry type the project actually distributes.

KeyStatusNotes
mcp.name🤝 Help wantedWired in anodizer .anodizer.yaml; blocked on dockers: block / first live publish
mcp.packages[]🤝 Help wantedWired in anodizer .anodizer.yaml (packages[].registry_type: oci); blocked on dockers: block / first live publish
mcp.auth.type: none🤝 Help wantedcrates/stage-publish/src/mcp/auth.rs (None branch) — unit-tested; blocked on dockers: block before dogfood publish
mcp.auth.type: github🤝 Help wantedcrates/stage-publish/src/mcp/auth.rs (PAT exchange branch) — unit-tested; blocked on dockers: block before dogfood publish
mcp.auth.type: github-oidc🤝 Help wantedcrates/stage-publish/src/mcp/auth.rs (OIDC id-token branch); blocked on dockers: block before dogfood publish
mcp.repository🤝 Help wantedcrates/stage-publish/src/mcp/manifest.rs — unit-tested; blocked on dockers: block before dogfood publish
mcp.skip (tera, accepts disable: alias)🤝 Help wantedcrates/stage-publish/src/mcp/mod.rs — unit-tested; blocked on dockers: block before dogfood publish