Repository navigation
121 lines (110 loc) · 4.6 KB
/
Copy pathpull-request-metadata.yml
File metadata and controls
121 lines (110 loc) · 4.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
name: Pull Request Metadata
on:
pull_request:
types: [opened, edited, synchronize, reopened]
workflow_dispatch:
inputs:
pr_number:
description: Pull request number to validate
required: true
type: number
permissions:
contents: read
pull-requests: read
jobs:
validate:
name: Pull Request Metadata
runs-on: ubuntu-latest
steps:
- name: Resolve pull request metadata
env:
EVENT_NAME: ${{ github.event_name }}
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ inputs.pr_number }}
run: |
if [[ "${EVENT_NAME}" == "pull_request" ]]; then
cp "${GITHUB_EVENT_PATH}" "${RUNNER_TEMP}/pull-request.json"
else
gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" \
> "${RUNNER_TEMP}/pull-request.json"
fi
- name: Validate branch, title, and description
env:
PR_METADATA_PATH: ${{ runner.temp }}/pull-request.json
run: |
python - <<'PY'
import json
import os
import re
import sys
with open(os.environ["PR_METADATA_PATH"], encoding="utf-8") as metadata_file:
payload = json.load(metadata_file)
pull_request = payload.get("pull_request", payload)
branch = pull_request["head"]["ref"]
title = pull_request["title"]
body = pull_request.get("body") or ""
branch_pattern = re.compile(
r"^(feat|fix|perf|refactor|test|docs|build|ci|chore|revert)/"
r"(?:chi-[0-9]+-)?[a-z0-9]+(?:-[a-z0-9]+)*$"
)
automation_pattern = re.compile(
r"^(release-please--|dependabot/|renovate/|github-actions/)"
)
title_pattern = re.compile(
r"^(feat|fix|perf|refactor|test|docs|build|ci|chore|revert)"
r"(?:\([a-z0-9][a-z0-9-]*\))?!?: [^ ].+$"
)
errors: list[str] = []
automated = automation_pattern.match(branch) is not None
if not automated and branch_pattern.fullmatch(branch) is None:
errors.append(
"Branch must use <type>/<kebab-case> or "
"<type>/chi-<number>-<kebab-case>."
)
if title_pattern.fullmatch(title) is None:
errors.append("PR title must follow Conventional Commits.")
if not automated:
if len(body) > 2_000:
errors.append("PR description must not exceed 2,000 characters.")
sections = list(
re.finditer(r"^## (Why|Changes|Verification)\s*$", body, re.MULTILINE)
)
found = [match.group(1) for match in sections]
expected = ["Why", "Changes", "Verification"]
if found != expected:
errors.append(
"PR description must contain Why, Changes, and Verification "
"sections in that order."
)
else:
for index, match in enumerate(sections):
end = sections[index + 1].start() if index + 1 < len(sections) else len(body)
content = re.sub(r"<!--.*?-->", "", body[match.end():end], flags=re.DOTALL)
if not content.strip():
errors.append(f"PR description section {match.group(1)} is empty.")
if errors:
print(f"Invalid pull request metadata for {branch}:", file=sys.stderr)
for error in errors:
print(f"- {error}", file=sys.stderr)
sys.exit(1)
print(f"Pull request metadata is valid for {branch}.")
PY
- name: Validate release pull request contents
env:
GH_TOKEN: ${{ github.token }}
PR_METADATA_PATH: ${{ runner.temp }}/pull-request.json
run: |
branch="$(jq -r '.pull_request.head.ref // .head.ref' "${PR_METADATA_PATH}")"
if [[ ! "${branch}" =~ ^release-please-- ]]; then
exit 0
fi
pull_request="$(jq -r '.pull_request.number // .number' "${PR_METADATA_PATH}")"
unexpected_files="$(gh api --paginate \
"repos/${GITHUB_REPOSITORY}/pulls/${pull_request}/files" \
--jq '.[].filename' | grep -Ev \
'^(\.release-please-manifest\.json|CHANGELOG\.md|Cargo\.lock|Cargo\.toml|pyproject\.toml|uv\.lock)$' || true)"
if [[ -n "${unexpected_files}" ]]; then
echo "Release pull request contains non-metadata changes:" >&2
echo "${unexpected_files}" >&2
exit 1
fi