diff --git a/.github/workflows/agentex-tutorials-test.yml b/.github/workflows/agentex-tutorials-test.yml index 7d57dff09..2d0e23371 100644 --- a/.github/workflows/agentex-tutorials-test.yml +++ b/.github/workflows/agentex-tutorials-test.yml @@ -2,7 +2,7 @@ name: Test Tutorial Agents on: pull_request: - branches: [main, next] + branches: [main] push: branches: [main] workflow_dispatch: diff --git a/.github/workflows/lint-pr.yaml b/.github/workflows/lint-pr.yaml index bde91e1f3..28cc60759 100644 --- a/.github/workflows/lint-pr.yaml +++ b/.github/workflows/lint-pr.yaml @@ -7,11 +7,9 @@ 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 @@ -19,6 +17,9 @@ env: # 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: @@ -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 @@ -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='' - - # 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 diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index a20022ce7..84d635e4a 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -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 diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index e9ddc6392..5797341fc 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -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: @@ -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 @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 7dd7f2ed3..ea404e29b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e8e6e8813..27cb7c8e0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 7e1e4ae8a..40654d3fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,9 +2,11 @@ # This is the Stainless-generated REST client. The hand-authored ADK # overlay (formerly `src/agentex/lib/*`) now lives in `adk/` and ships # as the sibling `agentex-sdk` package — see `adk/pyproject.toml`. +# Keep `description` as generated: codegen rewrites `version` on every release, +# and a hand edit on the line next to it conflicts each time. name = "agentex-client" version = "0.28.2" -description = "The official Python REST client for the Agentex API" +description = "The official Python library for the agentex API" dynamic = ["readme"] license = "Apache-2.0" authors = [