fix(release): auto-bump via protected-branch-safe bump PR (API merge) #186

Merged
conductor merged 2 commits from fix/auto-version-protected-push into master 2026-09-06 13:32:25 +01:00
+114 -23
View File
@@ -32,38 +32,129 @@ jobs:
- name: Auto-bump if the PR did not carry a version bump
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
RELEASE_SERVER_URL: ${{ gitea.server_url }}
RELEASE_REPO: ${{ gitea.repository }}
PR_TITLE: ${{ gitea.event.pull_request.title }}
PR_BODY: ${{ gitea.event.pull_request.body }}
run: |
LATEST=$(git tag --sort=-v:refname | head -1 | sed 's/^v//')
if [ -z "$LATEST" ]; then
echo "No previous tags — releasing first version $RELEASE_VERSION"
exit 0
fi
if [ "$LATEST" = "$RELEASE_VERSION" ]; then
echo "PR did not bump the version — auto-bumping patch on master (Nick directive: bumping is automatic from the Gitea end)."
NEW=$(python3 -c "import sys; major,minor,patch='$RELEASE_VERSION'.split('.'); print(f'{major}.{minor}.{int(patch)+1}')")
echo "Auto-bump: $RELEASE_VERSION -> $NEW"
# Update all five version surfaces
sed -i "s/__version__ = \"$RELEASE_VERSION\"/__version__ = \"$NEW\"/" src/rune/__init__.py
sed -i "s/Version: v$RELEASE_VERSION/Version: v$NEW/" src/rune/rune_tui.py
sed -i "s/Rune v$RELEASE_VERSION |/Rune v$NEW |/" src/rune/rune_tui.py
sed -i "s/\"$RELEASE_VERSION\"/\"$NEW\"/" src/rune/canary_gitea_automation.py
sed -i "s/v$RELEASE_VERSION/v$NEW/" README.md
git config user.name "conductor"
git config user.email "nick@thingscouldgetdicey.online"
git add -A
git commit -m "chore(release): auto-bump version to $NEW (post-merge automation)"
# Detached-HEAD safe push (PR #171 fix): Actions checks out the
# merge commit, not a branch — "git push origin master" fails with
# "src refspec master does not match any". Push HEAD:master and
# authenticate via the GITEA_TOKEN secret.
git remote set-url origin "https://conductor:${GITEA_TOKEN}@gitea.thingscouldgetdicey.online/conductor/rune.git"
git push origin HEAD:master
echo "RELEASE_VERSION=$NEW" >> "$GITHUB_ENV"
echo "AUTO_BUMPED=true" >> "$GITHUB_ENV"
else
if [ "$LATEST" != "$RELEASE_VERSION" ]; then
echo "New version $RELEASE_VERSION (last tag v$LATEST) — bump rode the PR"
exit 0
fi
NEW=$(python3 -c "import sys; major,minor,patch='$RELEASE_VERSION'.split('.'); print(f'{major}.{minor}.{int(patch)+1}')")
export NEW_VERSION="$NEW"
echo "Auto-bump: $RELEASE_VERSION -> $NEW (via protected-branch-safe bump PR)"
python3 - <<'PYBUMP'
import json, os, sys, time, urllib.request
TOKEN = os.environ["GITEA_TOKEN"]
BASE = os.environ["RELEASE_SERVER_URL"] + "/api/v1/repos/" + os.environ["RELEASE_REPO"]
VERSION = os.environ["RELEASE_VERSION"]
NEW = os.environ["NEW_VERSION"]
PR_TITLE = os.environ.get("PR_TITLE", "")
PR_BODY = os.environ.get("PR_BODY", "")
BRANCH = f"chore/auto-bump-{NEW}"
def api(path, method="GET", data=None, ok=(200,)):
req = urllib.request.Request(
BASE + path,
data=json.dumps(data).encode() if data is not None else None,
headers={"Authorization": "token " + TOKEN, "Content-Type": "application/json"},
method=method,
)
try:
with urllib.request.urlopen(req, timeout=30) as r:
body = r.read()
return r.status, (json.loads(body) if body else {})
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", "replace")
print(f" API {method} {path} -> {e.code}: {body[:200]}")
return e.code, {"_body": body}
# 1. bump branch from master
status, _ = api("/branches", "POST", {"new_branch_name": BRANCH, "old_branch_name": "master"})
if status not in (201, 409):
print(f"branch create failed ({status})"); sys.exit(1)
# 2. commit the five version-surface edits via the contents API
surfaces = {
"src/rune/__init__.py": [
(f'__version__ = "{VERSION}"', f'__version__ = "{NEW}"'),
],
"src/rune/rune_tui.py": [
(f"Version: v{VERSION}", f"Version: v{NEW}"),
(f"Rune v{VERSION} |", f"Rune v{NEW} |"),
],
"src/rune/canary_gitea_automation.py": [
(f'"{VERSION}"', f'"{NEW}"'),
],
"README.md": [
(f"v{VERSION}", f"v{NEW}"),
],
}
for path, pairs in surfaces.items():
status, cur = api(f"/contents/{path}?ref={BRANCH}")
if status != 200:
# optional surface (e.g. pattern absent) — skip gracefully
print(f" skip {path} ({status})"); continue
import base64
text = base64.b64decode(cur["content"]).decode("utf-8")
changed = text
for old, newv in pairs:
changed = changed.replace(old, newv)
if changed == text:
print(f" no-op {path}"); continue
status, r = api(f"/contents/{path}", "PUT", {
"message": f"chore(release): auto-bump version to {NEW} (post-merge automation)",
"content": base64.b64encode(changed.encode()).decode(),
"branch": BRANCH, "sha": cur["sha"],
})
if status not in (200, 201):
print(f" commit {path} failed ({status})"); sys.exit(1)
# 3. bump PR
status, pr = api("/pulls", "POST", {
"title": f"chore(release): auto-bump version to {NEW} (post-merge automation)",
"body": f"Automatic version bump after merge of: {PR_TITLE}\n\n{PR_BODY[:1500]}",
"head": BRANCH, "base": "master",
})
if status not in (200, 201):
print(f"PR create failed ({status})"); sys.exit(1)
pr_number = pr["number"]
head_sha = pr["head"]["sha"]
print(f" bump PR #{pr_number} head {head_sha[:12]}")
# 4. wait for the protected check on the bump PR (same gate as everything else)
deadline = time.time() + 900 # 15 min ceiling
state = None
while time.time() < deadline:
st, cs = api(f"/commits/{head_sha}/status")
state = cs.get("state")
if state in ("success", "failure"):
break
time.sleep(15)
if state != "success":
print(f"bump PR CI did not go green (state={state})"); sys.exit(1)
# 5. merge via API (respects branch protection; user_can_merge)
status, _ = api(f"/pulls/{pr_number}/merge", "POST", {"Do": "merge"})
if status not in (200, 405):
print(f"merge failed ({status})"); sys.exit(1)
# 6. confirm merged, then export the version for the release step
st, pr2 = api(f"/pulls/{pr_number}")
if not pr2.get("merged"):
print("merge did not take"); sys.exit(1)
print(f"AUTO_BUMPED=true")
with open(os.environ["GITHUB_ENV"], "a") as env:
env.write(f"RELEASE_VERSION={NEW}\n")
env.write("AUTO_BUMPED=true\n")
PYBUMP
- name: Create release from merged pull request
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}