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
10 changes: 8 additions & 2 deletions .github/workflows/dependabot-lock-resync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,14 @@ jobs:
# seconds earlier. Observed on #801:
# 00:04:30 triggering_actor=dependabot[bot] -> success, pushed the resync commit
# 00:04:48 triggering_actor=mefor-lock-resync[bot] -> failure, "client-id ... is required"
# Skipping the self-triggered re-run is correct: the work is already done. The secrets are
# configured correctly under Settings > Secrets and variables > Dependabot; nothing to add.
# Skipping the self-triggered re-run is correct: the work is already done.
#
# (This line used to end "the secrets are configured correctly ... nothing to add". True of the
# PRIVATE repo, and it did NOT survive the cutover: MEFORORG carried only MEFOR_FORBIDDEN_TOKENS,
# so the App credentials were absent and this job would have skipped on every Dependabot uv PR
# while the comment asserted it was configured. LOCK_RESYNC_APP_ID + LOCK_RESYNC_APP_PRIVATE_KEY
# were added to MEFORORG > Secrets > Dependabot on 2026-07-27. State a configuration fact WITH
# the repository it applies to, or a migration turns it into a confident lie.)
if: >-
${{ github.event.pull_request.user.login == 'dependabot[bot]'
&& github.triggering_actor == 'dependabot[bot]' }}
Expand Down
38 changes: 30 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,18 @@ jobs:
with:
subject-path: "dist/*.tar.gz, dist/*.whl, messagefoundry-sbom.cdx.json, messagefoundry-vex.openvex.json"

- name: Create GitHub release
# CREATE-OR-UPDATE, not create. This was a bare `gh release create`, which fails with "a release
# with the same tag name already exists" on any re-run — and because it sits BEFORE the PyPI
# publish below, that turned one publish failure into a permanent retry deadlock: every re-run
# died here and SKIPPED the publish, so a re-run could not even test the fix it was meant to
# verify. Observed on v0.3.1 — attempt 1 created the release then failed at PyPI (403, publisher
# mismatch); attempts 2-3 died here; only deleting the public release by hand let attempt 4 through.
#
# The step ORDER is deliberate and stays: the reversible action (a GitHub release, deletable)
# precedes the irreversible one (a PyPI upload burns that version forever). Publishing first would
# leave a failure here with an un-retryable PyPI version and no release. The defect was never the
# ordering — it was that the reversible half could not be repeated.
- name: Create or update the GitHub release
if: startsWith(github.ref, 'refs/tags/')
env:
GH_TOKEN: ${{ github.token }}
Expand All @@ -248,13 +259,24 @@ jobs:
# Release notes = this version's CHANGELOG section (fall back to a one-liner).
awk -v v="$ver" 'BEGIN{f=0} /^## \[/{ if(f) exit; if(index($0,"["v"]")) f=1 } f{print}' CHANGELOG.md > notes.md
[ -s notes.md ] || echo "MessageFoundry $tag" > notes.md
pre=""
case "$ver" in *-*) pre="--prerelease";; esac
gh release create "$tag" \
dist/*.tar.gz dist/*.whl dist/*.sigstore* \
messagefoundry-sbom.cdx.json messagefoundry-sbom.cdx.json.sigstore* \
messagefoundry-vex.openvex.json messagefoundry-vex.openvex.json.sigstore* \
--title "$tag" --notes-file notes.md $pre
# `edit` needs an explicit true/false: a bare `--prerelease` only ever SETS the flag, so a
# re-run could never demote a release that had been mis-marked as a pre-release.
case "$ver" in
*-*) pre_create="--prerelease"; pre_edit="--prerelease=true" ;;
*) pre_create=""; pre_edit="--prerelease=false" ;;
esac
assets=( dist/*.tar.gz dist/*.whl dist/*.sigstore*
messagefoundry-sbom.cdx.json messagefoundry-sbom.cdx.json.sigstore*
messagefoundry-vex.openvex.json messagefoundry-vex.openvex.json.sigstore* )
if gh release view "$tag" >/dev/null 2>&1; then
echo "Release $tag already exists — updating in place (this is a re-run)."
gh release edit "$tag" --title "$tag" --notes-file notes.md "$pre_edit"
# --clobber: re-uploading an existing asset name errors otherwise, and a re-run regenerates
# every artifact (fresh signatures), so replacing is the correct semantic.
gh release upload "$tag" "${assets[@]}" --clobber
else
gh release create "$tag" "${assets[@]}" --title "$tag" --notes-file notes.md $pre_create
fi

- name: Upload artifacts (dry-run / always)
if: always()
Expand Down
31 changes: 30 additions & 1 deletion tests/test_release_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,11 @@ def test_release_pypi_publish_is_last_step_and_tag_gated() -> None:
)

# Publish (irreversible) must come AFTER build, leak-gate, sign and the GitHub release.
#
# This order is load-bearing, not cosmetic: the GitHub release is REVERSIBLE (deletable) and the
# PyPI upload is not (a version number is burned forever). Doing the reversible half first is what
# made the v0.3.1 publisher failure recoverable at all — a failed publish left a release we could
# delete and retry, rather than an un-retryable PyPI version with no release.
def idx(tok: str) -> int:
i = release_job.find(tok)
assert i != -1, f"expected marker missing from release job: {tok!r}"
Expand All @@ -208,7 +213,7 @@ def idx(tok: str) -> int:
idx("Build sdist + wheel"),
idx("Leak gate — sdist MUST be package-only"),
idx("python -m sigstore sign"),
idx("Create GitHub release"),
idx("Create or update the GitHub release"),
idx("Publish to PyPI"),
]
assert order == sorted(order), (
Expand Down Expand Up @@ -270,6 +275,30 @@ def test_release_jobs_are_gated_ON_the_source_repo() -> None:
assert "wshallwshall" not in rel, "release.yml must not reference the retired private vault"


def test_the_github_release_step_is_idempotent() -> None:
"""A re-run must be able to repeat the release step, or a publish failure wedges the tag forever.

The step sits BEFORE the irreversible PyPI upload (deliberately — see the ordering test above), so
when it was a bare `gh release create` the first publish failure was terminal: the release now
existed, so every re-run died on "a release with the same tag name already exists" and SKIPPED the
publish. The retry could not even reach the thing it was retrying. v0.3.1 needed a human to delete
a public release before attempt 4 could get through.
"""
rel = _release()
assert "gh release view" in rel, (
"the release step no longer probes for an existing release — a re-run will fail on 'already "
"exists' and skip the PyPI publish below it"
)
assert "gh release edit" in rel and "--clobber" in rel, (
"create-or-update is incomplete: an existing release must be edited and its assets replaced "
"(--clobber), since a re-run regenerates every artifact with fresh signatures"
)
# `gh release edit --prerelease` (bare) only ever SETS the flag; demoting needs an explicit value.
assert "--prerelease=true" in rel and "--prerelease=false" in rel, (
"edit uses a bare --prerelease, so a re-run could never demote a mis-marked pre-release"
)


def test_no_self_referential_slug_rewrite_survives() -> None:
"""The README slug rewrite is GONE, and no `sed s#X#X#` may come back.

Expand Down
Loading