From 9e39767e809b04f0786fb17377eb800fa624bddc Mon Sep 17 00:00:00 2001 From: stlc-bot Date: Tue, 4 Aug 2026 21:00:00 +0000 Subject: [PATCH 1/3] ci: add stlc promotion workflows --- .github/workflows/stlc-promote.yml | 61 ++++++++++++++++ .github/workflows/stlc-sync.yml | 107 +++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 .github/workflows/stlc-promote.yml create mode 100644 .github/workflows/stlc-sync.yml diff --git a/.github/workflows/stlc-promote.yml b/.github/workflows/stlc-promote.yml new file mode 100644 index 0000000..0812667 --- /dev/null +++ b/.github/workflows/stlc-promote.yml @@ -0,0 +1,61 @@ +name: Promote SDKs + +# Manually fast-forwards production main to the reviewed staging main. The +# ancestor check refuses divergent histories; this workflow never force-pushes. +on: + workflow_dispatch: {} + +permissions: + contents: read + +jobs: + promote: + if: github.repository == 'kernel/kernel-go-sdk-staging' + runs-on: ${{ vars.STLC_RUNNER || 'ubuntu-latest' }} + environment: production + steps: + - name: Check out staging + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Mint production token + id: production-token + uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 + with: + app-id: ${{ secrets.ADMIN_APP_ID }} + private-key: ${{ secrets.ADMIN_APP_PRIVATE_KEY }} + owner: kernel + repositories: kernel-go-sdk + permission-contents: write + permission-workflows: write + + - name: Fetch production main + env: + GH_TOKEN: ${{ steps.production-token.outputs.token }} + PRODUCTION_REPO: kernel/kernel-go-sdk + run: | + git remote add production "https://x-access-token:${GH_TOKEN}@github.com/${PRODUCTION_REPO}.git" + git fetch production main + + - name: Check whether production already has staging's content + id: diff + run: | + MERGED=$(git merge-tree --write-tree production/main origin/main) || MERGED=conflict + PRODUCTION_TREE=$(git rev-parse 'production/main^{tree}') + if [ "$MERGED" = "$PRODUCTION_TREE" ]; then + echo "Production already contains staging's content. Nothing to promote." + echo "synced=true" >> "$GITHUB_OUTPUT" + else + echo "synced=false" >> "$GITHUB_OUTPUT" + fi + + - name: Promote staging to production + if: steps.diff.outputs.synced == 'false' + run: | + if ! git merge-base --is-ancestor production/main origin/main; then + echo "::error title=Promote blocked::production/main is not an ancestor of staging main. Back-sync production first." + exit 1 + fi + git push production origin/main:refs/heads/main diff --git a/.github/workflows/stlc-sync.yml b/.github/workflows/stlc-sync.yml new file mode 100644 index 0000000..ca263e1 --- /dev/null +++ b/.github/workflows/stlc-sync.yml @@ -0,0 +1,107 @@ +name: Sync SDK repos + +# Keeps production and staging on one fast-forward-only history. Optional +# dispatch tokens make the polling loop eager; the scheduled back-sync is the +# safety backstop when those secrets are absent. +on: + schedule: + - cron: '7,37 * * * *' + workflow_dispatch: {} + repository_dispatch: + types: [prod-released] + release: + types: [published] + push: + branches: [main] + +jobs: + back-sync: + if: >- + github.repository == 'kernel/kernel-go-sdk-staging' && + (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch') + runs-on: ${{ vars.STLC_RUNNER || 'ubuntu-latest' }} + permissions: + contents: write + concurrency: + group: stlc-back-sync + cancel-in-progress: true + steps: + - name: Check out staging + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - name: Fetch production main + run: | + git remote add production "https://github.com/kernel/kernel-go-sdk.git" + git -c "http.https://github.com/.extraheader=" fetch production main + + - name: Check whether production has content staging lacks + id: diff + run: | + MERGED=$(git merge-tree --write-tree origin/main production/main) || MERGED=conflict + STAGING_TREE=$(git rev-parse 'origin/main^{tree}') + if [ "$MERGED" = "$STAGING_TREE" ]; then + echo "Staging already has production's content. Nothing to pull back." + echo "behind=false" >> "$GITHUB_OUTPUT" + else + echo "behind=true" >> "$GITHUB_OUTPUT" + fi + + - name: Sync production to staging + if: steps.diff.outputs.behind == 'true' + run: | + if ! git merge-base --is-ancestor origin/main production/main; then + echo "::error title=Back-sync blocked::staging main is not an ancestor of production/main." + exit 1 + fi + git push origin production/main:refs/heads/main + + notify-back-sync: + if: >- + github.repository == 'kernel/kernel-go-sdk' && + (github.event_name == 'release' || github.event_name == 'workflow_dispatch') + runs-on: ${{ vars.STLC_RUNNER || 'ubuntu-latest' }} + permissions: + contents: read + steps: + - name: Dispatch back-sync to staging + env: + DISPATCH_TOKEN: ${{ secrets.STAGING_DISPATCH_TOKEN }} + REF_NAME: ${{ github.ref_name }} + run: | + set -euo pipefail + if [ -z "${DISPATCH_TOKEN:-}" ]; then + echo "::notice::STAGING_DISPATCH_TOKEN not configured; the scheduled back-sync remains active." + exit 0 + fi + payload=$(jq -n --arg ref "$REF_NAME" '{event_type:"prod-released",client_payload:{ref:$ref}}') + curl --fail-with-body -sS -X POST -H "Authorization: Bearer $DISPATCH_TOKEN" -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" "https://api.github.com/repos/kernel/kernel-go-sdk-staging/dispatches" -d "$payload" + + seal-dispatch: + if: github.repository == 'kernel/kernel-go-sdk-staging' && github.event_name == 'push' + runs-on: ${{ vars.STLC_RUNNER || 'ubuntu-latest' }} + permissions: + contents: read + concurrency: + group: seal-dispatch-${{ github.ref }} + cancel-in-progress: false + steps: + - name: Dispatch tracking sync + env: + DISPATCH_TOKEN: ${{ secrets.CONFIG_DISPATCH_TOKEN }} + HEAD_MSG: ${{ github.event.head_commit.message }} + HEAD_AUTHOR_NAME: ${{ github.event.head_commit.author.name }} + SHA: ${{ github.sha }} + run: | + set -euo pipefail + if printf '%s' "$HEAD_MSG" | grep -q 'Stainless-Generated-From' || [ "$HEAD_AUTHOR_NAME" = "stlc-bot" ]; then + echo "Generated commit; skipping tracking dispatch." + exit 0 + fi + if [ -z "${DISPATCH_TOKEN:-}" ]; then + echo "::notice::CONFIG_DISPATCH_TOKEN not configured; the config repo's scheduled sync remains active." + exit 0 + fi + payload=$(jq -n --arg sha "$SHA" --arg repo "${{ github.repository }}" '{event_type:"seal-custom-code",client_payload:{target:"all",sha:$sha,repo:$repo}}') + curl --fail-with-body -sS -X POST -H "Authorization: Bearer $DISPATCH_TOKEN" -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" "https://api.github.com/repos/kernel/kernel/dispatches" -d "$payload" From b94cd585bac0a149f65dab3212731ad312628ce6 Mon Sep 17 00:00:00 2001 From: Steven Miller Date: Tue, 4 Aug 2026 21:03:55 +0000 Subject: [PATCH 2/3] ci: promote through merge-commit pull requests --- .github/workflows/stlc-promote.yml | 32 ++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/.github/workflows/stlc-promote.yml b/.github/workflows/stlc-promote.yml index 0812667..b0ed377 100644 --- a/.github/workflows/stlc-promote.yml +++ b/.github/workflows/stlc-promote.yml @@ -1,8 +1,11 @@ name: Promote SDKs -# Manually fast-forwards production main to the reviewed staging main. The -# ancestor check refuses divergent histories; this workflow never force-pushes. +# Production requires pull requests, so staging is promoted through a merge- +# commit PR. Never squash or rebase this cross-repo PR: preserving the incoming +# commits keeps production and staging on one ancestry chain. on: + push: + branches: [main] workflow_dispatch: {} permissions: @@ -12,7 +15,9 @@ jobs: promote: if: github.repository == 'kernel/kernel-go-sdk-staging' runs-on: ${{ vars.STLC_RUNNER || 'ubuntu-latest' }} - environment: production + concurrency: + group: stlc-promote + cancel-in-progress: true steps: - name: Check out staging uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -30,6 +35,7 @@ jobs: repositories: kernel-go-sdk permission-contents: write permission-workflows: write + permission-pull-requests: write - name: Fetch production main env: @@ -51,11 +57,21 @@ jobs: echo "synced=false" >> "$GITHUB_OUTPUT" fi - - name: Promote staging to production + - name: Push the production release branch if: steps.diff.outputs.synced == 'false' + env: + GH_TOKEN: ${{ steps.production-token.outputs.token }} + PRODUCTION_REPO: kernel/kernel-go-sdk + run: git push production origin/main:refs/heads/stainless/release --force + + - name: Open or update the promote PR + if: steps.diff.outputs.synced == 'false' + env: + GH_TOKEN: ${{ steps.production-token.outputs.token }} + PRODUCTION_REPO: kernel/kernel-go-sdk run: | - if ! git merge-base --is-ancestor production/main origin/main; then - echo "::error title=Promote blocked::production/main is not an ancestor of staging main. Back-sync production first." - exit 1 + existing=$(gh pr list --repo "$PRODUCTION_REPO" --head stainless/release --state open --json number --jq '.[0].number') + if [ -z "$existing" ]; then + gh pr create --repo "$PRODUCTION_REPO" --base main --head stainless/release --title "Release SDK updates" --body "$(git log --oneline production/main..origin/main)" fi - git push production origin/main:refs/heads/main + gh pr merge stainless/release --repo "$PRODUCTION_REPO" --merge --auto || echo "Auto-merge unavailable; merge the promote PR manually with a merge commit." From f0262729f36648472689bae53ba860d7feda1b38 Mon Sep 17 00:00:00 2001 From: Steven Miller Date: Wed, 5 Aug 2026 13:09:02 +0000 Subject: [PATCH 3/3] ci: harden promotion pull request updates --- .github/workflows/stlc-promote.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/stlc-promote.yml b/.github/workflows/stlc-promote.yml index b0ed377..5d446ce 100644 --- a/.github/workflows/stlc-promote.yml +++ b/.github/workflows/stlc-promote.yml @@ -70,8 +70,14 @@ jobs: GH_TOKEN: ${{ steps.production-token.outputs.token }} PRODUCTION_REPO: kernel/kernel-go-sdk run: | - existing=$(gh pr list --repo "$PRODUCTION_REPO" --head stainless/release --state open --json number --jq '.[0].number') + body=$(mktemp) + git log --oneline production/main..origin/main > "$body" + existing=$(gh pr list --repo "$PRODUCTION_REPO" --head stainless/release --state open --json number --jq 'if length == 0 then "" else .[0].number end') if [ -z "$existing" ]; then - gh pr create --repo "$PRODUCTION_REPO" --base main --head stainless/release --title "Release SDK updates" --body "$(git log --oneline production/main..origin/main)" + gh pr create --repo "$PRODUCTION_REPO" --base main --head stainless/release --title "Release SDK updates" --body-file "$body" + else + gh pr edit "$existing" --repo "$PRODUCTION_REPO" --title "Release SDK updates" --body-file "$body" + fi + if ! gh pr merge stainless/release --repo "$PRODUCTION_REPO" --merge --auto; then + echo "::warning title=Manual promotion required::Merge the promote PR with a merge commit." fi - gh pr merge stainless/release --repo "$PRODUCTION_REPO" --merge --auto || echo "Auto-merge unavailable; merge the promote PR manually with a merge commit."