mirror of
https://github.com/enix/x509-certificate-exporter.git
synced 2026-08-19 03:56:21 +00:00
286 lines
12 KiB
YAML
286 lines
12 KiB
YAML
name: Release PR
|
|
|
|
# Maintains a perpetually-updated "release PR" against main, computed
|
|
# from Conventional Commits since the last released tag. Merging that
|
|
# PR causes release-please to:
|
|
# - Tag the merge commit as `vX.Y.Z`
|
|
# - Create a GitHub Release stub
|
|
# - That tag push then triggers .github/workflows/release.yaml,
|
|
# which builds binaries + images, signs everything, and finalises
|
|
# the Release with all the assets.
|
|
#
|
|
# Auth runs under a GitHub App identity (RELEASE_PLEASE_APP_*), not the
|
|
# default GITHUB_TOKEN. Two reasons:
|
|
# 1. A tag pushed by GITHUB_TOKEN does NOT trigger downstream workflows
|
|
# (GitHub guard against recursive workflow chains). release.yaml's
|
|
# `on: push: tags: v*` would never fire. App tokens are exempt.
|
|
# 2. PRs/commits authored by the App carry a clean bot identity that
|
|
# stays consistent across release-please's own commits and our
|
|
# post-process commits below.
|
|
#
|
|
# Manual setup required:
|
|
# - GitHub App with these permissions, installed on this repo:
|
|
# Contents: Read & write
|
|
# Pull requests: Read & write
|
|
# Metadata: Read
|
|
# - Client ID + private key as repo secrets (the Client ID is on the
|
|
# App's settings page under "About" → "Client ID"; it is NOT the
|
|
# numeric App ID — those are different identifiers, and
|
|
# actions/create-github-app-token v3+ prefers the Client ID):
|
|
# RELEASE_PLEASE_APP_CLIENT_ID
|
|
# RELEASE_PLEASE_APP_PRIVATE_KEY
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
|
|
# Runner GITHUB_TOKEN is unused by this workflow — every action that
|
|
# touches the repo uses the App token instead. Leaving permissions at
|
|
# the defensive default of `contents: read` for least privilege.
|
|
permissions:
|
|
contents: read
|
|
|
|
# Only one release PR being computed at a time. A second push during an
|
|
# in-flight run would otherwise produce a stale PR or re-tag attempts.
|
|
concurrency:
|
|
group: release-pr
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
release-please:
|
|
name: Release Please
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
steps:
|
|
# Mint a short-lived installation token for the App. This is the
|
|
# identity used by every subsequent step that talks to the repo:
|
|
# checkout (for `git push`), release-please-action, and the
|
|
# post-process step below.
|
|
- name: Generate App token
|
|
id: app-token
|
|
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
|
|
with:
|
|
client-id: ${{ secrets.RELEASE_PLEASE_APP_CLIENT_ID }}
|
|
private-key: ${{ secrets.RELEASE_PLEASE_APP_PRIVATE_KEY }}
|
|
|
|
# Checkout with the App token — `actions/checkout` persists the
|
|
# token in `.git/config` so later `git push` operations from the
|
|
# post-process step authenticate as the App.
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
with:
|
|
token: ${{ steps.app-token.outputs.token }}
|
|
|
|
- uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5
|
|
id: release-please
|
|
with:
|
|
token: ${{ steps.app-token.outputs.token }}
|
|
config-file: release-please-config.json
|
|
manifest-file: .release-please-manifest.json
|
|
|
|
# Post-process: keep three artifacthub.io annotations in sync with
|
|
# the release computed by release-please.
|
|
# - prerelease : "true" when version is -alpha/-beta/-rc
|
|
# - changes : typed ArtifactHub entries from the latest CHANGELOG section
|
|
# - containsSecurityUpdates : "true" if that section has an H3 mentioning
|
|
# "security"
|
|
# Idempotent: re-runs converge on the same Chart.yaml content, so
|
|
# release-please force-pushes that drop our commit are tolerated —
|
|
# the next push to main re-runs us and re-applies.
|
|
- name: Sync chart artifacthub annotations
|
|
env:
|
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
|
|
shell: python
|
|
run: |
|
|
"""
|
|
Update three artifacthub.io annotations on chart/Chart.yaml,
|
|
driven by the release-please PR's computed version and CHANGELOG:
|
|
|
|
- prerelease : "true" iff version is -alpha/-beta/-rc
|
|
- changes : typed ArtifactHub entries (added/fixed/
|
|
changed/security) parsed from the latest
|
|
CHANGELOG section, with commit + issue links
|
|
- containsSecurityUpdates : "true" iff that section has an H3
|
|
sub-section whose title mentions
|
|
"security" (case-insensitive)
|
|
|
|
Idempotent. Re-runs converge on the same Chart.yaml content, so
|
|
release-please force-pushes that drop our commit are tolerated —
|
|
the next push to main re-runs us and re-applies the changes.
|
|
"""
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import yaml
|
|
from pathlib import Path
|
|
|
|
|
|
def run(*args: str) -> None:
|
|
subprocess.run(args, check=True, text=True)
|
|
|
|
|
|
def out(*args: str) -> str:
|
|
return subprocess.run(
|
|
args, check=True, text=True, capture_output=True
|
|
).stdout.strip()
|
|
|
|
|
|
# Find the open release-please PR; bail out if none.
|
|
pr_number = out(
|
|
"gh", "pr", "list",
|
|
"--state", "open",
|
|
"--label", "autorelease: pending",
|
|
"--json", "number",
|
|
"--jq", ".[0].number // empty",
|
|
)
|
|
if not pr_number:
|
|
print("No release-please PR open; skipping annotation sync")
|
|
sys.exit(0)
|
|
|
|
branch = out(
|
|
"gh", "pr", "view", pr_number,
|
|
"--json", "headRefName",
|
|
"--jq", ".headRefName",
|
|
)
|
|
run("git", "fetch", "origin", f"{branch}:{branch}")
|
|
run("git", "checkout", branch)
|
|
|
|
# Resolve new annotation values from the freshly-checked-out PR
|
|
# branch's manifest + CHANGELOG.
|
|
version: str = json.loads(
|
|
Path(".release-please-manifest.json").read_text()
|
|
)["."]
|
|
prerelease = "true" if re.search(r"-(alpha|beta|rc)\b", version, re.I) else "false"
|
|
|
|
# Map release-please CHANGELOG section headers (from release-please-config.json)
|
|
# to ArtifactHub entry kinds.
|
|
SECTION_KIND: dict[str, str] = {
|
|
"Security Updates": "security",
|
|
"Features": "added",
|
|
"Bug Fixes": "fixed",
|
|
"Performance": "changed",
|
|
"Dependencies": "changed",
|
|
"Documentation": "changed",
|
|
}
|
|
|
|
# release-please bullet format:
|
|
# * [**scope:** ]description[ ([sha](commit-url))[, closes [#N](url) ...]]
|
|
RE_BULLET = re.compile(
|
|
r"^\*\s+"
|
|
r"(?:\*\*(?P<scope>[^*]+)\*\*:\s+)?"
|
|
r"(?P<description>.*?)"
|
|
r"(?:\s+\(\[[0-9a-f]+\]\((?P<commit>[^)]+)\)\)"
|
|
r"(?:,\s*closes\s+(?P<issues>.+))?"
|
|
r")?$"
|
|
)
|
|
RE_ISSUE = re.compile(r"\[#(?P<id>\d+)\]\((?P<url>[^)]+)\)")
|
|
|
|
entries: list[dict] = []
|
|
contains_security = "false"
|
|
changelog = Path("CHANGELOG.md")
|
|
if changelog.is_file():
|
|
section_match = re.search(
|
|
r"^##\s+.+?\n(.+?)(?=^##\s|\Z)",
|
|
changelog.read_text(),
|
|
re.M | re.S,
|
|
)
|
|
if section_match:
|
|
section = section_match.group(1)
|
|
current_kind: str | None = None
|
|
for line in section.splitlines():
|
|
h3 = re.match(r"^###\s+(.+)$", line)
|
|
if h3:
|
|
current_kind = SECTION_KIND.get(h3.group(1).strip())
|
|
if current_kind == "security":
|
|
contains_security = "true"
|
|
continue
|
|
if current_kind is None:
|
|
continue
|
|
m = RE_BULLET.match(line.strip())
|
|
if not m or not m.group("description"):
|
|
continue
|
|
scope = m.group("scope")
|
|
description = m.group("description").strip()
|
|
if scope and scope != "*":
|
|
description = f"{scope}: {description}"
|
|
links: list[dict] = []
|
|
if m.group("commit"):
|
|
links.append({"name": "GitHub commit", "url": m.group("commit")})
|
|
if m.group("issues"):
|
|
for issue in RE_ISSUE.finditer(m.group("issues")):
|
|
links.append({
|
|
"name": f'GitHub issue #{issue.group("id")}',
|
|
"url": issue.group("url"),
|
|
})
|
|
entry: dict = {"kind": current_kind, "description": description}
|
|
if links:
|
|
entry["links"] = links
|
|
entries.append(entry)
|
|
|
|
if entries:
|
|
changes_yaml = yaml.dump(entries, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
|
indented = "\n".join(" " + line for line in changes_yaml.splitlines())
|
|
changes_block = " artifacthub.io/changes: |\n" + indented
|
|
else:
|
|
changes_block = ' artifacthub.io/changes: ""'
|
|
|
|
# Apply substitutions on Chart.yaml.
|
|
chart = Path("chart/Chart.yaml")
|
|
content = chart.read_text()
|
|
|
|
content = re.sub(
|
|
r"^( artifacthub\.io/containsSecurityUpdates:).*$",
|
|
rf'\1 "{contains_security}"',
|
|
content,
|
|
flags=re.M,
|
|
)
|
|
|
|
content = re.sub(
|
|
r"^( artifacthub\.io/prerelease:).*$",
|
|
rf'\1 "{prerelease}"',
|
|
content,
|
|
flags=re.M,
|
|
)
|
|
|
|
# `changes` may be on a single line (`: ""`) or a multi-line `|`
|
|
# block. Match the key plus any subsequent lines indented deeper
|
|
# than the key itself.
|
|
content = re.sub(
|
|
r"^ artifacthub\.io/changes:[^\n]*(?:\n {4,}[^\n]*)*",
|
|
lambda _: changes_block,
|
|
content,
|
|
count=1,
|
|
flags=re.M,
|
|
)
|
|
|
|
chart.write_text(content)
|
|
|
|
# Commit only if Chart.yaml actually changed.
|
|
if subprocess.run(["git", "diff", "--quiet", "chart/Chart.yaml"]).returncode == 0:
|
|
print("Chart.yaml annotations already in sync")
|
|
sys.exit(0)
|
|
|
|
# Match commit author to the App identity that release-please
|
|
# itself uses, so the post-process commit is visually consistent
|
|
# with the release-please commit on the same PR.
|
|
#
|
|
# App-issued installation tokens don't represent a user (`/user`
|
|
# 401s) and can't call `/app` (that endpoint requires a JWT
|
|
# signed with the App's private key, not an installation token).
|
|
# The slug is exposed by create-github-app-token as an action
|
|
# output, surfaced here via APP_SLUG. The bot user follows the
|
|
# convention `<slug>[bot]`; its numeric id (needed for the
|
|
# noreply email) comes from /users/<slug>[bot], a regular user
|
|
# endpoint reachable with the installation token.
|
|
import os
|
|
slug = os.environ["APP_SLUG"]
|
|
bot_login = f"{slug}[bot]"
|
|
bot_id = json.loads(out("gh", "api", f"/users/{bot_login}"))["id"]
|
|
run("git", "config", "user.name", bot_login)
|
|
run("git", "config", "user.email",
|
|
f"{bot_id}+{bot_login}@users.noreply.github.com")
|
|
|
|
run("git", "add", "chart/Chart.yaml")
|
|
run("git", "commit", "-m", "chore(chart): sync artifacthub annotations")
|
|
run("git", "push", "origin", branch)
|