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
41 changes: 35 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false

- name: Install git-cliff
env:
Expand Down Expand Up @@ -53,7 +54,17 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_VERSION: ${{ steps.version.outputs.release_version }}
run: |
git-cliff --config cliff.toml --tag "${RELEASE_VERSION}" --output CHANGELOG.md
previous_tag="$(python3 scripts/release.py latest-aio-tag)"
if [[ -n "${previous_tag}" ]]; then
git-cliff --config cliff.toml --tag "${RELEASE_VERSION}" "${previous_tag}..HEAD" --output CHANGELOG.md
else
git-cliff --config cliff.toml --tag "${RELEASE_VERSION}" --output CHANGELOG.md
fi
parsed_version="$(python3 scripts/release.py latest-changelog-version)"
if [[ "${parsed_version}" != "${RELEASE_VERSION}" ]]; then
echo "Generated changelog top section ${parsed_version} does not match ${RELEASE_VERSION}" >&2
exit 1
fi

- name: Create release PR
if: steps.changes.outputs.has_changes == 'true'
Expand All @@ -80,18 +91,21 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false

- name: Determine release version
id: version
run: |
release_version="$(python3 scripts/release.py latest-changelog-version)"
echo "release_version=${release_version}" >> "${GITHUB_OUTPUT}"
release_commit="$(git log --format='%H%x09%s' HEAD | awk -F '\t' -v title="chore(release): ${release_version}" '$2 == title {print $1; exit}')"
if [[ -z "${release_commit}" ]]; then
echo "Unable to find a merged release commit for ${release_version} on main." >&2
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}"
changelog_version="$(python3 scripts/release.py latest-changelog-version)"
if [[ "${changelog_version}" != "${release_version}" ]]; then
echo "CHANGELOG top entry ${changelog_version} does not match ${release_version}" >&2
exit 1
fi
echo "release_commit=${release_commit}" >> "${GITHUB_OUTPUT}"

- name: Extract release notes
id: notes
Expand All @@ -106,25 +120,40 @@ jobs:

- name: Create Git tag if missing
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_VERSION: ${{ steps.version.outputs.release_version }}
RELEASE_COMMIT: ${{ steps.version.outputs.release_commit }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
if git rev-parse "${RELEASE_VERSION}" >/dev/null 2>&1; then
echo "Tag ${RELEASE_VERSION} already exists; skipping."
exit 0
fi

auth_token="${RELEASE_TOKEN:-${GITHUB_TOKEN}}"
if [[ -z "${auth_token}" ]]; then
echo "No token available for tag creation." >&2
exit 1
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local --unset-all http.https://github.com/.extraheader || true
git tag "${RELEASE_VERSION}" "${RELEASE_COMMIT}"
git push origin "${RELEASE_VERSION}"
git push "https://x-access-token:${auth_token}@github.com/${GITHUB_REPOSITORY}.git" "${RELEASE_VERSION}"

- name: Publish GitHub release
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
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 }}
run: |
export GITHUB_TOKEN="${RELEASE_TOKEN:-${GITHUB_TOKEN}}"

if gh release view "${RELEASE_VERSION}" >/dev/null 2>&1; then
echo "GitHub release ${RELEASE_VERSION} already exists; skipping."
exit 0
fi
notes_file="$(mktemp)"
Expand Down
43 changes: 42 additions & 1 deletion scripts/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
DEFAULT_CHANGELOG = ROOT / "CHANGELOG.md"
DEFAULT_DOCKERFILE = ROOT / "Dockerfile"
DEFAULT_UPSTREAM = ROOT / "upstream.toml"
AIO_TAG_PATTERN = "*-aio.*"


def load_upstream_version_key(path: pathlib.Path) -> str:
Expand Down Expand Up @@ -46,6 +47,19 @@ def git_tags() -> list[str]:
return [line.strip() for line in output.splitlines() if line.strip()]


def latest_aio_release_tag() -> str | None:
completed = subprocess.run(
["git", "describe", "--tags", "--abbrev=0", "--match", AIO_TAG_PATTERN, "HEAD"],
cwd=ROOT,
text=True,
capture_output=True,
)
if completed.returncode != 0:
return None
tag = completed.stdout.strip()
return tag or None


def latest_release_tag(dockerfile: pathlib.Path, upstream: pathlib.Path) -> str | None:
upstream_version = read_upstream_version(dockerfile, upstream)
pattern = re.compile(rf"^{re.escape(upstream_version)}-aio\.(\d+)$")
Expand All @@ -61,7 +75,7 @@ def latest_release_tag(dockerfile: pathlib.Path, upstream: pathlib.Path) -> str


def has_unreleased_changes(dockerfile: pathlib.Path, upstream: pathlib.Path) -> bool:
latest_tag = latest_release_tag(dockerfile, upstream)
latest_tag = latest_aio_release_tag()
if latest_tag is None:
return True
output = subprocess.check_output(["git", "log", "--format=%s", f"{latest_tag}..HEAD"], cwd=ROOT, text=True)
Expand Down Expand Up @@ -111,6 +125,24 @@ def extract_release_notes(version: str, changelog: pathlib.Path) -> str:
return notes


def find_release_commit(version: str) -> str:
exact = f"chore(release): {version}"
with_suffix = re.compile(rf"^{re.escape(exact)} \(#\d+\)$")

output = subprocess.check_output(["git", "log", "--format=%H\t%s", "HEAD"], cwd=ROOT, text=True)
for line in output.splitlines():
if not line.strip():
continue
sha, subject = line.split("\t", 1)
if subject == exact or with_suffix.match(subject):
return sha

raise SystemExit(
f"Unable to find a merged release commit for {version} on main. "
f"Expected '{exact}' or '{exact} (#123)'."
)


def main() -> None:
parser = argparse.ArgumentParser(description="Release helpers for AIO repos.")
subparsers = parser.add_subparsers(dest="command", required=True)
Expand All @@ -123,20 +155,29 @@ def main() -> None:
changes_parser = subparsers.add_parser("has-unreleased-changes")
changes_parser.add_argument("--dockerfile", type=pathlib.Path, default=DEFAULT_DOCKERFILE)
changes_parser.add_argument("--upstream-config", type=pathlib.Path, default=DEFAULT_UPSTREAM)
subparsers.add_parser("latest-aio-tag")
latest_parser = subparsers.add_parser("latest-changelog-version")
latest_parser.add_argument("--changelog", type=pathlib.Path, default=DEFAULT_CHANGELOG)
notes_parser = subparsers.add_parser("extract-release-notes")
notes_parser.add_argument("version")
notes_parser.add_argument("--changelog", type=pathlib.Path, default=DEFAULT_CHANGELOG)
commit_parser = subparsers.add_parser("find-release-commit")
commit_parser.add_argument("version")
args = parser.parse_args()
if args.command == "upstream-version":
print(read_upstream_version(args.dockerfile, args.upstream_config))
elif args.command == "next-version":
print(next_release_version(args.dockerfile, args.upstream_config))
elif args.command == "has-unreleased-changes":
print("true" if has_unreleased_changes(args.dockerfile, args.upstream_config) else "false")
elif args.command == "latest-aio-tag":
latest_tag = latest_aio_release_tag()
if latest_tag:
print(latest_tag)
elif args.command == "latest-changelog-version":
print(latest_changelog_version(args.changelog))
elif args.command == "find-release-commit":
print(find_release_commit(args.version))
else:
print(extract_release_notes(args.version, args.changelog))

Expand Down
Loading