-
Notifications
You must be signed in to change notification settings - Fork 8
144 lines (129 loc) · 6.21 KB
/
lint-pr.yaml
File metadata and controls
144 lines (129 loc) · 6.21 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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
name: Lint PR
on:
pull_request:
types:
- opened
- edited
- synchronize
- reopened
- labeled
- unlabeled
jobs:
validate-pr-title:
name: Validate PR title (Conventional Commits)
runs-on: ubuntu-latest
steps:
- name: Check Conventional Commits format
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
# Exempt automated PRs (Stainless codegen, release-please, dependabot, etc.).
# These bots may not always emit Conventional-Commits-formatted titles
# (dependabot's default "Bump foo from 1.0 to 1.1" doesn't match) and we
# don't want their PRs blocked by this check. Mirrors validate-pr-base.
case "$PR_AUTHOR" in
stainless-app|stainless-app\[bot\]|release-please\[bot\]|github-actions\[bot\]|dependabot\[bot\])
echo "PR is from automation ($PR_AUTHOR); skipping title check."
exit 0
;;
esac
# Conventional Commits: <type>(<optional-scope>)(!): <subject>
PATTERN='^(feat|fix|docs|style|refactor|test|chore|ci|build|perf|revert)(\([^)]+\))?!?: .+'
if printf '%s' "$PR_TITLE" | grep -qE "$PATTERN"; then
echo "PR title is a valid Conventional Commit: $PR_TITLE"
exit 0
fi
# ::error must be on stdout for GitHub Actions to surface it as an annotation.
echo "::error title=Invalid PR title::PR title must follow Conventional Commits format. Got: $PR_TITLE"
{
echo " Got: $PR_TITLE"
echo " Expected: <type>(<optional-scope>)(!): <subject>"
echo " Types: feat, fix, docs, style, refactor, test, chore, ci, build, perf, revert"
echo ""
echo " Examples:"
echo " feat: add new endpoint"
echo " fix(client): handle empty response"
echo " chore!: drop python 3.11 support"
} >&2
exit 1
validate-pr-base:
name: Validate PR base branch
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Validate base branch and manage PR comment
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
PR_BASE: ${{ github.event.pull_request.base.ref }}
HAS_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'target-main') }}
run: |
MARKER='<!-- lint-pr-validate-base -->'
# Look up an existing marker comment so we can update/delete it.
# --paginate handles PRs with >30 comments. If the lookup fails
# (transient API error, fork PR token without read scope), continue
# with no existing_id so we still emit the failure annotation.
existing_id=$(gh api --paginate "repos/$REPO/issues/$PR_NUMBER/comments" \
--jq ".[] | select(.body | contains(\"$MARKER\")) | .id" 2>/dev/null \
| head -n1) || existing_id=""
delete_comment() {
if [ -n "$existing_id" ]; then
gh api -X DELETE "repos/$REPO/issues/comments/$existing_id" >/dev/null 2>&1 || true
fi
}
# PR doesn't target main — nothing to enforce.
if [ "$PR_BASE" != "main" ]; then
delete_comment
echo "PR base is '$PR_BASE'; check passes."
exit 0
fi
# Exempt automated PRs (must mirror validate-pr-title's list).
case "$PR_AUTHOR" in
stainless-app|stainless-app\[bot\]|release-please\[bot\]|github-actions\[bot\]|dependabot\[bot\])
delete_comment
echo "PR is from automation ($PR_AUTHOR); allowing PR targeting main."
exit 0
;;
esac
# Per-PR opt-out via label.
if [ "$HAS_LABEL" = "true" ]; then
delete_comment
echo "Found 'target-main' label; allowing PR targeting main."
exit 0
fi
# Failure path: try to post or update an explanatory comment.
# The write may fail on fork PRs (GITHUB_TOKEN has read-only scope
# upstream) or due to transient API errors. Guard each gh call so
# the ::error annotation and exit 1 still run regardless.
body_file=$(mktemp)
{
echo "$MARKER"
echo
echo "**This PR is targeting \`main\`, but PRs should target the \`next\` branch by default.**"
echo
echo "The \`main\` branch is reserved for release-please and Stainless automation. To resolve, pick one of:"
echo
echo "- **Re-target the PR to \`next\`** (recommended). On the PR page, click **Edit** next to the title and change the base branch to \`next\`."
echo "- **Add the \`target-main\` label** if this is an intentional exception (e.g. an urgent hotfix). The check will re-run and pass."
echo
echo "See \`CONTRIBUTING.md\` for the full branch model."
} > "$body_file"
comment_status="ok"
if [ -n "$existing_id" ]; then
gh api -X PATCH "repos/$REPO/issues/comments/$existing_id" \
-F body=@"$body_file" >/dev/null 2>&1 || comment_status="failed"
[ "$comment_status" = "ok" ] && echo "Updated existing PR comment ($existing_id)."
else
gh pr comment "$PR_NUMBER" --repo "$REPO" --body-file "$body_file" >/dev/null 2>&1 || comment_status="failed"
[ "$comment_status" = "ok" ] && echo "Posted new PR comment."
fi
if [ "$comment_status" = "failed" ]; then
echo "::warning title=Could not write PR comment::Likely a fork PR (no upstream write scope) or a transient API error. The check still fails — see the next annotation for resolution steps."
fi
# ::error must be on stdout to surface as an annotation.
echo "::error title=PR should target 'next'::Re-target to 'next' or add the 'target-main' label. See the PR comment for full details."
exit 1