Skip to content

Commit 2eb0cb6

Browse files
committed
ci(release): pin package tags to release targets
1 parent 29fd980 commit 2eb0cb6

5 files changed

Lines changed: 117 additions & 8 deletions

File tree

.github/workflows/build.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -456,15 +456,23 @@ jobs:
456456
wrapper_track="${aio_track}"
457457
else
458458
changelog_version="$(python3 scripts/release.py latest-changelog-version 2>/dev/null || true)"
459-
release_commit_pattern="^chore\\(release\\): $(python3 -c 'import re, sys; print(re.escape(sys.argv[1]))' "${changelog_version}")( \\(#[0-9]+\\))?$"
460459
case "${changelog_version}" in
461460
"${upstream_version}"-aio.*)
462461
candidate_revision="${changelog_version##*.}"
463-
if [[ "${candidate_revision}" =~ ^[0-9]+$ ]] && printf '%s\n' "${commit_message}" | grep -Eq "${release_commit_pattern}"; then
462+
release_target="$(python3 scripts/release.py find-release-target-commit "${changelog_version}" 2>/dev/null || true)"
463+
if [[ "${candidate_revision}" =~ ^[0-9]+$ && "${release_target}" == "${GITHUB_SHA}" ]]; then
464464
aio_track="aio-v${candidate_revision}"
465465
release_package_tag="${upstream_version}-${aio_track}"
466466
version_label="${release_package_tag}"
467467
wrapper_track="${aio_track}"
468+
else
469+
release_commit_pattern="^chore\\(release\\): $(python3 -c 'import re, sys; print(re.escape(sys.argv[1]))' "${changelog_version}")( \\(#[0-9]+\\))?$"
470+
if [[ "${candidate_revision}" =~ ^[0-9]+$ ]] && printf '%s\n' "${commit_message}" | grep -Eq "${release_commit_pattern}"; then
471+
aio_track="aio-v${candidate_revision}"
472+
release_package_tag="${upstream_version}-${aio_track}"
473+
version_label="${release_package_tag}"
474+
wrapper_track="${aio_track}"
475+
fi
468476
fi
469477
;;
470478
esac

.github/workflows/publish-release.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ jobs:
2727
echo "release_version=${release_version}" >> "${GITHUB_OUTPUT}"
2828
release_commit="$(python3 scripts/release.py find-release-commit "${release_version}")"
2929
echo "release_commit=${release_commit}" >> "${GITHUB_OUTPUT}"
30-
echo "Matched release commit ${release_commit} for ${release_version}"
30+
release_target="$(python3 scripts/release.py find-release-target-commit "${release_version}")"
31+
echo "release_target=${release_target}" >> "${GITHUB_OUTPUT}"
32+
echo "Matched release commit ${release_commit} and target ${release_target} for ${release_version}"
3133
3234
- name: Require successful CI for release commit
3335
env:
3436
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35-
RELEASE_COMMIT: ${{ steps.version.outputs.release_commit }}
37+
RELEASE_COMMIT: ${{ steps.version.outputs.release_target }}
3638
WORKFLOW_SELECTOR: build.yml
3739
run: |
3840
runs_json="$(gh run list \
@@ -84,7 +86,7 @@ jobs:
8486
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
8587
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
8688
RELEASE_VERSION: ${{ steps.version.outputs.release_version }}
87-
RELEASE_COMMIT: ${{ steps.version.outputs.release_commit }}
89+
RELEASE_COMMIT: ${{ steps.version.outputs.release_target }}
8890
GITHUB_REPOSITORY: ${{ github.repository }}
8991
run: |
9092
if git rev-parse "${RELEASE_VERSION}" >/dev/null 2>&1; then
@@ -110,7 +112,7 @@ jobs:
110112
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
111113
RELEASE_VERSION: ${{ steps.version.outputs.release_version }}
112114
RELEASE_NOTES: ${{ steps.notes.outputs.release_notes }}
113-
RELEASE_COMMIT: ${{ steps.version.outputs.release_commit }}
115+
RELEASE_COMMIT: ${{ steps.version.outputs.release_target }}
114116
run: |
115117
export GITHUB_TOKEN="${RELEASE_TOKEN:-${GITHUB_TOKEN}}"
116118

docs/releases.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@ Release commits also publish the immutable packaging line tag, for example `v0.6
2525
1. Trigger **Prepare Release / Sure-AIO** from `main`.
2626
2. The workflow computes the next `upstream-aio.N` version, updates `CHANGELOG.md`, syncs the XML `<Changes>` block, and opens a release PR.
2727
3. Review and merge that PR into `main`.
28-
4. Wait for the `CI / Sure-AIO` run on the release commit to finish green. That same `main` push also publishes the updated package tags automatically.
28+
4. Wait for the `CI / Sure-AIO` run on the release target commit to finish green. That same `main` push also publishes the updated package tags automatically.
2929
5. Trigger **Publish Release / Sure-AIO** from `main`.
30-
6. The workflow verifies CI on the exact release commit, creates the Git tag if needed, and publishes the GitHub Release.
30+
6. The workflow verifies CI on the exact release target commit, creates the Git tag if needed, and publishes the GitHub Release.

scripts/release.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,35 @@ def find_release_commit(version: str) -> str:
134134
)
135135

136136

137+
def git_is_ancestor(ancestor: str, descendant: str) -> bool:
138+
return (
139+
git_completed("merge-base", "--is-ancestor", ancestor, descendant).returncode
140+
== 0
141+
)
142+
143+
144+
def find_release_target_commit(version: str) -> str:
145+
release_commit = find_release_commit(version)
146+
head = git_output("rev-parse", "HEAD").strip()
147+
148+
if release_commit == head:
149+
return release_commit
150+
151+
if not git_is_ancestor(release_commit, head):
152+
raise SystemExit(
153+
f"Release commit {release_commit} for {version} is not reachable from HEAD."
154+
)
155+
156+
first_parent_commits = git_output(
157+
"rev-list", "--first-parent", "--reverse", "HEAD"
158+
).splitlines()
159+
for candidate in first_parent_commits:
160+
if git_is_ancestor(release_commit, candidate):
161+
return candidate
162+
163+
return release_commit
164+
165+
137166
def main() -> None:
138167
parser = argparse.ArgumentParser(description="Release helpers for sure-aio.")
139168
subparsers = parser.add_subparsers(dest="command", required=True)
@@ -166,6 +195,8 @@ def main() -> None:
166195

167196
commit_parser = subparsers.add_parser("find-release-commit")
168197
commit_parser.add_argument("version")
198+
target_parser = subparsers.add_parser("find-release-target-commit")
199+
target_parser.add_argument("version")
169200

170201
args = parser.parse_args()
171202

@@ -192,6 +223,9 @@ def main() -> None:
192223
if args.command == "find-release-commit":
193224
print(find_release_commit(args.version))
194225
return
226+
if args.command == "find-release-target-commit":
227+
print(find_release_target_commit(args.version))
228+
return
195229

196230
raise SystemExit(f"Unknown command: {args.command}")
197231

tests/unit/test_release_helpers.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from __future__ import annotations
2+
3+
from subprocess import ( # nosec B404 - tests construct return objects only
4+
CompletedProcess,
5+
)
6+
7+
import pytest
8+
9+
from scripts import release
10+
11+
12+
def test_find_release_target_commit_returns_squash_release_commit(
13+
monkeypatch: pytest.MonkeyPatch,
14+
) -> None:
15+
def fake_git_output(*args: str) -> str:
16+
if args == ("log", "--format=%H\t%s", "HEAD"):
17+
return "release-sha\tchore(release): v1.0.0-aio.3\n"
18+
if args == ("rev-parse", "HEAD"):
19+
return "release-sha\n"
20+
raise AssertionError(f"unexpected git_output args: {args}")
21+
22+
monkeypatch.setattr(release, "git_output", fake_git_output)
23+
24+
assert (
25+
release.find_release_target_commit("v1.0.0-aio.3") == "release-sha"
26+
) # nosec B101
27+
28+
29+
def test_find_release_target_commit_returns_merge_commit_after_intervening_main_commit(
30+
monkeypatch: pytest.MonkeyPatch,
31+
) -> None:
32+
def fake_git_output(*args: str) -> str:
33+
if args == ("log", "--format=%H\t%s", "HEAD"):
34+
return "\n".join(
35+
[
36+
"later-sha\tfix(release): later workflow fix",
37+
"merge-sha\tMerge pull request #43 from JSONbored/release/v1.0.0-aio.3",
38+
"main-sha\tfix(ci): intervening main change",
39+
"release-sha\tchore(release): v1.0.0-aio.3",
40+
]
41+
)
42+
if args == ("rev-parse", "HEAD"):
43+
return "later-sha\n"
44+
if args == ("rev-list", "--first-parent", "--reverse", "HEAD"):
45+
return "main-sha\nmerge-sha\nlater-sha\n"
46+
raise AssertionError(f"unexpected git_output args: {args}")
47+
48+
def fake_git_completed(*args: str) -> CompletedProcess[str]:
49+
ancestor_pairs = {
50+
("release-sha", "later-sha"),
51+
("release-sha", "merge-sha"),
52+
}
53+
if args[:2] == ("merge-base", "--is-ancestor"):
54+
return CompletedProcess(
55+
args=args,
56+
returncode=0 if (args[2], args[3]) in ancestor_pairs else 1,
57+
)
58+
raise AssertionError(f"unexpected git_completed args: {args}")
59+
60+
monkeypatch.setattr(release, "git_output", fake_git_output)
61+
monkeypatch.setattr(release, "git_completed", fake_git_completed)
62+
63+
assert (
64+
release.find_release_target_commit("v1.0.0-aio.3") == "merge-sha"
65+
) # nosec B101

0 commit comments

Comments
 (0)