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
2 changes: 1 addition & 1 deletion .github/workflows/agentex-tutorials-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Test Tutorial Agents

on:
pull_request:
branches: [main, next]
branches: [main]
push:
branches: [main]
workflow_dispatch:
Expand Down
127 changes: 16 additions & 111 deletions .github/workflows/lint-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,19 @@ on:
- edited
- synchronize
- reopened
- labeled
- unlabeled

env:
# This repo's own SDK automation App's bot, exempt from both checks below.
# This repo's own SDK automation App's bot, exempt from the title check below.
#
# Matched by the bot user's numeric ID, not its login. A GitHub App bot's login
# follows the App's name, so renaming the App renamed its bot and silently broke
# the previous login-based entry. Worse, the old name was then free to register,
# so any App that took it would have inherited the exemption. A user ID never
# changes and is never reused.
SDK_AUTOMATION_BOT_ID: '333044712'
# The release App's bot, which opens the release-please pull requests. Exempt
# from the title check too, and matched by ID for the same reasons.
RELEASE_BOT_ID: '335733747'

jobs:
validate-pr-title:
Expand All @@ -34,19 +35,19 @@ jobs:
# 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.
# don't want their PRs blocked by this check.
#
# This repo's own SDK automation App is exempt too, matched by ID through
# SDK_AUTOMATION_BOT_ID at the top of this file. release-please runs here as
# a CLI under that App rather than as the release-please[bot] GitHub App, so
# its release pull requests are authored by the App's bot and the list below
# never matched them. Their titles come from release-please's configured
# pull-request-title-pattern, which is not always a Conventional Commits type
# and cannot be changed without also changing the string release-please
# parses back when it cuts the release. The same App opens the promote pull
# requests.
if [ "$PR_AUTHOR_ID" = "$SDK_AUTOMATION_BOT_ID" ]; then
echo "PR is from this repo's SDK automation ($PR_AUTHOR); skipping title check."
# This repo's own automation Apps are exempt too, matched by ID through
# SDK_AUTOMATION_BOT_ID and RELEASE_BOT_ID at the top of this file. The SDK
# automation App opens the promote pull requests. release-please runs here as
# a CLI under the release App rather than as the release-please[bot] GitHub
# App, so its release pull requests are authored by that App's bot and the
# list below never matched them. Their titles come from release-please's
# configured pull-request-title-pattern, which is not always a Conventional
# Commits type and cannot be changed without also changing the string
# release-please parses back when it cuts the release.
if [ "$PR_AUTHOR_ID" = "$SDK_AUTOMATION_BOT_ID" ] || [ "$PR_AUTHOR_ID" = "$RELEASE_BOT_ID" ]; then
echo "PR is from this repo's own automation ($PR_AUTHOR); skipping title check."
exit 0
fi
case "$PR_AUTHOR" in
Expand Down Expand Up @@ -77,99 +78,3 @@ jobs:
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_AUTHOR_ID: ${{ github.event.pull_request.user.id }}
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).
if [ "$PR_AUTHOR_ID" = "$SDK_AUTOMATION_BOT_ID" ]; then
delete_comment
echo "PR is from this repo's SDK automation ($PR_AUTHOR); allowing PR targeting main."
exit 0
fi
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
2 changes: 1 addition & 1 deletion .github/workflows/release-doctor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
release_doctor:
name: release doctor
runs-on: ubuntu-latest
if: github.repository == 'scaleapi/scale-agentex-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
if: github.repository == 'scaleapi/scale-agentex-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please'))

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down
72 changes: 52 additions & 20 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,50 @@ name: Release Please
# Hand-edited from the stlc-generated template. `.github/workflows/*.yml` is
# scaffold-once, so this survives every later build -- upstream's own source cites
# exactly this PAT-to-App swap as the reason that preservation exists. Do NOT run
# `stlc build --rewrite-scaffold` without reapplying these three changes.
# `stlc build --rewrite-scaffold` without reapplying the changes below.
#
# What changed from the generated file, and why each is load-bearing:
#
# 1. App token instead of `secrets.RELEASE_PLEASE_TOKEN`, which does not exist
# and which we do not want to create -- eliminating PATs was the point of the
# App migration. It is deliberately NOT `GITHUB_TOKEN`: releases created by
# 1. A token minted from a dedicated release App instead of
# `secrets.RELEASE_PLEASE_TOKEN`, which does not exist and which we do not want
# to create -- eliminating PATs was the point of the App migration. It is
# deliberately NOT the codegen App, so the codegen App's key does not have to
# live on the production repos. Nor is it `GITHUB_TOKEN`: releases created by
# GITHUB_TOKEN do not trigger other workflows, so publish-*.yml would never
# fire and the release would stop one hop short of the registry.
#
# 2. The `npx release-please@16` CLI instead of googleapis/release-please-action.
# scale-agentex-typescript sets `allowed_actions: selected` and does not permit
# that action; the CLI needs only actions/-owned steps, which
# `github_owned_allowed: true` covers on both production repos.
# 2. The release-please CLI (`npx`, exact version) instead of
# googleapis/release-please-action. scale-agentex-typescript sets
# `allowed_actions: selected` and does not permit that action; the CLI needs
# only actions/-owned steps, which `github_owned_allowed: true` covers on both
# production repos.
#
# 3. `issues: write` on the minted token. release-please drives its
# autorelease:pending -> autorelease:tagged labels through the Issues API.
# Without it you get duplicate release pull requests. The generated file omits
# it, and the omission is silent until it bites.
#
# Requires AGENTEX_SDK_SYNC_PRIVATE_KEY (secret) and AGENTEX_SDK_SYNC_APP_ID
# (variable) on the PRODUCTION repo -- a workflow only reads secrets from the repo
# it runs in, and the guard below means that is production.
# 4. `environment: release` on the job. That environment holds the release App's
# key and deploys from main only. GitHub creates any environment a job names
# that does not exist yet, with no protection, so a typo in the name silently
# creates an unprotected environment.
#
# 5. `github-release` runs before `release-pr` (the release-please-action order).
# A release-pr failure, such as the workflows refusal below, then cannot stop a
# merged release PR from being tagged and published. In the other order
# release-pr skips the run while a merged release is still untagged ("untagged,
# merged release PRs outstanding"), so the next release PR waits for another push.
#
# The release App has no `workflows` permission. If release-please reports "refusing
# to allow a GitHub App to create or update workflow", close the release PR and
# delete its branch, then re-run this workflow (or wait for the next push to main);
# it opens the release PR again from main.
#
# Requires, on the PRODUCTION repo (a workflow only reads secrets from the repo it
# runs in, and the guard below means that is production):
# - the secret AGENTEX_RELEASE_APP_PRIVATE_KEY in the environment `release`
# - the repository variable AGENTEX_RELEASE_APP_ID
# - the release App installed on this repo (contents, pull requests and issues: write)
on:
push:
branches:
Expand All @@ -35,19 +56,29 @@ on:
permissions:
contents: read

# One run at a time, so two quick pushes cannot race to open duplicate release PRs or
# cut the same release twice. A running job is never cancelled; a newer push replaces
# a still-queued run, which is harmless because release-please reads live repo state.
concurrency:
group: release-please
cancel-in-progress: false

jobs:
release-please:
# Self-routing: this file is SHA-identical on the staging trunk, where it must
# stay inert. Only production cuts releases.
if: github.repository == 'scaleapi/scale-agentex-python'
runs-on: ubuntu-latest
# See 4 above. Keep this name identical to the provisioned environment, and give
# it no required reviewers: a reviewer there would hold every push to main.
environment: release
steps:
- name: Mint release token
id: release-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.AGENTEX_SDK_SYNC_APP_ID }}
private-key: ${{ secrets.AGENTEX_SDK_SYNC_PRIVATE_KEY }}
app-id: ${{ vars.AGENTEX_RELEASE_APP_ID }}
private-key: ${{ secrets.AGENTEX_RELEASE_APP_PRIVATE_KEY }}
owner: scaleapi
repositories: scale-agentex-python
permission-contents: write
Expand All @@ -59,22 +90,23 @@ jobs:
with:
node-version: '20'

- name: Release PR + GitHub release
- name: GitHub release + release PR
env:
RP_TOKEN: ${{ steps.release-token.outputs.token }}
run: |
# release-pr opens or updates the version-bump pull request;
# github-release turns an already-merged one into the tag + GitHub Release
# that publish-pypi.yml / publish-npm.yml trigger on. Both are idempotent,
# so running the pair on every push carries a release the whole way.
# github-release turns an already-merged release PR into the tag + GitHub
# Release that publish-pypi.yml / publish-npm.yml trigger on; release-pr then
# opens or updates the next version-bump pull request. Both are idempotent,
# so running the pair on every push carries a release the whole way, and a
# release-pr failure cannot stop a merged release from being tagged.
#
# No checkout step is needed: release-please reads the config and manifest
# from the repo over the API.
npx --yes release-please@16 release-pr \
npx --yes release-please@16.18.0 github-release \
--token="$RP_TOKEN" --repo-url="${{ github.repository }}" \
--config-file=release-please-config.json \
--manifest-file=.release-please-manifest.json
npx --yes release-please@16 github-release \
npx --yes release-please@16.18.0 release-pr \
--token="$RP_TOKEN" --repo-url="${{ github.repository }}" \
--config-file=release-please-config.json \
--manifest-file=.release-please-manifest.json
15 changes: 10 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## 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`.
- This repository is a Stainless-generated SDK. Open PRs against `main`; there is no `next` branch
any more, and no label is needed. Merging the release-please PR publishes the release to PyPI.
- 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).
- Most of the SDK is generated, so API-surface changes belong in the API spec in
scaleapi/scale-agentex (`agentex/openapi.yaml`), not in generated files. Hand-written code lives
in `src/agentex/lib/` and `examples/`.
- For maintainers: promote PRs open as drafts; approve them, never mark them ready or merge them,
then, once checks are green, re-run the promote workflow. Merge a human PR into `main` only while
staging `main` is an ancestor of production `main` and no codegen run is in flight, then run the
back-sync (`stlc-sync.yml` in scaleapi/scale-agentex). Promote, then release, in one sitting. The
runbook is at the top of `.github/workflows/stlc-promote.yml` in scaleapi/scale-agentex.
- See `CONTRIBUTING.md` for the full workflow.

## Development Commands
Expand Down
36 changes: 24 additions & 12 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,31 @@ release pipeline working, contributions need to follow the branch model and comm

### Branch model

- Always open PRs against the `next` branch — not `main`. Stainless watches `next` to produce SDK
builds and the automated version-bump PR.
- Open PRs against `main`, the only integration branch. There is no `next` branch any more, and no
label is needed.
- 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.
1. Pull the latest `main` locally and branch off it.
2. Make and push your changes, then open a PR targeting `main`.
3. Get the PR reviewed and merged into `main`.
4. release-please keeps a release PR open on `main` that bumps the version and changelog. Merging
that PR cuts the release and publishes it to PyPI.
- Most of the SDK is generated from the API spec. Changes to the API surface belong in the spec in
[scaleapi/scale-agentex](https://github.com/scaleapi/scale-agentex) (`agentex/openapi.yaml`), not
in generated files, where hand edits are replayed onto every regeneration and can conflict with
it. Hand-written code lives in `src/agentex/lib/` and `examples/`, which the generator never
modifies.

#### For maintainers

- Promote PRs (`chore: promote staging … to production`) open as drafts. Approve them, but never
mark them ready for review or merge them; once their checks are green, re-run the promote
workflow, which fast-forwards `main`.
- Merge a human PR into `main` only while staging `main` is an ancestor of production `main` and no
codegen run is in flight, then run the back-sync workflow (`stlc-sync.yml` in
scaleapi/scale-agentex).
- Promote, then release, in one sitting.
- See the runbook at the top of `.github/workflows/stlc-promote.yml` in scaleapi/scale-agentex for
details.

### Conventional commits

Expand Down
Loading
Loading