Skip to content

ci: standardize release process#7

Merged
eddietejeda merged 3 commits into
masterfrom
ci/standardize-release-process
May 20, 2026
Merged

ci: standardize release process#7
eddietejeda merged 3 commits into
masterfrom
ci/standardize-release-process

Conversation

@eddietejeda
Copy link
Copy Markdown
Contributor

@eddietejeda eddietejeda commented May 20, 2026

Summary

  • Add scripts/release.sh (prepare + publish)
  • Auto-create GitHub Releases on tag push (release.yml)
  • Require changelog entry when version bumps (check-release.yml)
  • Add RELEASING.md and CHANGELOG.md

Test plan

  • CI checks passing
  • Changelog and release scripts added
  • Run ./scripts/release.sh prepare patch on next release (post-merge)

Add scripts/release.sh for version bumps, changelog updates, tagging, and
GitHub Release creation via CI. Enforce changelog checks on version PRs.
Comment thread scripts/release.sh Outdated
Comment on lines +135 to +145
else:
insert = (
f"## [Unreleased]\n\n"
f"## [{ver}] - {date}\n\n"
"### Changed\n\n"
f"- Release {ver}\n\n"
)
if "## [Unreleased]" in text:
text = text.replace("## [Unreleased]", insert.strip() + "\n\n## [Unreleased]", 1)
else:
text = re.sub(r"(^# Changelog.*?)(\n\n|\Z)", r"\1\n\n" + insert, text, count=1, flags=re.S)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Blocking: this branch produces a CHANGELOG with two ## [Unreleased] headings.

insert.strip() already begins with ## [Unreleased], then the replacement string appends another \n\n## [Unreleased]. Running this against the CHANGELOG in this PR to release 0.1.2 yields:

## [Unreleased]

## [0.1.2] - 2026-05-20

### Changed

- Release 0.1.2

## [Unreleased]      ← duplicate

## [0.1.1] - 2026-05-19
...

check-release.py won't catch this because it only looks for the new version heading.

Fix: replace once with just the new version section, leaving the existing ## [Unreleased] heading in place — e.g.:

Suggested change
else:
insert = (
f"## [Unreleased]\n\n"
f"## [{ver}] - {date}\n\n"
"### Changed\n\n"
f"- Release {ver}\n\n"
)
if "## [Unreleased]" in text:
text = text.replace("## [Unreleased]", insert.strip() + "\n\n## [Unreleased]", 1)
else:
text = re.sub(r"(^# Changelog.*?)(\n\n|\Z)", r"\1\n\n" + insert, text, count=1, flags=re.S)
else:
new_section = (
f"## [{ver}] - {date}\n\n"
"### Changed\n\n"
f"- Release {ver}\n\n"
)
if "## [Unreleased]" in text:
text = text.replace("## [Unreleased]\n\n", f"## [Unreleased]\n\n{new_section}", 1)
else:
text = re.sub(r"(^# Changelog.*?)(\n\n|\Z)", r"\1\n\n## [Unreleased]\n\n" + new_section, text, count=1, flags=re.S)

Comment thread scripts/release.sh Outdated
Comment on lines +123 to +134
if unreleased and unreleased.group(1).strip():
body = unreleased.group(1).rstrip() + "\n"
text = re.sub(r"^## \[Unreleased\]\s*\n.*?(?=^## \[|\Z)", "", text, count=1, flags=re.M | re.S)
insert = f"## [Unreleased]\n\n## [{ver}] - {date}\n{body}\n"
if text.startswith("# Changelog"):
parts = text.split("\n\n", 2)
if len(parts) >= 2:
text = parts[0] + "\n\n" + parts[1] + "\n\n" + insert + (parts[2] if len(parts) > 2 else "")
else:
text = insert + text
else:
text = insert + text
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Blocking: when [Unreleased] has content, this branch misplaces the Keep-a-Changelog header text.

After re.sub removes the Unreleased section, text is:

# Changelog\n\nAll notable changes...\n\nThe format is based on...\nand this project adheres to...\n\n## [0.1.1] - ...

text.split("\n\n", 2) yields three parts where parts[2] begins with "The format is based on...". The reassembly inserts the new version block between parts[1] and parts[2], leaving the format/semver header paragraph stranded below the new release section:

# Changelog

All notable changes to this project will be documented in this file.

## [Unreleased]

## [0.1.2] - 2026-05-20
### Added
- foo

The format is based on [Keep a Changelog]...
and this project adheres to...

## [0.1.1] - ...

Fix: don't try to split-and-reinsert around the preamble. Match the existing ## [Unreleased] section directly and replace it, keeping all preceding text untouched — same approach as the suggestion below on the else branch.

Comment on lines +39 to +44
fi
{
echo "body<<EOF"
echo "$body"
echo "EOF"
} >> "$GITHUB_OUTPUT"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: (not blocking) the EOF heredoc delimiter is collidable. If a future changelog entry contains a line that's exactly EOF, parsing of $GITHUB_OUTPUT will terminate early and corrupt the body. GitHub's docs recommend a random delimiter, e.g.:

delim="EOF_$(openssl rand -hex 8)"
{
  echo "body<<${delim}"
  echo "$body"
  echo "${delim}"
} >> "$GITHUB_OUTPUT"

Unlikely to bite, but trivially cheap to harden.

Comment thread scripts/release.sh
Comment on lines +83 to +88
default_branch() {
local remote="${1:-origin}"
git symbolic-ref --quiet "refs/remotes/${remote}/HEAD" 2>/dev/null | sed "s|refs/remotes/${remote}/||" \
|| { git branch -r | sed -n "s|^ ${remote}/\\(main\\|master\\)$|\\1|p" | head -1; } \
|| echo main
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: (not blocking) the pipe to sed masks the exit status of git symbolic-ref. When refs/remotes/origin/HEAD is unset, symbolic-ref writes nothing to stdout but sed still succeeds with empty output, so the || fallbacks never run and default_branch returns "". Downstream git fetch origin "" then fails with a confusing error.

Capture the symbolic-ref output first, then fall back:

default_branch() {
  local remote="${1:-origin}" ref
  if ref=$(git symbolic-ref --quiet "refs/remotes/${remote}/HEAD" 2>/dev/null); then
    echo "${ref#refs/remotes/${remote}/}"
    return
  fi
  git branch -r | sed -n "s|^  ${remote}/\\(main\\|master\\)$|\\1|p" | head -1 | grep . \
    || echo main
}

Comment thread scripts/check-release.py

import re
import subprocess
import sys
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

super nit: (not blocking) sys is imported but never used.

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Review

Blocking Issues

  • scripts/release.sh:135-145 — the empty-[Unreleased] branch of update_changelog produces a CHANGELOG with two ## [Unreleased] headings (the replacement string already contains ## [Unreleased] from insert.strip() and then appends another). Running prepare patch against the CHANGELOG in this PR demonstrates the duplicate. check-release.py doesn't catch it because it only validates the new version heading.
  • scripts/release.sh:123-134 — the non-empty-[Unreleased] branch splits on \n\n and reassembles around the preamble, leaving the "The format is based on Keep a Changelog…" paragraph stranded below the new release section instead of in the header.

Both paths are reachable on the very next ./scripts/release.sh prepare invocation against the post-merge CHANGELOG, so the release automation will produce a malformed file the first time it's used.

Action Required

Rewrite update_changelog to insert the new version section directly after the existing ## [Unreleased] heading without disturbing the preamble or duplicating headings. See the suggestion on line 145 for one shape. Nits/super nits left inline can be addressed at your discretion.

Avoid duplicate [Unreleased] headings and keep the preamble intact when
preparing releases. Add unit tests and harden release workflow output.
claude[bot]
claude Bot previously approved these changes May 20, 2026
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Prior blocking issues resolved by the new scripts/update_changelog.py and accompanying tests.

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Prior blocking issues resolved (duplicate ## [Unreleased] heading, preamble placement). Tests cover both empty and populated Unreleased cases.

@eddietejeda eddietejeda merged commit 52c525c into master May 20, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant