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
144 changes: 144 additions & 0 deletions .github/workflows/lint-pr.yaml
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
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Comment thread
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
11 changes: 11 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Contribution workflow

- This repository is a Stainless-generated SDK. Open PRs against the `next` branch (not `main`).
Stainless watches `next` and release-please opens release PRs from `next` → `main`.
- PR titles must follow [Conventional Commits](https://www.conventionalcommits.org/) — the
`Validate PR title (Conventional Commits)` CI check enforces this on every PR.
- The `Validate PR base branch` CI check fails on PRs targeting `main` from non-automation accounts
and posts a comment with resolution steps. Add the `target-main` label only for genuine
exceptions (e.g. an urgent hotfix).
- See `CONTRIBUTING.md` for the full workflow.

## Development Commands

### Package Management in the top level repo
Expand Down
33 changes: 33 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,39 @@ Alternatively if you don't want to install `Rye`, you can stick with the standar
$ pip install -r requirements-dev.lock
```

## Contribution workflow

This repository is generated and released by [Stainless](https://www.stainless.com/). To keep the
release pipeline working, contributions need to follow the branch model and commit conventions below.

### Branch model

- Always open PRs against the `next` branch — not `main`. Stainless watches `next` to produce SDK
builds and the automated version-bump PR.
- Typical flow:
1. Pull the latest `next` locally and branch off it.
2. Make and push your changes, then open a PR targeting `next`.
3. Get the PR reviewed and merged into `next`.
4. Stainless will open (or update) a release PR bumping the version — review and merge that PR
to ship to `main`/PyPI. A new release PR will not be cut while a previous one is still open,
so unblock pending release PRs before expecting a new one.
- Do not merge generated code directly into `next` via PR. Let the generator produce those changes.
- The `Validate PR base branch` CI check fails on PRs targeting `main` from non-automation accounts
and posts a comment with resolution steps. If you genuinely need to PR directly to `main` (e.g. an
urgent hotfix), add the `target-main` label to bypass the check.

### Conventional commits

Commit messages and PR titles must follow [Conventional Commits](https://www.conventionalcommits.org/),
because the changelog and release notes are derived from them. The `Validate PR title (Conventional Commits)`
CI check enforces this on every PR. Common prefixes:

- `feat(api): ...` — new functionality
- `fix(types): ...` — bug fixes
- `docs(readme): ...` — documentation-only changes (required for manual README/docs overrides to be
picked up by the generator)
- `chore(internal): ...` — internal changes that don't affect users

## Modifying/Adding code

Most of the SDK is generated code. Modifications to code will be persisted between generations, but may
Expand Down
Loading