Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,8 @@ jobs:
packages: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0

- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
Expand Down Expand Up @@ -418,15 +420,23 @@ jobs:
wrapper_track="${aio_track}"
else
changelog_version="$(python3 scripts/release.py latest-changelog-version 2>/dev/null || true)"
release_commit_pattern="^chore\\(release\\): $(python3 -c 'import re, sys; print(re.escape(sys.argv[1]))' "${changelog_version}")( \\(#[0-9]+\\))?$"
case "${changelog_version}" in
"${upstream_version}"-aio.*)
candidate_revision="${changelog_version##*.}"
if [[ "${candidate_revision}" =~ ^[0-9]+$ ]] && printf '%s\n' "${commit_message}" | grep -Eq "${release_commit_pattern}"; then
release_target="$(python3 scripts/release.py find-release-target-commit "${changelog_version}" 2>/dev/null || true)"
if [[ "${candidate_revision}" =~ ^[0-9]+$ && "${release_target}" == "${GITHUB_SHA}" ]]; then
Comment thread
JSONbored marked this conversation as resolved.
aio_track="aio-v${candidate_revision}"
release_package_tag="${upstream_version}-${aio_track}"
version_label="${release_package_tag}"
wrapper_track="${aio_track}"
else
release_commit_pattern="^chore\\(release\\): $(python3 -c 'import re, sys; print(re.escape(sys.argv[1]))' "${changelog_version}")( \\(#[0-9]+\\))?$"
if [[ "${candidate_revision}" =~ ^[0-9]+$ ]] && printf '%s\n' "${commit_message}" | grep -Eq "${release_commit_pattern}"; then
aio_track="aio-v${candidate_revision}"
release_package_tag="${upstream_version}-${aio_track}"
version_label="${release_package_tag}"
wrapper_track="${aio_track}"
fi
fi
;;
esac
Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ jobs:
echo "release_version=${release_version}" >> "${GITHUB_OUTPUT}"
release_commit="$(python3 scripts/release.py find-release-commit "${release_version}")"
echo "release_commit=${release_commit}" >> "${GITHUB_OUTPUT}"
echo "Matched release commit ${release_commit} for ${release_version}"
release_target="$(python3 scripts/release.py find-release-target-commit "${release_version}")"
echo "release_target=${release_target}" >> "${GITHUB_OUTPUT}"
echo "Matched release commit ${release_commit} and target ${release_target} for ${release_version}"

- name: Require successful CI for release commit
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_COMMIT: ${{ steps.version.outputs.release_commit }}
RELEASE_COMMIT: ${{ steps.version.outputs.release_target }}
WORKFLOW_SELECTOR: build.yml
run: |
runs_json="$(gh run list \
Expand Down Expand Up @@ -84,7 +86,7 @@ jobs:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_VERSION: ${{ steps.version.outputs.release_version }}
RELEASE_COMMIT: ${{ steps.version.outputs.release_commit }}
RELEASE_COMMIT: ${{ steps.version.outputs.release_target }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
if git rev-parse "${RELEASE_VERSION}" >/dev/null 2>&1; then
Expand All @@ -110,7 +112,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_VERSION: ${{ steps.version.outputs.release_version }}
RELEASE_NOTES: ${{ steps.notes.outputs.release_notes }}
RELEASE_COMMIT: ${{ steps.version.outputs.release_commit }}
RELEASE_COMMIT: ${{ steps.version.outputs.release_target }}
run: |
export GITHUB_TOKEN="${RELEASE_TOKEN:-${GITHUB_TOKEN}}"

Expand Down
4 changes: 2 additions & 2 deletions docs/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ Release commits also publish the immutable packaging line tag, for example `v0.6
1. Trigger **Prepare Release / Sure-AIO** from `main`.
2. The workflow computes the next `upstream-aio.N` version, updates `CHANGELOG.md`, syncs the XML `<Changes>` block, and opens a release PR.
3. Review and merge that PR into `main`.
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.
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.
5. Trigger **Publish Release / Sure-AIO** from `main`.
6. The workflow verifies CI on the exact release commit, creates the Git tag if needed, and publishes the GitHub Release.
6. The workflow verifies CI on the exact release target commit, creates the Git tag if needed, and publishes the GitHub Release.
34 changes: 34 additions & 0 deletions scripts/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,35 @@ def find_release_commit(version: str) -> str:
)


def git_is_ancestor(ancestor: str, descendant: str) -> bool:
return (
git_completed("merge-base", "--is-ancestor", ancestor, descendant).returncode
== 0
)


def find_release_target_commit(version: str) -> str:
release_commit = find_release_commit(version)
head = git_output("rev-parse", "HEAD").strip()

if release_commit == head:
return release_commit

if not git_is_ancestor(release_commit, head):
raise SystemExit(
f"Release commit {release_commit} for {version} is not reachable from HEAD."
)

first_parent_commits = git_output(
"rev-list", "--first-parent", "--reverse", "HEAD"
).splitlines()
for candidate in first_parent_commits:
if git_is_ancestor(release_commit, candidate):
return candidate

return release_commit


def main() -> None:
parser = argparse.ArgumentParser(description="Release helpers for sure-aio.")
subparsers = parser.add_subparsers(dest="command", required=True)
Expand Down Expand Up @@ -166,6 +195,8 @@ def main() -> None:

commit_parser = subparsers.add_parser("find-release-commit")
commit_parser.add_argument("version")
target_parser = subparsers.add_parser("find-release-target-commit")
target_parser.add_argument("version")

args = parser.parse_args()

Expand All @@ -192,6 +223,9 @@ def main() -> None:
if args.command == "find-release-commit":
print(find_release_commit(args.version))
return
if args.command == "find-release-target-commit":
print(find_release_target_commit(args.version))
return

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

Expand Down
65 changes: 65 additions & 0 deletions tests/unit/test_release_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from __future__ import annotations

from subprocess import ( # nosec B404 - tests construct return objects only
CompletedProcess,
)

import pytest

from scripts import release


def test_find_release_target_commit_returns_squash_release_commit(
monkeypatch: pytest.MonkeyPatch,
) -> None:
def fake_git_output(*args: str) -> str:
if args == ("log", "--format=%H\t%s", "HEAD"):
return "release-sha\tchore(release): v1.0.0-aio.3\n"
if args == ("rev-parse", "HEAD"):
return "release-sha\n"
raise AssertionError(f"unexpected git_output args: {args}")

monkeypatch.setattr(release, "git_output", fake_git_output)

assert (
release.find_release_target_commit("v1.0.0-aio.3") == "release-sha"
) # nosec B101


def test_find_release_target_commit_returns_merge_commit_after_intervening_main_commit(
monkeypatch: pytest.MonkeyPatch,
) -> None:
def fake_git_output(*args: str) -> str:
if args == ("log", "--format=%H\t%s", "HEAD"):
return "\n".join(
[
"later-sha\tfix(release): later workflow fix",
"merge-sha\tMerge pull request #43 from JSONbored/release/v1.0.0-aio.3",
"main-sha\tfix(ci): intervening main change",
"release-sha\tchore(release): v1.0.0-aio.3",
]
)
if args == ("rev-parse", "HEAD"):
return "later-sha\n"
if args == ("rev-list", "--first-parent", "--reverse", "HEAD"):
return "main-sha\nmerge-sha\nlater-sha\n"
raise AssertionError(f"unexpected git_output args: {args}")

def fake_git_completed(*args: str) -> CompletedProcess[str]:
ancestor_pairs = {
("release-sha", "later-sha"),
("release-sha", "merge-sha"),
}
if args[:2] == ("merge-base", "--is-ancestor"):
return CompletedProcess(
args=args,
returncode=0 if (args[2], args[3]) in ancestor_pairs else 1,
)
raise AssertionError(f"unexpected git_completed args: {args}")

monkeypatch.setattr(release, "git_output", fake_git_output)
monkeypatch.setattr(release, "git_completed", fake_git_completed)

assert (
release.find_release_target_commit("v1.0.0-aio.3") == "merge-sha"
) # nosec B101
18 changes: 18 additions & 0 deletions tests/unit/test_workflow_release_checkout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from __future__ import annotations

from pathlib import Path


def test_publish_checkout_fetches_full_history_before_release_target_lookup() -> None:
workflow = Path(".github/workflows/build.yml").read_text()
release_lookup = (
'release_target="$(python3 scripts/release.py find-release-target-commit'
)
release_lookup_index = workflow.index(release_lookup)
checkout_index = workflow.rfind("uses: actions/checkout@", 0, release_lookup_index)

assert checkout_index != -1 # nosec B101

checkout_block = workflow[checkout_index:release_lookup_index]

assert "fetch-depth: 0" in checkout_block # nosec B101
Loading