|
1 | | -"""Stages 5 and 8: the two backend pom edits, each as a reviewed PR. |
| 1 | +"""Stages 5 and 8: the two backend pom edits, each committed straight to master. |
| 2 | +
|
| 3 | +Not PRs. Both commits are release logistics -- a version string and a frontend |
| 4 | +pin -- so there is nothing to review, and a PR would file them in the same |
| 5 | +release-drafter draft this release goes on to publish. |
2 | 6 |
|
3 | 7 | Both use `mvn versions:set` rather than sed. The disabled `snapshot-release.yml` |
4 | 8 | is a standing demonstration of why: its sed looked for `<version>...-SNAPSHOT</version>` |
|
12 | 16 | import logging |
13 | 17 | import re |
14 | 18 | import subprocess |
15 | | -import time |
16 | 19 | from pathlib import Path |
17 | 20 |
|
18 | 21 | from . import version as ver |
@@ -85,60 +88,54 @@ def assert_only_pom_changed(workdir: Path) -> None: |
85 | 88 | raise PomError(f"expected only pom.xml to change, got: {changed or 'nothing'}") |
86 | 89 |
|
87 | 90 |
|
88 | | -# ---- the shared branch/PR/merge path --------------------------------------- |
| 91 | +# ---- the shared commit path ------------------------------------------------ |
| 92 | + |
| 93 | +PUSH_ATTEMPTS = 3 |
89 | 94 |
|
90 | 95 |
|
91 | | -def open_and_merge(gh: GitHub, config: dict, workdir: Path, repo: str, base: str, |
92 | | - branch: str, message: str, body: str, dry_run: bool) -> dict: |
| 96 | +def commit_to_base(workdir: Path, repo: str, base: str, message: str, body: str, |
| 97 | + expect: tuple[str, str | None], dry_run: bool) -> dict: |
| 98 | + """Commit pom.xml onto `base` itself, fast-forward only. |
| 99 | +
|
| 100 | + `expect` is the (project version, frontend pin) this edit intends to leave on |
| 101 | + the branch. If the push is rejected because someone merged first, the commit |
| 102 | + is rebased onto whatever landed and the pom is re-read: a clean rebase that |
| 103 | + still changed those two values means two things are editing the pom at once, |
| 104 | + which needs a human rather than a retry. |
| 105 | + """ |
93 | 106 | if dry_run: |
94 | | - logger.info("[dry-run] would commit %r on %s and merge into %s", message, branch, base) |
| 107 | + logger.info("[dry-run] would commit %r onto %s", message, base) |
95 | 108 | logger.info("%s", run(["git", "diff"], workdir)) |
96 | 109 | return {"dry_run": True} |
97 | 110 |
|
98 | 111 | run(["git", "config", "user.name", "cbioportal-release-manager"], workdir) |
99 | 112 | run(["git", "config", "user.email", |
100 | 113 | "cbioportal-release-manager@users.noreply.github.com"], workdir) |
101 | | - run(["git", "checkout", "-B", branch], workdir) |
102 | 114 | run(["git", "add", "pom.xml"], workdir) |
103 | | - run(["git", "commit", "-m", message], workdir) |
104 | | - run(["git", "push", "--force-with-lease", "origin", branch], workdir) |
105 | | - |
106 | | - pull = gh.find_pull(repo, branch, base) or gh.create_pull( |
107 | | - repo, branch, base, message, body |
108 | | - ) |
109 | | - logger.info("%s: PR #%s %s", repo, pull["number"], pull["html_url"]) |
110 | | - |
111 | | - await_pr_checks(gh, config, repo, pull["number"]) |
112 | | - gh.merge_pull(repo, pull["number"], method="squash") |
113 | | - logger.info("%s: merged #%s", repo, pull["number"]) |
114 | | - return pull |
115 | | - |
116 | | - |
117 | | -def await_pr_checks(gh: GitHub, config: dict, repo: str, number: int, |
118 | | - sleep=time.sleep) -> None: |
119 | | - key = "backend" if repo == config["repos"]["backend"] else "frontend" |
120 | | - required = config["required_checks"][key] |
121 | | - if not required: |
122 | | - logger.warning("%s PR #%s: no required checks configured; merging unguarded", |
123 | | - repo, number) |
124 | | - return |
125 | | - attempts = config["timeouts"]["checks_attempts"] |
126 | | - interval = config["timeouts"]["checks_interval_seconds"] |
127 | | - |
128 | | - for attempt in range(1, attempts + 1): |
129 | | - pull = gh.get(f"/repos/{repo}/pulls/{number}") |
130 | | - states = gh.combined_state(repo, pull["head"]["sha"]) |
131 | | - pending = [n for n in required if states.get(n) in (None, "pending")] |
132 | | - failed = [n for n in required |
133 | | - if states.get(n) not in (None, "pending", "success", "neutral", "skipped")] |
134 | | - if failed: |
135 | | - raise PomError(f"{repo} PR #{number}: required checks failed: {failed}") |
136 | | - if not pending: |
137 | | - logger.info("%s PR #%s: required checks green", repo, number) |
138 | | - return |
139 | | - logger.info("waiting on %s (%d/%d)", pending, attempt, attempts) |
140 | | - sleep(interval) |
141 | | - raise PomError(f"{repo} PR #{number}: checks did not finish in time") |
| 115 | + run(["git", "commit", "-m", message, "-m", body], workdir) |
| 116 | + |
| 117 | + for attempt in range(1, PUSH_ATTEMPTS + 1): |
| 118 | + try: |
| 119 | + run(["git", "push", "origin", f"HEAD:{base}"], workdir) |
| 120 | + except PomError: |
| 121 | + if attempt == PUSH_ATTEMPTS: |
| 122 | + raise |
| 123 | + logger.warning("%s: push to %s rejected, rebasing (%d/%d)", |
| 124 | + repo, base, attempt, PUSH_ATTEMPTS) |
| 125 | + # FETCH_HEAD, not origin/<base>: actions/checkout does not leave a |
| 126 | + # remote-tracking ref that can be relied on here. |
| 127 | + run(["git", "fetch", "origin", base], workdir) |
| 128 | + run(["git", "rebase", "FETCH_HEAD"], workdir) |
| 129 | + landed = read_values(workdir) |
| 130 | + if landed != expect: |
| 131 | + raise PomError( |
| 132 | + f"{repo}: {base} moved and the pom no longer matches this edit: " |
| 133 | + f"got {landed}, expected {expect}" |
| 134 | + ) from None |
| 135 | + continue |
| 136 | + sha = run(["git", "rev-parse", "HEAD"], workdir).strip() |
| 137 | + logger.info("%s: committed %s to %s", repo, sha[:8], base) |
| 138 | + return {"sha": sha} |
142 | 139 |
|
143 | 140 |
|
144 | 141 | # ---- stage 5 --------------------------------------------------------------- |
@@ -167,16 +164,16 @@ def bump_frontend(gh: GitHub, config: dict, plan: dict, workdir: Path, |
167 | 164 | ) |
168 | 165 |
|
169 | 166 | drift = drift_note(gh, repo, base, plan["backend"]["sha"]) |
170 | | - return open_and_merge( |
171 | | - gh, config, workdir, repo, base, |
172 | | - branch=f"release/{target}-frontend", |
| 167 | + return commit_to_base( |
| 168 | + workdir, repo, base, |
173 | 169 | message=f"Frontend {target}", |
174 | 170 | body=( |
175 | 171 | f"Points the backend at frontend `{target}`.\n\n" |
176 | 172 | f"- `<version>`: `{current_version}` -> `{target}`\n" |
177 | 173 | f"- `<{prop}>`: `{current_frontend}` -> `{target}`\n\n" |
178 | | - f"Opened by cbioportal-release-manager.{drift}" |
| 174 | + f"Committed by cbioportal-release-manager.{drift}" |
179 | 175 | ), |
| 176 | + expect=(target, target), |
180 | 177 | dry_run=dry_run, |
181 | 178 | ) |
182 | 179 |
|
@@ -205,8 +202,7 @@ def drift_note(gh: GitHub, repo: str, base: str, pinned: str) -> str: |
205 | 202 | # ---- stage 8 --------------------------------------------------------------- |
206 | 203 |
|
207 | 204 |
|
208 | | -def bump_snapshot(gh: GitHub, config: dict, plan: dict, workdir: Path, |
209 | | - dry_run: bool) -> dict: |
| 205 | +def bump_snapshot(config: dict, plan: dict, workdir: Path, dry_run: bool) -> dict: |
210 | 206 | released = plan["version"] |
211 | 207 | target = ver.snapshot(released) |
212 | 208 | repo = config["repos"]["backend"] |
@@ -238,15 +234,15 @@ def bump_snapshot(gh: GitHub, config: dict, plan: dict, workdir: Path, |
238 | 234 | if parent_version(workdir) != before_parent: |
239 | 235 | raise PomError("snapshot bump changed the parent version") |
240 | 236 |
|
241 | | - return open_and_merge( |
242 | | - gh, config, workdir, repo, base, |
243 | | - branch=f"release/{target}", |
| 237 | + return commit_to_base( |
| 238 | + workdir, repo, base, |
244 | 239 | message=f"Prepare for {target}", |
245 | 240 | body=( |
246 | 241 | f"Reopens `{base}` for development after `{released}`.\n\n" |
247 | 242 | f"- `<version>`: `{current_version}` -> `{target}`\n" |
248 | 243 | f"- frontend pin left at `{current_frontend}`\n\n" |
249 | | - f"Opened by cbioportal-release-manager." |
| 244 | + f"Committed by cbioportal-release-manager." |
250 | 245 | ), |
| 246 | + expect=(target, current_frontend), |
251 | 247 | dry_run=dry_run, |
252 | 248 | ) |
0 commit comments