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.

Live configuration

Release / changelog / announce blocks from cfgd/.anodizer.yaml (snapshot 2026-05-24) — every key in the tables below is wired here. The announce: block has since moved into an includes: fragment, cfgd/.anodizer/announce.yaml.

# Per-crate release section:
release:
  github: { owner: tj-smith47, name: cfgd }
  draft: false
  prerelease: auto
  make_latest: auto
  mode: keep-existing
  target_commitish: "{{ .Commit }}"
  discussion_category_name: "Announcements"
  replace_existing_draft: false
  replace_existing_artifacts: true
  name_template: "{{ ProjectName }} {{ Tag }}"
  header: |
    What's new in {{ .ProjectName }} {{ .Tag }}.
  footer: |
    Released with [anodizer](https://github.com/tj-smith47/anodizer).
  include_meta: true
  extra_files:
    - { glob: "./install.sh", name_template: "install.sh" }

# Top-level changelog (groups + filters):
changelog:
  use: git
  groups:
    - { title: "Features",  regexp: "^.*feat[(\\w)]*:+.*$",  order: 0 }
    - { title: "Bug Fixes", regexp: "^.*fix[(\\w)]*:+.*$",   order: 1 }
    - { title: "Others",    order: 999 }
  filters:
    include: ["^feat", "^fix", "^perf", "^revert"]
    exclude: ["^docs:", "^test:", "^chore:", "^ci:"]

# Top-level announce (only the two live channels):
announce:
  webhook:
    enabled: true
    endpoint_url: "https://tj.jarvispro.io/webhooks/anodizer"
    message_template: '{"project":"{{ ProjectName }}","tag":"{{ Tag }}","url":"{{ ReleaseURL }}"}'
  email:
    enabled: true
    host: smtp.gmail.com
    port: 587
    from: toss45@gmail.com
    to: ["tj@jarvispro.io"]
    subject_template: "{{ ProjectName }} {{ Tag }} released"

cloudsmiths:
  - { id: cfgd, repo: tj-smith47/cfgd, package_format: deb, distros: [ubuntu/jammy] }

Release and changelog

KeyStatusNotes
release.github✅ Verifiedanodizer releases. Header/footer/draft/prerelease/make_latest all exercised
release.metadata✅ Verifiedrelease.include_meta: true attaches v0.23.0 metadata.json to every release. artifacts.json is deliberately not attached — it is the dist directory's local manifest, dropped from the uploadable set in v0.2.0 so include_meta ships exactly one file (see metadata). The v0.1.1 artifacts.json asset is the last one that predates that change
release.name_template / tag_template✅ VerifiedBoth shapes. Lockstep: anodizer .anodizer.yaml sets one crates.tag_template: "v{{ Version }}" that every crate shares, producing a single tag per release. Per-crate: cfgd .anodizer.yaml gives each workspace its own (crd-v / core-v / v / operator-v / csi-v), producing five independent tag families — see cfgd's tags
release.header / footer✅ Verifiedcfgd v0.3.5 release body (What's new header + Released with anodizer footer)
changelog.groups✅ VerifiedThe "Features" and "Bug Fixes" groups, each with its regexp + order, render as headed sections in the v0.23.0 release body
changelog.filters.include / exclude✅ Verifiedanodizer .anodizer.yaml (changelog.filters.include / exclude patterns)
changelog.use: git✅ VerifiedLockstep. Live on every anodizer release — anodizer .anodizer.yaml (use: git) renders the grouped ## Changelog in each release body. Dispatch: crates/stage-changelog/src/run.rs
changelog.use: github-native✅ VerifiedSingle-crate. brontes .anodizer.yaml at v0.2.0 (use: github-native) rendered the v0.1.0 and v0.2.0 release bodies (dogfooded through v0.2.0; brontes moved to use: git for v0.2.1). Code path: crates/stage-changelog/src/github_native.rs
changelog.use: github✅ Verified (tests)crates/stage-changelog/src/run.rs (use: github arm of the source dispatch), covered in crates/stage-changelog/src/tests.rs; no live release selects it
changelog.use: gitlab / gitea✅ Verified (tests)crates/stage-changelog/src/run.rs (gitlab / gitea arms of the source dispatch), covered in crates/stage-changelog/src/tests.rs; we dogfood on GitHub only
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) and a Submitter gate that aborts the Submitter group when required Assets or Manager publishers fail. 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.

The rollback machinery evolved further on 2026-07-20: automatic in-process rollback (--rollback, on_failure: rollback, and the --rollback-only --from-run=<id> replay path) was removed in favor of a convergent model — every publisher implements reconcile(), so a re-run of the identical command safely skips whatever already published. Deliberate withdrawal moved to anodizer tag rollback, which now also unwinds previously-succeeded publishers via the same rollback() overrides the old replay path drove. Rows below that document the removed surface are kept for historical record — the run evidence they cite is real and still demonstrates the failure mode that motivated the redesign — and are marked ⛔ Removed rather than deleted.

KeyStatusNotes
Three-group Submitter gate (default-on)✅ VerifiedFired live in the failed v0.15.1 publish (run 28809062839): after the required gemfury publisher failed, the log shows skipping cargo — gated by an earlier required failure (one-way-door protection) (same for chocolatey and winget) and the run summary records submitter_gated=true. crates/stage-publish/src/dispatch.rs
--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⛔ Removed (2026-07-20)The flag and the automatic rollback it triggered are gone; the replay executor relocated into anodizer tag rollback's publisher-unwind step (still exercises rollback() overrides). Historical evidence — the best-effort rollback path fired live (as the then-default failure policy) in the failed v0.15.1 publish (run 28809062839): required failure(s) detected; invoking best-effort rollbackdispatching rollback for 12 target(s)rollback complete — 7 rolled back, 0 failed, 3 skipped-no-scope. See Release Resilience — Recovering a poisoned tag
--rollback-only --from-run=<id> replay⛔ Removed (2026-07-20)The flag is gone; the same replay executor now runs as anodizer tag rollback's publisher-unwind step, triggered by deliberate withdrawal rather than a CLI flag. Its live coverage moved with it: crates/cli/src/commands/tag/rollback/unwind.rs — both dist layouts discovered from the tag alone, dry-run mutating nothing, dispatch persisting rollback.json, and unreadable state refusing unless --force (unwind::tests::*); end-to-end wiring through the command (commands::tag::rollback::tests::run_unwinds_recorded_publishers_before_deleting_tags), plus the guard-runs-first ordering (crates/cli/src/commands/tag/rollback/tests.rs run_skips_the_unwind_when_the_guard_refuses). The --from-run path-traversal guard survives on promote --from-run, the one remaining consumer (crates/cli/tests/integration.rs::promote_from_run_rejects_path_traversal_at_binary_surface)
--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; the default collect-then-bail mode it inverts is live-proven at run 28809062839
--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. The summary emission itself is live: both v0.15.5 (run 28882554907, wrote ./dist/run-v0.15.5/summary.json) and the failed v0.15.1 run wrote it at the default path, and release.yml uploads it as the run-summary-* artifact — the explicit --summary-json=<path> override flag is what remains unexercised
announce.gate_on config (default required_publishers)✅ VerifiedEvaluated live at v0.15.5 (run 28882554907): with gate_on: required_publishers set in anodizer .anodizer.yaml, the announce stage fired (webhook + email) after all required publishers succeeded and the run summary recorded announce_gated=false. The skip-on-failure branch remains test-proven (crates/stage-announce/src/run.rs announce_skips_when_gate_required_and_required_failure, announce_skips_when_gate_all_and_any_failure)
Preflight rollback-scope checks✅ Verified (tests)Still live post-2026-07-20, retargeted: the checked scope is the credential anodizer tag rollback will need if the release is ever withdrawn, rather than an in-release automatic rollback. crates/stage-publish/src/preflight/checkers.rs — warn / strict-block paths covered in crates/stage-publish/src/preflight/tests.rs by preflight_warns_on_missing_rollback_scope, preflight_blocks_on_missing_rollback_scope_when_strict; the third arm (a required publisher's missing scope bailing the run under the old best-effort policy) went away with automatic rollback, and config::tests::test_validate_on_failure_not_rollback_rejects_root_setting now pins that on_failure: rollback is a hard config error. No live release has tripped the remaining checks
AnnounceStage emit-summary-on-skip✅ Verifiedcrates/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. Fired live in the failed v0.15.1 publish (run 28809062839): the pipeline failed before the announce stage ran, yet wrote ./dist/run-v0.15.1/summary.json still emitted the full status table
BlobStage writes to ctx.publish_report✅ VerifiedLive on every release since MinIO blob upload went in: v0.15.5 (run 28882554907) records blob Assets required succeeded in the run summary, and the failed v0.15.1 run records blob Assets required rolled-back — both only possible via the publish-report entry the stage writes. crates/stage-blob/src/run.rs
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/mod.rs) regardless of the publish: flag, preventing the v0.2.0 double-upload regression (commit b3791cf). Live behavior matches: the v0.15.5 publish (run 28882554907) shows exactly one snap publish stage and a single snapcraft row in the run summary
Required-failure → non-zero exit gate✅ Verifiedcrates/stage-publish/src/lib.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. Fired live in both directions: the failed v0.15.1 publish (run 28809062839) exited non-zero on 1 required publisher(s) failed: gemfury (job failed with exit code 1), while v0.15.5 (run 28882554907) stayed green despite an optional snapcraft failure — the optional-failure branch live
--strict (fold with preflight.strict)✅ Verified (tests)crates/core/src/context/mode.rs (Context::preflight_is_strict, the effective-strictness combiner) — either flag turns strict on, covered by crates/cli/src/commands/release/tests.rs::strict_or_config_strict_promotes_preflight_to_strict; promotion of indeterminate credential/permission probes (5xx / rate-limit / inconclusive) to blockers covered by crates/core/src/git/github_probe.rs strict_promotes_indeterminate_arms_to_blocker; mutex with --allow-nondeterministic covered by crates/cli/tests/integration.rs::release_strict_conflicts_with_allow_nondeterministic
Convergent re-run (per-publisher reconcile())✅ Verified (tests)crates/core/src/publisher.rs (ReconcileState) + crates/stage-publish/src/dispatch.rs — the reconcile arm's Absent / Complete / Diverged / Unknown branches, its dry-run and simulate_failure bypasses, and required-vs-optional Diverged handling are covered by the reconcile* unit tests in the same crate; per-publisher coverage for cargo, chocolatey, npm, pypi, winget, homebrew, homebrew-core, scoop, nix and krew sits beside each publisher. This replaces the former --allow-rerun refuse-guard: re-running is now the safe happy path, and crates/cli/tests/integration.rs::test_release_allow_rerun_flag_is_rejected_as_unknown pins that the removed flag hard-errors instead of silently no-op'ing
Real DELETE rollback for blobs[]⏳ PendingThe rollback path was exercised live in the failed v0.15.1 publish (run 28809062839): structured evidence drove per-object DELETE s3://…/v0.15.1/<asset> attempts against all 47 uploaded keys — but every delete failed against MinIO (Generic S3 error … builder error), each surfaced as a manual cleanup may be required warning as designed (blob rollback complete — 0 deleted, 0 already absent, 47 failed). A successful live deletion is still unproven. Unit coverage: crates/stage-blob/src/publisher.rs blob_publisher_rollback_decodes_structured_targets_and_attempts_delete et al. (commit 1195ce5)
Real DELETE rollback for cloudsmiths[]✅ Verified (tests)crates/stage-publish/src/cloudsmith/publisher.rs — slug captured at upload time round-trips through PublishEvidence.extra so rollback can issue real DELETE against the Cloudsmith API; coverage in crates/stage-publish/src/cloudsmith/publisher_tests.rs 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✅ Verifiedcrates/stage-publish/src/run_summary.rs (status_table_rows) — width adapts to the longest publisher name (capped at 40 chars with UTF-8-safe ellipsis truncation); covered by status_table_rows_render_per_publisher_and_run_flags, status_table_rows_keep_long_names_untruncated_under_cap, status_table_rows_truncate_extremely_long_names (commit 52c51da). Rendered live at the end of both v0.15.5 (run 28882554907) and the failed v0.15.1 run — 17-publisher status table with per-group/required/status columns
One-way-door burn probes (crates.io / Chocolatey / winget)✅ Verified (tests)crates/stage-publish/src/cargo/already_published.rs, crates/stage-publish/src/chocolatey/mod.rs, crates/stage-publish/src/post_publish/winget.rs — before rollback touches an irreversible registry, a probe checks whether the version already landed (crates.io index, Chocolatey package page scrape — the flat OData feed hides pending moderation — and the winget upstream PR search). Positive evidence refuses rollback; ambiguity stays clear. No live release has tripped a burn probe yet — in the one live failure so far (run 28809062839) the Submitter gate stopped cargo/chocolatey/winget before they published, so there was no landed one-way-door publish for a probe to check
release.on_failure policy (rollback | hold) + auto-degrade past burned doors⛔ Removed (2026-07-20)on_failure: rollback and the auto-degrade-to-hold logic are gone; hold is now the only accepted value, and rollback is a hard config-validation error. Historical evidence for the removed rollback path: it fired live in the failed v0.15.1 publish (run 28809062839) — rollback executed in-process and the publish-only hold rule kept the pre-existing tag + bump commit in place (publish-only run failed — holding the already-released tag and version-bump commit in place …). hold itself remains live and is wired in brontes .anodizer.yaml (release.on_failure: hold). See Release Resilience — release.on_failure
Machine-readable exit contract (exit 2 + anodizer-error-class: deterministic)✅ Verified (tests)crates/cli/tests/exit_class_integration.rs — config/CLI/flag errors that retrying can never fix exit 2 and stamp a stderr marker; transient failures keep exit 1. Retry wrappers (including anodizer-action) key off both. Consistent live: the failed v0.15.1 publish (run 28809062839) exited 1 (transient publisher failure); the deterministic exit-2 branch has not fired live
Run-wide retry budget bounding publisher ladders✅ Verified (tests)crates/core/src/config/retry.rs — a retry.max_elapsed wall-clock budget (default 15 min, raisable) bounds every publisher's retry ladder and the deadline-aware HTTP retry wrappers, so a transient storm fails cleanly instead of multiplying per-publisher ladders. Wired live in anodizer .anodizer.yaml (retry.max_elapsed: 15m); a live retry ladder ran under it at v0.15.5 (run 28882554907, snapcraft upload attempt 1/10 failed (5xx), retrying…) — the budget ceiling itself has never been reached
Retry backoff attribution in the run summary✅ Verified (tests)crates/core/src/retry/mod.rs + crates/stage-publish/src/run_summary.rs — total backoff wait is accounted run-wide and attributed per publisher/stage in the summary, so a slow release is explainable from the audit artifact. v0.15.5's snapcraft retries (run 28882554907) accrued real backoff, but that run's summary.json (written by the pre-attribution v0.15.4 binary) carries no backoff fields — the attribution landed after v0.15.5 and awaits the next release for live proof
Liveness heartbeat during slow subprocess waits✅ Verified (tests)crates/core/src/progress.rs — long-running stage subprocesses emit a still … line on a fixed cadence so a legitimate slow wait is distinguishable from a hang; cadence override/disable via env
Snap Store review-hold surfacing✅ Verified (tests)crates/stage-snapcraft/src/publish_stage/mod.rs — an upload parked in manual review now reports HELD at default visibility (per upload + end-of-stage rollup) and stamps held_for_review on the evidence snapshot, instead of riding the success path. Motivated by a real incident: cfgd v0.5.0 went green while the store stayed at 0.3.5
Snap Store channel-map landing probe (verify_release)✅ Verified (tests)crates/stage-verify-release/src/snap_store.rs — verify-release asserts each uploaded snap is actually live in the store's channel map, catching a review hold that parked the revision outside every channel. Not yet exercised live: at v0.15.5 (run 28882554907) the snap upload itself failed (Store 5xx + upload-uniqueness error), so verify-release had no landed snap to probe
Landed release-asset verification✅ VerifiedLive at v0.15.5 (run 28882554907): with verify_release.assert_assets: true the verify stage fetched the published asset set via crates/stage-release/src/github/lookup.rs (fetch_published_assets) and re-checked the landed set — Verifying release … all post-publish checks passed, summary line verify-release passed
Emission-validate shard accountability✅ Verified (tests)crates/stage-publish/src/snapshot_validation.rs — sharded / target-restricted builds emit an aggregate validated emissions … (skipped M expectations …) result line via a per-expectation skip tally, so a partial-target validation is never mistaken for a full-set one; dry-run URL derivation validates publish URLs without network

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✅ Verifiedanodizer release.yml (determinism-check: calls the reusable determinism.yml 4-shard matrix on every tag; the release: job consumes the preserved dist via release --publish-only)
anodize check config (post-restructure)🤝 Help wantedcrates/cli/src/commands/check/config/mod.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/drift.rs (inject_drift_byte) + env-gate in crates/cli/src/commands/check/determinism/mod.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::drift::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/builtin.rs — CycloneDX output byte-stable for the same timestamp + honors / varies with SDE; coverage in crates/stage-sbom/src/tests.rs via cyclonedx_output_byte_stable_for_same_timestamp, sbom_metadata_timestamp_honors_sde, sbom_metadata_timestamp_varies_with_sde (commit 4a34d1a)

Announcers

13 channels implemented. Two run on live releases of both anodizer and cfgd; the other eleven have full test coverage but no live secrets configured.

KeyStatusNotes
announce.webhook✅ VerifiedLockstep and per-crate. anodizer .anodizer.yaml posts a JSON payload (project / tag / url) with an X-Anodizer-Source: release header and expected_status_codes: [200, 202]; it fired at v0.15.5 (run 28882554907) once the required publishers passed the gate_on check. cfgd wires the same endpoint from an includes: fragment, .anodizer/announce.yaml
announce.smtp✅ VerifiedLockstep and per-crate. anodizer .anodizer.yaml (Gmail SMTP, port 587, encryption: starttls) sent alongside the webhook at v0.15.5 (run 28882554907); cfgd wires the same channel in .anodizer/announce.yaml
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)✅ VerifiedThe S3 provider runs live on every anodizer release against a self-hosted MinIO: v0.15.5 (run 28882554907) logged uploaded 47 object(s), skipped 0 (identical) → s3://…/v0.15.5, and the run summary records per-object s3://anodizer-releases/v0.15.5/… evidence. GCS and Azure providers share the object_store code path but have no live deployment
artifactories[]🤝 Help wantedTarget, mode, TLS, headers wired and configured (disabled) in anodizer .anodizer.yaml (artifactories: … skip: true — no Artifactory instance); live runs evaluate the entry and skip it (skipped artifactory entry 'production' — skip condition evaluated truthy)
uploads[]✅ VerifiedLive on every anodizer release: the jarvispro entry in anodizer .anodizer.yaml HTTP-PUTs the CLI archives + checksums to a self-hosted mirror — v0.15.5 (run 28882554907) logged uploading 3 artifacts to 'jarvispro' (mode=archive)uploaded 3 artifact(s), skipped 0 (already present) → jarvispro, summary uploads Assets optional succeeded
uploads[].exclude / per-destination exclude globs✅ Verified (tests)crates/core/src/config/upload.rs (exclude: ["*.sha256", "*.sig", ...] drops matching assets for one destination without touching the others). No live deployment
gemfury[] (alias furies[])🤝 Help wantedThe publisher live-fired at v0.15.1 (run 28809062839): deb/rpm/apk pushes to https://push.fury.io/tj-smith47 were dispatched but every push 403'd (account access denied — the free-tier account is over quota), which is the required failure that triggered that run's rollback. The entry is now disabled in anodizer .anodizer.yaml (gemfury: … skip: true); a successful live publish needs a paid Fury plan. See gemfury: docs
cloudsmiths[]✅ VerifiedThe jarvispro/anodizer Cloudsmith repo carries live anodizer packages in all three configured formats (deb + rpm + alpine, at the latest release version; via GET /v1/packages/jarvispro/anodizer/). Wired in anodizer's config (cloudsmiths: organization jarvispro, repository anodizer, per-format distributions). Real DELETE rollback is test-covered (see Release resilience below). See cloudsmiths: docs

Custom publishers

KeyStatusNotes
publishers[]✅ Verifiedcrates/cli/src/commands/publisher.rs (custom command per artifact)
Submission attribution (PR footers + generated-file headers)✅ VerifiedThe generated-file header is live in the tap: Casks/anodizer.rb (v0.23.0) opens with # This file was generated by anodizer (https://github.com/tj-smith47/anodizer). DO NOT EDIT.. crates/stage-publish/src/util/attribution.rs; the Automatically submitted by anodizer PR footer lands on the next external-index submission PR

MCP registry

Publishes an MCP server manifest to https://registry.modelcontextprotocol.io. The manifest points at ghcr.io/tj-smith47/anodizer:<version>, the multi-arch OCI image built by dockers_v2:. The image's ENTRYPOINT runs the anodizer binary and CMD defaults to mcp (see Dockerfile), so consumers docker run --rm -i ghcr.io/tj-smith47/anodizer:<ver> and the container speaks MCP over stdio out of the box.

The registry has served anodizer since 0.6.0 and now holds 34 published versions through 0.23.0 — one per release, each pointing at that release's OCI image. Every publish authenticates by exchanging the workflow's GitHub OIDC token; no registry credential is stored.

KeyStatusNotes
mcp.name✅ Verifiedio.github.tj-smith47/anodizer is the live server name in the registry, set by anodizer .anodizer.yaml
mcp.packages[]✅ VerifiedEach published version carries registry_type: oci + identifier: ghcr.io/tj-smith47/anodizer + transport: stdio — visible in the registry entries, resolving to the image dockers_v2: pushed for that release
mcp.auth.type: github-oidc✅ VerifiedThe only auth path any of the 34 published versions used: crates/stage-publish/src/mcp/auth.rs exchanges the Actions id-token, and publish-oidc.yml grants the id-token: write it needs
mcp.auth.type: none✅ Verified (tests)crates/stage-publish/src/mcp/auth.rs (None branch) — unit-tested; private mirrors only
mcp.auth.type: github✅ Verified (tests)crates/stage-publish/src/mcp/auth.rs (PAT exchange branch) — unit-tested; for non-GHA CI
mcp.repository✅ Verifiedanodizer .anodizer.yaml (url: https://github.com/tj-smith47/anodizer, source: github) renders into the repository block of every live registry entry; manifest assembly at crates/stage-publish/src/mcp/manifest.rs
mcp.skip (tera, accepts disable: alias)✅ Verified (tests)crates/stage-publish/src/mcp/mod.rs — unit-tested; not used in production (block is unconditionally enabled)
OCI version field omitted✅ Verified (tests)Per commit 596e1a3: OCI registry types get an empty version field on the published manifest — the registry resolves the version from the image tag. Other registry types (npm, pypi, ...) receive the release version verbatim