Skip to content

Commit 23ac372

Browse files
committed
minor changes
1 parent d9b9e4e commit 23ac372

10 files changed

Lines changed: 371 additions & 88 deletions

File tree

‎.github/workflows/promote.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ name: Promote
55
# a cron job. The manual dispatch does the actual promotion once that call is made.
66

77
on:
8-
schedule:
9-
- cron: "17 1 * * 2"
8+
# schedule:
9+
# - cron: "17 1 * * 2"
1010
workflow_dispatch:
1111
inputs:
1212
version:

‎.github/workflows/rehearse.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ name: Rehearse
55
# than during it. It writes nothing, so it is safe to let fail loudly.
66

77
on:
8-
schedule:
9-
- cron: "23 20 * * 1"
8+
# schedule:
9+
# - cron: "23 20 * * 1"
1010
workflow_dispatch:
1111

1212
permissions:

‎README.md‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,17 @@ sits behind the `release-approval` environment.
8585

8686
Dispatch **Release**. Leave `bump` on `patch` unless the release needs a database
8787
migration or breaks something. When it finishes, `Release vX.Y.Z` is filed in this
88-
repo with the image tag and `sha256` digest; close it once deployed.
88+
repo with the image tag and `sha256` digest, assigned to `RELEASE_ASSIGNEE` and put
89+
on [cBioPortal Team Planning](https://github.com/orgs/cBioPortal/projects/19) in
90+
**Todo** and the current sprint; close it once deployed.
8991

9092
## Repository setup
9193

9294
A GitHub App installed on `cbioportal`, `cbioportal-frontend`, `cbioportal-helm`,
9395
`cbioportal-docker-compose` and this repo, with permissions: contents **write**,
94-
pull requests **write**, issues **write**, actions **write**, metadata **read**.
96+
pull requests **write**, issues **write**, actions **write**, metadata **read**, and
97+
the organisation permission projects **write** so stage 9 can put the release issue
98+
on the team planning board.
9599

96100
| Kind | Name | Value |
97101
| --- | --- | --- |
@@ -101,17 +105,23 @@ pull requests **write**, issues **write**, actions **write**, metadata **read**.
101105
| Environment | `release-approval` | with required reviewers |
102106
| Labels | `release`, `succeeded`, `failed`, `promotion` | in this repo |
103107

108+
The App must also be on the bypass list for branch protection on `cbioportal`
109+
`master`. Stages 5 and 8 commit the pom directly to `master` rather than opening a
110+
PR — those two commits are release logistics, and a PR would file them in the
111+
release notes this release then publishes. Without the bypass both stages fail on
112+
the push.
113+
104114
## Maintaining config.toml
105115

106116
Everything environment-specific lives there. The parts most likely to need
107117
updating:
108118

109119
**`[required_checks]`** — an allowlist of checks that must be green before a
110-
release starts (stage 1, on the pinned SHA) and before a pom PR is merged
111-
(stages 5 and 8), covering both GitHub check-runs and CircleCI commit statuses.
120+
release starts (stage 1, on the pinned SHA), covering both GitHub check-runs and
121+
CircleCI commit statuses.
112122

113-
Both lists are currently **empty, so CI does not gate a release at all**; stages 1,
114-
5 and 8 each log a warning saying so. That is deliberate — the combined status of
123+
Both lists are currently **empty, so CI does not gate a release at all**; stage 1
124+
logs a warning saying so. That is deliberate — the combined status of
115125
both masters is normally `failure` (sonarcloud, GitBook, Dependabot,
116126
`e2e_localdb`), so there was no dependable gate to enforce. It is an allowlist,
117127
never "everything green": add a name only if it should genuinely stop a release.

‎config.toml‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,16 @@ maven_attempts = 30
3737
maven_interval_seconds = 60
3838
docker_attempts = 30
3939
docker_interval_seconds = 60
40-
checks_attempts = 60
41-
checks_interval_seconds = 30
40+
41+
[project]
42+
# The team planning board. Assigning the release issue is not enough on its own:
43+
# the sprint view only lists issues that are items on this project with a status
44+
# and an iteration set. https://github.com/orgs/cBioPortal/projects/19
45+
org = "cBioPortal"
46+
number = 19
47+
status_field = "Status"
48+
status_value = "Todo"
49+
sprint_field = "Sprint"
4250

4351
[promotion]
4452
# A pre-release older than this is due for promotion to official.

‎src/release/cli.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def cmd_await_backend(args, config) -> int:
139139

140140
def cmd_bump_snapshot(args, config) -> int:
141141
with logs.group("reopen master for development"):
142-
bump_snapshot(_gh(), config, plan.read(args.plan), Path(args.workdir), args.dry_run)
142+
bump_snapshot(config, plan.read(args.plan), Path(args.workdir), args.dry_run)
143143
return 0
144144

145145

‎src/release/gh.py‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ def patch(self, path: str, json: dict):
7878
def put(self, path: str, json: dict):
7979
return self.request("PUT", path, json=json)
8080

81+
def graphql(self, query: str, **variables):
82+
"""The one thing REST cannot do: Projects v2 is GraphQL-only."""
83+
payload = self.post("/graphql", {"query": query, "variables": variables})
84+
if payload.get("errors"):
85+
raise GitHubError(f"graphql: {payload['errors']}")
86+
return payload["data"]
87+
8188
def paginate(self, path: str, key: str | None = None):
8289
"""Yield items across pages. `key` names the list field for envelope responses."""
8390
url = f"{path}{'&' if '?' in path else '?'}per_page=100"
@@ -177,9 +184,6 @@ def find_pull(self, repo: str, head_branch: str, base: str) -> dict | None:
177184
pulls = self.get(f"/repos/{repo}/pulls?head={owner}:{head_branch}&base={base}&state=open")
178185
return pulls[0] if pulls else None
179186

180-
def merge_pull(self, repo: str, number: int, method: str = "squash") -> dict:
181-
return self.put(f"/repos/{repo}/pulls/{number}/merge", {"merge_method": method})
182-
183187
def dispatch_workflow(self, repo: str, workflow: str, ref: str, inputs: dict) -> dict:
184188
return self.post(
185189
f"/repos/{repo}/actions/workflows/{workflow}/dispatches",

‎src/release/pom.py‎

Lines changed: 52 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
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.
26
37
Both use `mvn versions:set` rather than sed. The disabled `snapshot-release.yml`
48
is a standing demonstration of why: its sed looked for `<version>...-SNAPSHOT</version>`
@@ -12,7 +16,6 @@
1216
import logging
1317
import re
1418
import subprocess
15-
import time
1619
from pathlib import Path
1720

1821
from . import version as ver
@@ -85,60 +88,54 @@ def assert_only_pom_changed(workdir: Path) -> None:
8588
raise PomError(f"expected only pom.xml to change, got: {changed or 'nothing'}")
8689

8790

88-
# ---- the shared branch/PR/merge path ---------------------------------------
91+
# ---- the shared commit path ------------------------------------------------
92+
93+
PUSH_ATTEMPTS = 3
8994

9095

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+
"""
93106
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)
95108
logger.info("%s", run(["git", "diff"], workdir))
96109
return {"dry_run": True}
97110

98111
run(["git", "config", "user.name", "cbioportal-release-manager"], workdir)
99112
run(["git", "config", "user.email",
100113
"cbioportal-release-manager@users.noreply.github.com"], workdir)
101-
run(["git", "checkout", "-B", branch], workdir)
102114
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}
142139

143140

144141
# ---- stage 5 ---------------------------------------------------------------
@@ -167,16 +164,16 @@ def bump_frontend(gh: GitHub, config: dict, plan: dict, workdir: Path,
167164
)
168165

169166
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,
173169
message=f"Frontend {target}",
174170
body=(
175171
f"Points the backend at frontend `{target}`.\n\n"
176172
f"- `<version>`: `{current_version}` -> `{target}`\n"
177173
f"- `<{prop}>`: `{current_frontend}` -> `{target}`\n\n"
178-
f"Opened by cbioportal-release-manager.{drift}"
174+
f"Committed by cbioportal-release-manager.{drift}"
179175
),
176+
expect=(target, target),
180177
dry_run=dry_run,
181178
)
182179

@@ -205,8 +202,7 @@ def drift_note(gh: GitHub, repo: str, base: str, pinned: str) -> str:
205202
# ---- stage 8 ---------------------------------------------------------------
206203

207204

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:
210206
released = plan["version"]
211207
target = ver.snapshot(released)
212208
repo = config["repos"]["backend"]
@@ -238,15 +234,15 @@ def bump_snapshot(gh: GitHub, config: dict, plan: dict, workdir: Path,
238234
if parent_version(workdir) != before_parent:
239235
raise PomError("snapshot bump changed the parent version")
240236

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,
244239
message=f"Prepare for {target}",
245240
body=(
246241
f"Reopens `{base}` for development after `{released}`.\n\n"
247242
f"- `<version>`: `{current_version}` -> `{target}`\n"
248243
f"- frontend pin left at `{current_frontend}`\n\n"
249-
f"Opened by cbioportal-release-manager."
244+
f"Committed by cbioportal-release-manager."
250245
),
246+
expect=(target, current_frontend),
251247
dry_run=dry_run,
252248
)

0 commit comments

Comments
 (0)