fix(release): auto-bump via protected-branch-safe bump PR (API merge) #186
@@ -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 }}
|
||||
|
||||
Reference in new issue
Block a user