Skip to content

fix(release): resume immutable v0.5.0 publication - #282

Merged
DecisionNerd merged 1 commit into
mainfrom
fix/281-v050-publish-recovery
Aug 1, 2026
Merged

DecisionNerd merged 1 commit into
mainfrom
fix/281-v050-publish-recovery

Conversation

@DecisionNerd

@DecisionNerd DecisionNerd commented Aug 1, 2026 •

Copy link
Copy Markdown
Contributor

Closes #281

Summary

  • add an explicit manual recovery dispatch for the existing immutable v0.5.0 tag
  • require a public maintainer reason and waive only stale tagged Unreleased entries
  • reuse the exact retained same-SHA candidate while preserving every identity, checksum, license, ordering, and fail-closed publication check

Evidence

  • initial release run 30687300759 stopped before every registry write
  • retained candidate run 30686731253 passed with 35 validated artifacts
  • release publication preflight tests pass
  • workflow validation passes
  • make pre-push-fast passes

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Summary by CodeRabbit

  • New Features

    • Release validation can now proceed with unreleased changelog entries when explicitly permitted.
    • Manual release workflows include clearer waiver handling and recovery reason tracking.
  • Bug Fixes

    • Improved release preflight validation to consistently apply the configured unreleased-entry exception.

Note

Add manual recovery dispatch to resume immutable v0.5.0 publication

  • Adds a workflow_dispatch trigger to publish.yaml with release_tag, waive_unreleased_entries, and recovery_reason inputs to allow restarting a stalled release.
  • When triggered manually, the candidate-preflight job verifies the immutable tag exists and is an ancestor of main, fetches the current release-publish-preflight.py from origin/main, and runs it with --allow-unreleased-entries to waive only the stale [Unreleased] changelog check.
  • Adds --allow-unreleased-entries flag to release-publish-preflight.py so the validator can skip the non-empty [Unreleased] section error when explicitly requested.
  • Normal release-event triggers retain all prior strict checks (event SHA must match release SHA and origin/main tip).

Macroscope summarized ce3ce2c.

@github-actions github-actions Bot added documentation Improvements or additions to documentation ci-cd CI/CD configuration changes tooling Developer tooling and automation release:none No release note or version impact labels Aug 1, 2026
@coderabbitai

coderabbitai Bot commented Aug 1, 2026 •

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The release preflight adds an opt-in --allow-unreleased-entries override. Validation preserves the default error. Tests cover the override and manual recovery workflow assertions.

Changes

Release preflight recovery

Layer / File(s) Summary
Preflight override wiring
scripts/ci/release-publish-preflight.py
validate accepts the allow_unreleased_entries option. The CLI exposes the matching flag and passes it to validation.
Recovery workflow validation
scripts/ci/test-release-publish-preflight.py
Tests accept stale unreleased entries when enabled and verify manual dispatch, waiver, recovery metadata, flag wiring, git show, and main-branch validation.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive The preflight flag and tests support the waiver objective, but workflow dispatch, candidate reuse, run records, and CI gating are excluded from review. Inspect .github/workflows/publish.yaml, excluded by !/.github/, to verify the workflow-specific acceptance criteria.
Out of Scope Changes check ❓ Inconclusive The two reviewable files are in scope, but excluded workflow and documentation files prevent a complete out-of-scope assessment. Include the excluded files in review or provide summaries to confirm that no unrelated changes are present.
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly describes the primary change: resuming publication of the immutable v0.5.0 release.
Description check ✅ Passed The description clearly explains the recovery flow, waiver scope, issue link, preserved guarantees, and validation evidence.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/281-v050-publish-recovery

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/ci/test-release-publish-preflight.py`:
- Around line 100-111: Update the assertions in the test around the extracted
candidate-preflight block to verify workflow semantics rather than only string
presence: require WAIVE_UNRELEASED_ENTRIES=true and a non-empty RECOVERY_REASON
exclusively in the workflow_dispatch recovery branch, ensure the reason is
recorded, immutable v0.5.0 same-SHA recovery is validated, and
--allow-unreleased-entries is added only after those checks. Also assert that
the release-triggered path cannot use the waiver.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: eacded6c-e213-458e-aa53-bc51169047b6

📥 Commits

Reviewing files that changed from the base of the PR and between 7b2f542 and ce3ce2c.

⛔ Files ignored due to path filters (4)
  • .github/workflows/publish.yaml is excluded by !**/.github/**
  • CHANGELOG.md is excluded by !**/*.md
  • docs/development/v0.5.0-release-operator-runbook.md is excluded by !**/*.md, !**/docs/**
  • docs/reference/changelog.md is excluded by !**/*.md, !**/docs/**
📒 Files selected for processing (2)
  • scripts/ci/release-publish-preflight.py
  • scripts/ci/test-release-publish-preflight.py

Comment on lines 100 to +111
workflow = WORKFLOW.read_text(encoding="utf-8")
preflight = workflow.split(" candidate-preflight:\n", 1)[1].split("\n publish-pypi:", 1)[0]
assert "release-publish-preflight.py" in preflight
assert "github.event.release.tag_name" in preflight
assert "github.sha" in preflight
assert "refs/remotes/origin/main" in preflight
assert "workflow_dispatch:" in workflow
assert "waive_unreleased_entries:" in workflow
assert "RECOVERY_REASON" in preflight
assert "--allow-unreleased-entries" in preflight
assert "git show" in preflight
assert "refs/remotes/origin/main:scripts/ci/release-publish-preflight.py" in preflight

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

workflow="$(fd -t f -g 'publish.yaml' . | head -n1)"
test -n "$workflow"

rg -n -C 12 \
  'workflow_dispatch:|waive_unreleased_entries|RECOVERY_REASON|allow-unreleased-entries|v0\.5\.0|git show|refs/remotes/origin/main:scripts/ci/release-publish-preflight.py' \
  "$workflow"

Repository: CurateLabs/graphforge

Length of output: 159


🏁 Script executed:

set -euo pipefail
printf '%s\n' 'Candidate workflow files:'
git ls-files | rg '(^|/)(publish|release).*\.ya?ml$|\.github/workflows/.*\.ya?ml$' || true
printf '%s\n' 'Relevant identifiers:'
rg -n -C 8 \
  'candidate-preflight|waive_unreleased_entries|RECOVERY_REASON|allow-unreleased-entries|release-publish-preflight|workflow_dispatch|v0\.5\.0|refs/remotes/origin/main' \
  .github scripts 2>/dev/null || true
printf '%s\n' 'Test file:'
sed -n '1,180p' scripts/ci/test-release-publish-preflight.py

Repository: CurateLabs/graphforge

Length of output: 50379


🏁 Script executed:

set -euo pipefail
printf '%s\n' 'Workflow source section:'
sed -n '1,82p' .github/workflows/publish.yaml
printf '%s\n' 'Workflow test section:'
sed -n '80,125p' scripts/ci/test-release-publish-preflight.py
printf '%s\n' 'All workflow assertions and recovery references:'
rg -n -C 4 \
  'assert |waive_unreleased_entries|recovery_reason|RECOVERY_REASON|allow-unreleased-entries|RELEASE_TAG|RELEASE_SHA|GITHUB_EVENT_NAME|github\.event\.release\.tag_name|github\.sha' \
  scripts/ci/test-release-publish-preflight.py .github/workflows/publish.yaml
printf '%s\n' 'Preflight waiver implementation and callers:'
rg -n -C 8 \
  'allow-unreleased-entries|allow_unreleased|unreleased|def validate|argparse|release-publish-preflight' \
  scripts/ci/release-publish-preflight.py scripts/ci/test-release-publish-preflight.py

Repository: CurateLabs/graphforge

Length of output: 43706


🏁 Script executed:

set -euo pipefail
python3 - <<'PY'
from pathlib import Path

workflow = Path(".github/workflows/publish.yaml").read_text(encoding="utf-8")
test = Path("scripts/ci/test-release-publish-preflight.py").read_text(encoding="utf-8")
preflight = workflow.split("  candidate-preflight:\n", 1)[1].split("\n  publish-pypi:", 1)[0]

# Reproduce the test's relevant raw-token checks.
checks = [
    "release-publish-preflight.py",
    "github.event.release.tag_name",
    "github.sha",
    "refs/remotes/origin/main",
    "workflow_dispatch:",
    "waive_unreleased_entries:",
    "RECOVERY_REASON",
    "--allow-unreleased-entries",
    "git show",
    "refs/remotes/origin/main:scripts/ci/release-publish-preflight.py",
]
assert all(token in (workflow if token == "workflow_dispatch:" or token == "waive_unreleased_entries:" else preflight)
           for token in checks)

# A comment-only bypass marker satisfies the current assertion while the
# executable waiver operation is absent.
mutated = preflight.replace(
    '            preflight_args+=(--allow-unreleased-entries)\n',
    '            # preflight_args+=(--allow-unreleased-entries)\n',
)
assert "--allow-unreleased-entries" in mutated
assert "preflight_args+=(--allow-unreleased-entries)" not in mutated

# The current source has a real executable waiver operation and a non-empty
# reason check, but the test does not bind either token to that recovery branch.
assert 'test -n "$RECOVERY_REASON"' in preflight
assert 'preflight_args+=(--allow-unreleased-entries)' in preflight
branch = preflight.split('if test "$GITHUB_EVENT_NAME" = release; then', 1)[1].split("\n          fi", 1)[0]
assert 'test "$WAIVE_UNRELEASED_ENTRIES" = true' in branch
assert 'test -n "$RECOVERY_REASON"' in branch
assert 'preflight_args+=(--allow-unreleased-entries)' in branch
assert 'printf \'Recovery reason: %s\\n\' "$RECOVERY_REASON"' in branch

print("raw assertions accept a comment-only waiver marker")
print("current waiver and reason checks are executable and inside the non-release branch")
PY

Repository: CurateLabs/graphforge

Length of output: 248


🏁 Script executed:

set -euo pipefail
python3 - <<'PY'
from pathlib import Path

workflow = Path(".github/workflows/publish.yaml").read_text(encoding="utf-8")
preflight = workflow.split("  candidate-preflight:\n", 1)[1].split("\n  publish-pypi:", 1)[0]

tokens = [
    "release-publish-preflight.py",
    "github.event.release.tag_name",
    "github.sha",
    "refs/remotes/origin/main",
    "workflow_dispatch:",
    "waive_unreleased_entries:",
    "RECOVERY_REASON",
    "--allow-unreleased-entries",
    "git show",
    "refs/remotes/origin/main:scripts/ci/release-publish-preflight.py",
]
for token in tokens:
    source = workflow if token in {"workflow_dispatch:", "waive_unreleased_entries:"} else preflight
    print(f"{token!r}: {token in source}")

print("\nRelevant executable lines:")
for number, line in enumerate(preflight.splitlines(), 1):
    if any(token in line for token in (
        "GITHUB_EVENT_NAME", "WAIVE_UNRELEASED_ENTRIES", "RECOVERY_REASON",
        "allow-unreleased-entries", "git show", "printf 'Recovery reason",
    )):
        print(f"{number}: {line}")

print("\nBranch extraction:")
marker = 'if test "$GITHUB_EVENT_NAME" = release; then'
print("marker present:", marker in preflight)
if marker in preflight:
    suffix = preflight.split(marker, 1)[1]
    print(suffix.split("\n          fi", 1)[0])
PY

Repository: CurateLabs/graphforge

Length of output: 1826


Make the recovery workflow test semantic. Assert that only the workflow_dispatch recovery branch requires WAIVE_UNRELEASED_ENTRIES=true and a non-empty RECOVERY_REASON, records the reason, validates the immutable v0.5.0 same-SHA recovery, and adds --allow-unreleased-entries after those checks. Assert that the release path cannot use the waiver.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/ci/test-release-publish-preflight.py` around lines 100 - 111, Update
the assertions in the test around the extracted candidate-preflight block to
verify workflow semantics rather than only string presence: require
WAIVE_UNRELEASED_ENTRIES=true and a non-empty RECOVERY_REASON exclusively in the
workflow_dispatch recovery branch, ensure the reason is recorded, immutable
v0.5.0 same-SHA recovery is validated, and --allow-unreleased-entries is added
only after those checks. Also assert that the release-triggered path cannot use
the waiver.

@DecisionNerd
DecisionNerd merged commit f3a72e3 into main Aug 1, 2026
20 checks passed
@DecisionNerd
DecisionNerd deleted the fix/281-v050-publish-recovery branch August 1, 2026 06:20
This was referenced Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-cd CI/CD configuration changes documentation Improvements or additions to documentation release:none No release note or version impact tooling Developer tooling and automation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

release: recover v0.5.0 publication after unreleased changelog preflight stop

1 participant