Skip to content
Merged
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
110 changes: 73 additions & 37 deletions .github/workflows/stlc-generate.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
name: Generate SDKs with stlc

# One workflow, three modes:
# One workflow, two modes:
# - pull_request: build every target, push a preview branch per staging repo, sticky-comment diff links.
# - push to main: build and push staging main directly (Promote then fast-forwards production), then seal tracking files back here.
# - schedule / dispatch: rebuild main; `stlc build` absorbs out-of-band staging custom code and re-seals, so this doubles as the tracking-file sync (the SDK repos' seal-dispatch fires it eagerly).
# - push to main (or manual dispatch): back-sync release commits from production into staging,
# build and push staging main directly (Promote then fast-forwards production), then seal
# tracking files back here.
#
# The schedule is only a backstop: `repository_dispatch: seal-custom-code` already
# fires as soon as an SDK repo seals custom code, so the cron exists purely to catch
# a missed dispatch. It ran every 6h until it was found to be the sole source of a
# no-op seal PR per run — see the churn guard in "Seal tracking files" below.
# There is deliberately no cron here, and nothing runs unless you push. The schedule this
# workflow used to carry was not the backstop its comment claimed: the `repository_dispatch`
# chain it was meant to back up never fired even once, because the PATs those dispatch jobs
# needed (CONFIG_DISPATCH_TOKEN, STAGING_DISPATCH_TOKEN) were never created, so both jobs hit
# their "not configured" skip on every run. The cron was doing 100% of the sync work — and
# was the sole source of the no-op seal PR churn. Rather than reinstate that cross-repo
# chain, the back-sync it depended on now runs inline below, with the credentials this
# workflow already has. If you are tempted to add a schedule back, add the missing sync
# to the push path instead.
on:
pull_request:
types: [opened, synchronize, reopened]
Expand All @@ -22,13 +28,9 @@ on:
- 'stainless/**'
- '.github/workflows/stlc-generate.yml'
- '.github/actions/setup-stlc/**'
schedule:
- cron: '0 6 * * *'
workflow_dispatch: {}
repository_dispatch:
types: [seal-custom-code]

run-name: ${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch') && 'Sync custom-code tracking' || '' }}
run-name: ${{ github.event_name == 'workflow_dispatch' && 'Manual rebuild and tracking sync' || '' }}

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
Expand Down Expand Up @@ -120,7 +122,7 @@ jobs:
echo "MSG_EOF"
} >> "$GITHUB_OUTPUT"

- name: Hold codegen while production is ahead (release not back-synced)
- name: Back-sync production into staging
if: github.event_name != 'pull_request'
working-directory: ${{ env.STAINLESS_WORKSPACE }}
env:
Expand All @@ -129,17 +131,22 @@ jobs:
run: |
set -euo pipefail

# Release commits (version bumps, changelogs) land on the production repos, so a
# staging trunk trails production after every release and codegen would otherwise
# rebuild on a stale base. Fast-forward staging from production right here, so a
# push to this repo is the only event the pipeline ever needs. This replaces the
# cross-repo back-sync workflow that used to poll every 30 minutes.
ls_heads() { git -c credential.helper= ls-remote --heads "$1" main; }

held=""
diverged=""
while IFS=$'\t' read -r target staging prod; do
{ [ -z "$prod" ] || [ "$prod" = "null" ] || [ "$staging" = "$prod" ]; } && continue

prod_url="https://github.com/${prod}.git"
if ! prod_heads=$(ls_heads "$prod_url"); then
prod_url="https://x-access-token:${GH_TOKEN}@github.com/${prod}.git"
if ! prod_heads=$(ls_heads "$prod_url"); then
echo "::warning title=Codegen hold skipped for ${target}::cannot verify production sync: ${prod} is unreachable with the configured credential. The hold is skipped for this target; grant the token read access to the production repo to enable it."
echo "::warning title=Back-sync skipped for ${target}::production repo ${prod} is unreachable with the configured credential. The build proceeds on staging as-is; grant the token read access to ${prod} to enable the back-sync."
continue
fi
fi
Expand All @@ -148,7 +155,7 @@ jobs:

staging_url="https://x-access-token:${GH_TOKEN}@github.com/${staging}.git"
if ! staging_heads=$(ls_heads "$staging_url"); then
echo "::warning title=Codegen hold skipped for ${target}::cannot verify production sync: staging repo ${staging} is unreachable. The hold is skipped for this target; if this is not transient, the build step will fail loudly on its own."
echo "::warning title=Back-sync skipped for ${target}::staging repo ${staging} is unreachable. If this is not transient, the build step will fail loudly on its own."
continue
fi

Expand All @@ -157,16 +164,25 @@ jobs:
d=$(mktemp -d); git -C "$d" init -q
git -C "$d" -c credential.helper= fetch -q "$staging_url" main:refs/remotes/staging/main
git -C "$d" -c credential.helper= fetch -q "$prod_url" main:refs/remotes/prod/main
if ! git -C "$d" merge-base --is-ancestor prod/main staging/main; then
held="$held $target"

if git -C "$d" merge-base --is-ancestor prod/main staging/main; then
echo "${target}: staging already contains production — nothing to back-sync."
elif git -C "$d" merge-base --is-ancestor staging/main prod/main; then
git -C "$d" -c credential.helper= push -q "$staging_url" prod/main:refs/heads/main
echo "${target}: fast-forwarded ${staging} main to ${prod} main."
else
# Neither is an ancestor of the other, so something was committed to production
# outside the promote path. Fast-forwarding would silently drop it — stop instead.
diverged="$diverged $target"
fi
rm -rf "$d"
done < <(yq -r '.targets | to_entries[] | [.key, (.value.staging_repo // .value.production_repo), (.value.production_repo // .value.staging_repo)] | @tsv' openapi.stainless.yml)
if [ -n "$held" ]; then
echo "::error title=Codegen held::production is ahead of the staging trunk (a release isn't back-synced yet) for:$held. Wait for the back-sync, then re-run."

if [ -n "$diverged" ]; then
echo "::error title=Back-sync blocked::staging and production have diverged for:$diverged. Production carries commits that are not on the staging trunk, so it cannot be fast-forwarded. Reconcile by hand, then re-run."
exit 1
fi
echo "No target has production verified ahead of staging — proceeding to build."
echo "Staging trunks are level with production — proceeding to build."

- name: Generate SDKs and push
env:
Expand Down Expand Up @@ -293,22 +309,42 @@ jobs:
exit 0
fi

branch="stlc/seal-tracking"
git checkout -B "$branch"
git add "$STAINLESS_WORKSPACE/custom-code"
git commit -m "chore(stlc): seal custom-code tracking files"
git push --force "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" "$branch"
open_pr=$(gh pr list --head "$branch" --state open --json number --jq '.[0].number // empty')
if [ -z "$open_pr" ]; then
gh pr create --base main --head "$branch" \
--title "chore(stlc): seal custom-code tracking files" \
--body "Automated by the stlc generate workflow: \`stlc build\` re-sealed custom code (after a spec change on \`main\`, or after absorbing custom code that landed on a staging repo out of band), updating the tracking files under \`stainless/custom-code/\`. Merging brings the config repo in sync with the SDK repos. Safe to merge — tracking-files-only commits are skipped by this workflow, so it won't trigger another build."
fi
# Deliberately not auto-merged. With the churn guard above, a seal PR only
# appears when custom code genuinely moved, which is worth a human look.
# Surface it loudly instead: stale tracking files block later builds.
pr_num="${open_pr:-$(gh pr list --head "$branch" --state open --json number --jq '.[0].number // empty')}"
echo "::warning title=Seal PR awaiting review::Custom code genuinely changed — review and merge ${REPO}#${pr_num:-?}. Stale tracking files block later builds until they are synced."
# Committed straight to main rather than raised as a PR. A seal is bookkeeping —
# it records the SHAs of custom code that already exists in the SDK repos, so
# there is no decision in it to review, and an unmerged seal PR blocks later
# builds. The `guard` job skips regeneration for tracking-files-only pushes, so
# this cannot loop. The churn guard above still runs first, so SHA-only churn is
# discarded rather than committed.
target_branch="${GITHUB_REF_NAME}"
push_url="https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git"

staged="$(mktemp -d)"
cp -a "$STAINLESS_WORKSPACE/custom-code/." "$staged/"

for attempt in 1 2 3; do
# Re-anchor on the current tip each time: main may have moved while we built.
git fetch -q --depth=1 "$push_url" "$target_branch"
git checkout -q -B stlc-seal FETCH_HEAD
rm -rf "${STAINLESS_WORKSPACE:?}/custom-code"
mkdir -p "$STAINLESS_WORKSPACE/custom-code"
cp -a "$staged/." "$STAINLESS_WORKSPACE/custom-code/"
git add -A "$STAINLESS_WORKSPACE/custom-code"

if git diff --cached --quiet; then
echo "Tracking files already sealed on ${target_branch} — nothing to push."
exit 0
fi

git commit -q -m "chore(stlc): seal custom-code tracking files"
if git push -q "$push_url" "HEAD:refs/heads/${target_branch}"; then
echo "Sealed custom-code tracking files onto ${target_branch}."
exit 0
fi
echo "Push rejected (${target_branch} moved) — retrying (attempt ${attempt}/3)."
done

echo "::error title=Seal failed::could not push tracking files to ${target_branch} after 3 attempts. Stale tracking files block later builds — reseal by re-running this workflow."
exit 1

- name: Alert on failure
if: failure()
Expand Down
Loading