-
Notifications
You must be signed in to change notification settings - Fork 7
ci: add conventional commit and PR base checks #346
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+188
−0
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
84ebd36
ci: add conventional commit PR title check and PR-base validation
declan-scale 9f95e79
ci: replace third-party action with inline conventional-commit regex
declan-scale 3ed13c6
fix(ci): escape brackets in case patterns for bot logins
declan-scale 2c4ff62
ci: comment on validate-pr-base failure, drop conventional-commit ski…
declan-scale b3d8a6a
fix(ci): emit ::error to stdout so GitHub Actions surfaces the annota…
declan-scale 4cf67eb
fix(ci): exempt bots from title check, harden comment posting, pagina…
declan-scale File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,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 | ||
|
declan-scale marked this conversation as resolved.
|
||
|
|
||
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.