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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ See [AGENTS.md](AGENTS.md) for the repository map and the same guidelines in age

## Releases

Releases are cut by maintainers with [`scripts/release.sh`](scripts/release.sh), which bumps `VERSION`, regenerates `CHANGELOG.md` from commit history with [git-cliff](https://git-cliff.org/), tags, and publishes the GitHub Release. You do not need to touch `VERSION` or `CHANGELOG.md` in a normal PR.
Releases are cut by maintainers from a CI-green `main` with [`scripts/release.sh`](scripts/release.sh), which bumps `VERSION`, regenerates `CHANGELOG.md` from commit history with [git-cliff](https://git-cliff.org/), tags, and publishes the GitHub Release. The release script skips the full local shell suite by default because CI already runs it on merge to `main`; for `--push` and `--publish`, it first verifies local `HEAD` matches upstream. Pass `--run-tests` when you want an extra local preflight. You do not need to touch `VERSION` or `CHANGELOG.md` in a normal PR.

## Reporting bugs and proposing features

Expand Down
10 changes: 10 additions & 0 deletions scripts/devloop_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -633,8 +633,12 @@ ok "pure helpers"
printf '%s\n' "9.9.9" > "$ROOT/site/public/VERSION"
dry_run_output="$(release_main "patch" --dry-run)" || fail "release dry-run required git-cliff"
contains "$dry_run_output" "next: 9.9.10 (v9.9.10)" "release dry-run"
contains "$dry_run_output" "would skip local tests (use --run-tests to run bash scripts/devloop_test.sh)" "release dry-run"
contains "$dry_run_output" "would tag: v9.9.10" "release dry-run"
test_dry_run_output="$(release_main "patch" --run-tests --dry-run)" || fail "release test dry-run required git-cliff"
contains "$test_dry_run_output" "would run bash scripts/devloop_test.sh" "release test dry-run"
publish_dry_run_output="$(release_main "patch" --publish --dry-run)" || fail "release publish dry-run required git-cliff"
contains "$publish_dry_run_output" "would verify local HEAD matches upstream, then skip local tests" "release publish dry-run"
contains "$publish_dry_run_output" "would push branch and tag" "release publish dry-run"
contains "$publish_dry_run_output" "would build release assets: devloop-9.9.10.tar.gz and devloop-9.9.10.tar.gz.sha256" "release publish dry-run"
contains "$publish_dry_run_output" "would create GitHub release: gh release create v9.9.10 --verify-tag --generate-notes" "release publish dry-run"
Expand All @@ -646,6 +650,12 @@ ok "pure helpers"
printf '%s\n' "dirty" > "$ROOT/dirty"
if release_assert_clean_tree >/dev/null 2>&1; then fail "release clean tree accepted dirty repo"; fi
rm "$ROOT/dirty"
if release_assert_head_matches_upstream >/dev/null 2>&1; then fail "release upstream accepted missing upstream"; fi
git -C "$ROOT" branch verified-main
git -C "$ROOT" branch --set-upstream-to=verified-main >/dev/null
release_assert_head_matches_upstream || fail "release upstream rejected matching HEAD"
git -C "$ROOT" commit --allow-empty -q -m local-ahead
if release_assert_head_matches_upstream >/dev/null 2>&1; then fail "release upstream accepted local ahead HEAD"; fi
[ -n "$(release_current_branch)" ] || fail "release current branch missing"
DEVLOOP_RELEASE_ALLOW_BRANCH=1 release_assert_push_branch || fail "release push branch rejected"
)
Expand Down
59 changes: 56 additions & 3 deletions scripts/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ RELEASE_CHECKSUM=""

release_usage() {
cat <<'EOF'
usage: ./scripts/release.sh <patch|minor|major> [--dry-run] [--publish] [--push]
usage: ./scripts/release.sh <patch|minor|major> [--dry-run] [--run-tests] [--publish] [--push]

Bumps VERSION, creates a release commit, and creates an annotated tag.
Use --run-tests to run bash scripts/devloop_test.sh before changing files.
Use --publish to push the commit and tag, then create a GitHub Release.

Examples:
./scripts/release.sh patch --dry-run
./scripts/release.sh patch --run-tests
./scripts/release.sh minor --publish
./scripts/release.sh major --push
EOF
Expand Down Expand Up @@ -171,18 +173,58 @@ release_assert_push_branch() {
return 1
}

release_assert_head_matches_upstream() {
local branch remote merge head upstream_head upstream_label
branch="$(release_current_branch)"
if [ -z "$branch" ]; then
printf '%s\n' "refusing to skip tests from detached HEAD" >&2
return 1
fi

remote="$(git -C "$ROOT" config "branch.$branch.remote" || true)"
merge="$(git -C "$ROOT" config "branch.$branch.merge" || true)"
if [ -z "$remote" ] || [ -z "$merge" ]; then
printf 'refusing to skip tests without an upstream for branch: %s\n' "$branch" >&2
printf '%s\n' "set an upstream, sync with it, or pass --run-tests" >&2
return 1
fi

head="$(git -C "$ROOT" rev-parse HEAD)"
if [ "$remote" = "." ]; then
upstream_label="${merge#refs/heads/}"
upstream_head="$(git -C "$ROOT" rev-parse "$upstream_label")" || return 1
else
upstream_label="$remote/${merge#refs/heads/}"
if ! git -C "$ROOT" fetch --quiet "$remote" "$merge"; then
printf 'failed to fetch upstream: %s\n' "$upstream_label" >&2
printf '%s\n' "fetch manually or pass --run-tests" >&2
return 1
fi
upstream_head="$(git -C "$ROOT" rev-parse FETCH_HEAD)" || return 1
fi

if [ "$head" = "$upstream_head" ]; then return 0; fi
printf '%s\n' "refusing to skip tests for a HEAD that differs from upstream" >&2
printf 'local HEAD: %s\n' "$head" >&2
printf 'upstream %s: %s\n' "$upstream_label" "$upstream_head" >&2
printf '%s\n' "sync with upstream or pass --run-tests" >&2
return 1
}

release_main() {
local bump=""
local current version
local dry_run=false
local publish=false
local push=false
local run_tests=false
local tag branch
local artifact_dir

while [ "$#" -gt 0 ]; do
case "$1" in
--dry-run) dry_run=true ;;
--run-tests) run_tests=true ;;
--publish) publish=true ;;
--push) push=true ;;
-h|--help) release_usage; return 0 ;;
Expand Down Expand Up @@ -235,7 +277,13 @@ release_main() {
if [ -n "$(git -C "$ROOT" status --porcelain)" ]; then
printf '%s\n' "note: actual release requires a clean working tree"
fi
printf '%s\n' "would run bash scripts/devloop_test.sh"
if [ "$run_tests" = true ]; then
printf '%s\n' "would run bash scripts/devloop_test.sh"
elif [ "$publish" = true ] || [ "$push" = true ]; then
printf '%s\n' "would verify local HEAD matches upstream, then skip local tests"
else
printf '%s\n' "would skip local tests (use --run-tests to run bash scripts/devloop_test.sh)"
fi
printf 'would commit: chore: release %s\n' "$version"
printf 'would tag: %s\n' "$tag"
printf '%s\n' "would regenerate CHANGELOG.md from tagged history, amend the release commit, and move the tag"
Expand All @@ -253,7 +301,12 @@ release_main() {
release_assert_clean_tree
if [ "$publish" = true ] || [ "$push" = true ]; then release_assert_push_branch; fi

bash "$ROOT/scripts/devloop_test.sh"
if [ "$run_tests" = true ]; then
bash "$ROOT/scripts/devloop_test.sh"
else
if [ "$publish" = true ] || [ "$push" = true ]; then release_assert_head_matches_upstream; fi
printf '%s\n' "skip local tests (use --run-tests to run bash scripts/devloop_test.sh)"
fi
release_write_version_files "$version"
git -C "$ROOT" add VERSION
if [ -f "$ROOT/site/public/VERSION" ]; then git -C "$ROOT" add site/public/VERSION; fi
Expand Down
Loading