From 5d937bf333e11e98251a16fa5f0103d5760bf770 Mon Sep 17 00:00:00 2001 From: Zachary Chua Date: Thu, 20 Aug 2026 14:57:02 -0700 Subject: [PATCH 1/3] Add publish-private-preview workflow Adds a workflow_dispatch workflow that publishes v1 private-preview specs for a given API version and cuts a release, mirroring the private-preview (beta) path of pay-server's api-codegen/tools/publish.sh. It pulls the specs from the Statics Commander CDN (instead of a local build dir), commits them to master, generates per-language diffs against the private-preview spec, and tags the release. Triggered by the zoolander LockAndReleasePrivatePreviewSpecs workflow. A dry_run input (default true) fetches, builds, and diffs without pushing, uploading the would-be output as an artifact for inspection. Co-Authored-By: Claude Opus 4.8 Committed-By-Agent: claude --- .github/workflows/publish-private-preview.yml | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 .github/workflows/publish-private-preview.yml diff --git a/.github/workflows/publish-private-preview.yml b/.github/workflows/publish-private-preview.yml new file mode 100644 index 000000000..52f1bb647 --- /dev/null +++ b/.github/workflows/publish-private-preview.yml @@ -0,0 +1,170 @@ +name: Publish Private Preview + +# Publishes the private-preview specs for a given API version and cuts a release. +# +# Functionally equivalent to the `beta` (private-preview) path of pay-server's +# api-codegen/tools/publish.sh, with two differences: +# 1. Specs are pulled from the Statics Commander CDN instead of a local build dir. +# 2. Per-language diffs are computed against the private-preview spec (not the GA +# spec), so the upcoming-changes reflect actual private-preview deltas. +# +# Triggered by the zoolander LockAndReleasePrivatePreviewSpecs workflow, which +# dispatches this workflow with the released version name. + +on: + workflow_dispatch: + inputs: + version: + description: 'Version name (e.g., 2026-07-08.preview)' + required: true + type: string + dry_run: + description: 'Dry run: fetch, build, and diff but do not push commits or tags' + required: false + type: boolean + default: true + +permissions: + contents: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Fetch app installation token + uses: tibdex/github-app-token@v1.5.2 + id: gh-api-token + with: + app_id: ${{ secrets.GH_APP_STRIPE_OPENAPI_APP_ID }} + private_key: ${{ secrets.GH_APP_STRIPE_OPENAPI_PRIVATE_KEY }} + + - name: Checkout openapi + uses: actions/checkout@v4 + with: + token: ${{ steps.gh-api-token.outputs.token }} + # Full history + tags are required so `git describe --tags` can find the + # last release tag and `git show :...` can read the prior spec. + fetch-depth: 0 + fetch-tags: true + + - name: Checkout sdk-codegen (provides the diff CLI) + uses: actions/checkout@v4 + with: + repository: stripe/sdk-codegen + ref: v17.0.0 + path: sdk-codegen + + - name: Setup just + uses: extractions/setup-just@v2 + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: '24' + + - name: Download private-preview specs from CDN + env: + VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + BASE="https://b.stripecdn.com/api-artifacts/assets/openapi/${VERSION}/private_preview" + + # Required specs — fail the job if absent. + curl -fsSL "$BASE/spec3.private_preview.sdk.yaml" -o openapi/spec3.private_preview.sdk.yaml + curl -fsSL "$BASE/spec3.private_preview.sdk.json" -o openapi/spec3.private_preview.sdk.json + + # Optional fixtures — skip cleanly if the CDN returns 404. + for f in fixtures3.private_preview.yaml fixtures3.private_preview.json; do + if ! curl -fsSL "$BASE/$f" -o "openapi/$f"; then + echo "Optional file $f absent from CDN; skipping." + rm -f "openapi/$f" + fi + done + + - name: Commit specs, generate diffs, and cut release + env: + DRY_RUN: ${{ inputs.dry_run }} + run: | + set -euo pipefail + + git config user.name "Stripe OpenAPI" + git config user.email "105521251+stripe-openapi[bot]@users.noreply.github.com" + + # --- Commit fixtures (if changed) --- + for f in openapi/fixtures3.private_preview.yaml openapi/fixtures3.private_preview.json; do + if [ -f "$f" ]; then git add "$f"; fi + done + if [ -n "$(git diff --name-only --staged)" ]; then + git commit -m "Update fixture data for private preview" + fi + + # --- Commit spec (if changed); only cut a release when the spec moved --- + git add ./openapi/spec3.private_preview.sdk.yaml ./openapi/spec3.private_preview.sdk.json + if [ -n "$(git diff --name-only --staged)" ]; then + git commit -m "Update OpenAPI specification for private preview" + + LAST_TAG=$(git describe --tags --abbrev=0) + NEW_TAG="v$(( ${LAST_TAG#v} + 1 ))" + echo "Previous tag ${LAST_TAG}; new tag ${NEW_TAG}" + + # Materialize the prior private-preview spec from the last release tag. + SPEC_PATH="openapi/spec3.private_preview.sdk.yaml" + OLD_SPEC="/tmp/oldspec.private_preview.sdk.yaml" + git show "${LAST_TAG}:${SPEC_PATH}" > "$OLD_SPEC" + + DIFF_DIR="${GITHUB_WORKSPACE}/openapi/upcoming-changes" + NEW_SPEC="${GITHUB_WORKSPACE}/${SPEC_PATH}" + mkdir -p "$DIFF_DIR" + + # Build the sdk-codegen CLI once, then invoke it the same way the + # justfile's `main` recipe does (node dist/main.js with node_modules/.bin + # on PATH) to avoid re-running yarn+tsc for every language. + cd "${GITHUB_WORKSPACE}/sdk-codegen" + just build-tools + export PATH="${PWD}/node_modules/.bin:${PATH}" + + # `rest.md` is the language-agnostic diff (no --lang), matching publish.sh. + node ./dist/main.js diff --old-path "$OLD_SPEC" --new-path "$NEW_SPEC" --output-path "$DIFF_DIR/rest.md" + for lang in go php node ruby java python dotnet; do + node ./dist/main.js diff --lang "$lang" --old-path "$OLD_SPEC" --new-path "$NEW_SPEC" --output-path "$DIFF_DIR/$lang.md" + done + cd "${GITHUB_WORKSPACE}" + + # --- Commit upcoming-changes (if changed) --- + git add ./openapi/upcoming-changes + if [ -n "$(git diff --name-only --staged)" ]; then + git commit -m "Update upcoming changes" + fi + + # Private-preview releases don't surface a full diff in the release notes. + git tag "$NEW_TAG" -m "This release only includes changes to the private preview spec." + + if [ "$DRY_RUN" = "true" ]; then + echo "Dry run: not pushing. Would push master + tag ${NEW_TAG}. Local commits/diffs are available as an artifact." + else + # Push branch and tag atomically so we never leave a dangling tag. + git push --atomic origin master "$NEW_TAG" + fi + else + echo "No private-preview spec changes; skipping release." + fi + + if [ "$DRY_RUN" = "true" ]; then + echo "Dry run: not pushing master." + else + # Catch-all: pushes a fixtures-only commit; no-op if the atomic push already ran. + git push origin master + fi + + - name: Upload generated output (dry-run inspection) + if: ${{ inputs.dry_run }} + uses: actions/upload-artifact@v4 + with: + name: private-preview-output + path: | + openapi/spec3.private_preview.sdk.yaml + openapi/spec3.private_preview.sdk.json + openapi/fixtures3.private_preview.yaml + openapi/fixtures3.private_preview.json + openapi/upcoming-changes/ + if-no-files-found: warn From 2c8c3bdd6d7157413b900c779233e9aa2c7aace4 Mon Sep 17 00:00:00 2001 From: Zachary Chua Date: Thu, 20 Aug 2026 15:48:06 -0700 Subject: [PATCH 2/3] Match publish.sh diff target; dry-run pushes to scratch branch - Diff the GA spec (openapi/spec3.sdk.yaml), old-tag vs. working tree, exactly as publish.sh's beta path does, instead of the private-preview spec. - In dry-run, force-push local commits to a disposable dry-run/private-preview branch for inspection instead of uploading an artifact; never create or push a tag (a stray vN would corrupt the next real run's describe increment). Co-Authored-By: Claude Opus 4.8 Committed-By-Agent: claude --- .github/workflows/publish-private-preview.yml | 43 ++++++++----------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/.github/workflows/publish-private-preview.yml b/.github/workflows/publish-private-preview.yml index 52f1bb647..803976d42 100644 --- a/.github/workflows/publish-private-preview.yml +++ b/.github/workflows/publish-private-preview.yml @@ -3,10 +3,9 @@ name: Publish Private Preview # Publishes the private-preview specs for a given API version and cuts a release. # # Functionally equivalent to the `beta` (private-preview) path of pay-server's -# api-codegen/tools/publish.sh, with two differences: -# 1. Specs are pulled from the Statics Commander CDN instead of a local build dir. -# 2. Per-language diffs are computed against the private-preview spec (not the GA -# spec), so the upcoming-changes reflect actual private-preview deltas. +# api-codegen/tools/publish.sh. The only difference: specs are pulled from the +# Statics Commander CDN instead of a local build dir. Per-language diffs are computed +# against the GA spec (openapi/spec3.sdk.yaml), exactly as publish.sh does. # # Triggered by the zoolander LockAndReleasePrivatePreviewSpecs workflow, which # dispatches this workflow with the released version name. @@ -90,6 +89,9 @@ jobs: git config user.name "Stripe OpenAPI" git config user.email "105521251+stripe-openapi[bot]@users.noreply.github.com" + # In a dry run, local commits are pushed here for inspection instead of master. + TEST_BRANCH="dry-run/private-preview" + # --- Commit fixtures (if changed) --- for f in openapi/fixtures3.private_preview.yaml openapi/fixtures3.private_preview.json; do if [ -f "$f" ]; then git add "$f"; fi @@ -107,9 +109,10 @@ jobs: NEW_TAG="v$(( ${LAST_TAG#v} + 1 ))" echo "Previous tag ${LAST_TAG}; new tag ${NEW_TAG}" - # Materialize the prior private-preview spec from the last release tag. - SPEC_PATH="openapi/spec3.private_preview.sdk.yaml" - OLD_SPEC="/tmp/oldspec.private_preview.sdk.yaml" + # Materialize the prior spec from the last release tag. Mirrors publish.sh, + # which diffs the GA spec (openapi/spec3.sdk.yaml) — old tag vs. working tree. + SPEC_PATH="openapi/spec3.sdk.yaml" + OLD_SPEC="/tmp/oldspec.sdk.yaml" git show "${LAST_TAG}:${SPEC_PATH}" > "$OLD_SPEC" DIFF_DIR="${GITHUB_WORKSPACE}/openapi/upcoming-changes" @@ -137,11 +140,12 @@ jobs: fi # Private-preview releases don't surface a full diff in the release notes. - git tag "$NEW_TAG" -m "This release only includes changes to the private preview spec." - if [ "$DRY_RUN" = "true" ]; then - echo "Dry run: not pushing. Would push master + tag ${NEW_TAG}. Local commits/diffs are available as an artifact." + # Never create/push a real tag in a dry run — a stray vN tag would corrupt + # the next real run's `git describe` increment. + echo "Dry run: would create tag ${NEW_TAG} and push master." else + git tag "$NEW_TAG" -m "This release only includes changes to the private preview spec." # Push branch and tag atomically so we never leave a dangling tag. git push --atomic origin master "$NEW_TAG" fi @@ -150,21 +154,12 @@ jobs: fi if [ "$DRY_RUN" = "true" ]; then - echo "Dry run: not pushing master." + # Push all local commits to a disposable scratch branch for inspection + # instead of master (master and tags are left untouched). Force is expected: + # each dry run builds a fresh history off the current master. + echo "Dry run: force-pushing local commits to ${TEST_BRANCH} for inspection." + git push --force origin "HEAD:refs/heads/${TEST_BRANCH}" else # Catch-all: pushes a fixtures-only commit; no-op if the atomic push already ran. git push origin master fi - - - name: Upload generated output (dry-run inspection) - if: ${{ inputs.dry_run }} - uses: actions/upload-artifact@v4 - with: - name: private-preview-output - path: | - openapi/spec3.private_preview.sdk.yaml - openapi/spec3.private_preview.sdk.json - openapi/fixtures3.private_preview.yaml - openapi/fixtures3.private_preview.json - openapi/upcoming-changes/ - if-no-files-found: warn From e76ddf635afe502670809a32067349dc2b51ed48 Mon Sep 17 00:00:00 2001 From: Zachary Chua Date: Fri, 21 Aug 2026 11:25:08 -0700 Subject: [PATCH 3/3] Generalize workflow with a mode input; rename to publish-v1-specs Adds a `mode` choice input (private-preview / public-preview / GA / all). Only private-preview is implemented; the other channels fail fast at a new "Resolve mode configuration" step until the zoolander dispatcher and CDN keys for them are wired up. Mode-specific values (CDN subpath, spec/fixture filenames, commit and tag messages, dry-run branch) are resolved into env vars so the download and release steps stay mode-agnostic. Dry-run branch is now dry-run/. Co-Authored-By: Claude Opus 4.8 Committed-By-Agent: claude --- ...vate-preview.yml => publish-v1-specs.yaml} | 96 +++++++++++++------ 1 file changed, 69 insertions(+), 27 deletions(-) rename .github/workflows/{publish-private-preview.yml => publish-v1-specs.yaml} (63%) diff --git a/.github/workflows/publish-private-preview.yml b/.github/workflows/publish-v1-specs.yaml similarity index 63% rename from .github/workflows/publish-private-preview.yml rename to .github/workflows/publish-v1-specs.yaml index 803976d42..c9b4eee29 100644 --- a/.github/workflows/publish-private-preview.yml +++ b/.github/workflows/publish-v1-specs.yaml @@ -1,14 +1,18 @@ -name: Publish Private Preview +name: Publish v1 Specs -# Publishes the private-preview specs for a given API version and cuts a release. +# Publishes v1 OpenAPI specs for a given API version and cuts a release, mirroring +# the corresponding path of pay-server's api-codegen/tools/publish.sh. Specs are +# pulled from the Statics Commander CDN instead of a local build dir. # -# Functionally equivalent to the `beta` (private-preview) path of pay-server's -# api-codegen/tools/publish.sh. The only difference: specs are pulled from the -# Statics Commander CDN instead of a local build dir. Per-language diffs are computed -# against the GA spec (openapi/spec3.sdk.yaml), exactly as publish.sh does. +# Modes (see the `mode` input): +# - private-preview: implemented. Mirrors publish.sh's `beta` path. +# - public-preview / GA / all: NOT implemented yet. These channels are published +# today by a separate pipeline (zoolander PublishOpenApiArtifacts -> +# sync-openapi-artifacts.yml, which only mirrors raw specs — no fixtures, diffs, +# or tags). They are reserved here for a future change that extends the zoolander +# LockAndReleasePrivatePreviewSpecs dispatcher to cover them. # -# Triggered by the zoolander LockAndReleasePrivatePreviewSpecs workflow, which -# dispatches this workflow with the released version name. +# Triggered by the zoolander LockAndReleasePrivatePreviewSpecs workflow. on: workflow_dispatch: @@ -17,8 +21,18 @@ on: description: 'Version name (e.g., 2026-07-08.preview)' required: true type: string + mode: + description: 'Which spec channel to publish' + required: true + type: choice + default: private-preview + options: + - private-preview + - public-preview + - GA + - all dry_run: - description: 'Dry run: fetch, build, and diff but do not push commits or tags' + description: 'Dry run: fetch, build, and diff but do not push to master or tag' required: false type: boolean default: true @@ -61,19 +75,49 @@ jobs: with: node-version: '24' - - name: Download private-preview specs from CDN + - name: Resolve mode configuration + env: + MODE: ${{ inputs.mode }} + run: | + set -euo pipefail + # Only private-preview is implemented today. The other channels exit here + # until the zoolander dispatcher and CDN keys for them are wired up. + case "$MODE" in + private-preview) + { + echo "CDN_DIR=private_preview" + echo "SPEC_FILES=spec3.private_preview.sdk.yaml spec3.private_preview.sdk.json" + echo "FIXTURE_FILES=fixtures3.private_preview.yaml fixtures3.private_preview.json" + echo "FIXTURE_MSG=Update fixture data for private preview" + echo "SPEC_MSG=Update OpenAPI specification for private preview" + echo "TAG_MSG=This release only includes changes to the private preview spec." + echo "TEST_BRANCH=dry-run/private-preview" + } >> "$GITHUB_ENV" + ;; + public-preview | GA | all) + echo "::error::mode '$MODE' is not implemented yet" + exit 1 + ;; + *) + echo "::error::unknown mode '$MODE'" + exit 1 + ;; + esac + + - name: Download specs from CDN env: VERSION: ${{ inputs.version }} run: | set -euo pipefail - BASE="https://b.stripecdn.com/api-artifacts/assets/openapi/${VERSION}/private_preview" + BASE="https://b.stripecdn.com/api-artifacts/assets/openapi/${VERSION}/${CDN_DIR}" # Required specs — fail the job if absent. - curl -fsSL "$BASE/spec3.private_preview.sdk.yaml" -o openapi/spec3.private_preview.sdk.yaml - curl -fsSL "$BASE/spec3.private_preview.sdk.json" -o openapi/spec3.private_preview.sdk.json + for f in $SPEC_FILES; do + curl -fsSL "$BASE/$f" -o "openapi/$f" + done # Optional fixtures — skip cleanly if the CDN returns 404. - for f in fixtures3.private_preview.yaml fixtures3.private_preview.json; do + for f in $FIXTURE_FILES; do if ! curl -fsSL "$BASE/$f" -o "openapi/$f"; then echo "Optional file $f absent from CDN; skipping." rm -f "openapi/$f" @@ -82,6 +126,7 @@ jobs: - name: Commit specs, generate diffs, and cut release env: + MODE: ${{ inputs.mode }} DRY_RUN: ${{ inputs.dry_run }} run: | set -euo pipefail @@ -89,28 +134,26 @@ jobs: git config user.name "Stripe OpenAPI" git config user.email "105521251+stripe-openapi[bot]@users.noreply.github.com" - # In a dry run, local commits are pushed here for inspection instead of master. - TEST_BRANCH="dry-run/private-preview" - # --- Commit fixtures (if changed) --- - for f in openapi/fixtures3.private_preview.yaml openapi/fixtures3.private_preview.json; do - if [ -f "$f" ]; then git add "$f"; fi + for f in $FIXTURE_FILES; do + if [ -f "openapi/$f" ]; then git add "openapi/$f"; fi done if [ -n "$(git diff --name-only --staged)" ]; then - git commit -m "Update fixture data for private preview" + git commit -m "$FIXTURE_MSG" fi # --- Commit spec (if changed); only cut a release when the spec moved --- - git add ./openapi/spec3.private_preview.sdk.yaml ./openapi/spec3.private_preview.sdk.json + for f in $SPEC_FILES; do + git add "openapi/$f" + done if [ -n "$(git diff --name-only --staged)" ]; then - git commit -m "Update OpenAPI specification for private preview" + git commit -m "$SPEC_MSG" LAST_TAG=$(git describe --tags --abbrev=0) NEW_TAG="v$(( ${LAST_TAG#v} + 1 ))" echo "Previous tag ${LAST_TAG}; new tag ${NEW_TAG}" - # Materialize the prior spec from the last release tag. Mirrors publish.sh, - # which diffs the GA spec (openapi/spec3.sdk.yaml) — old tag vs. working tree. + # Diff the GA spec (old tag vs. working tree), exactly as publish.sh does. SPEC_PATH="openapi/spec3.sdk.yaml" OLD_SPEC="/tmp/oldspec.sdk.yaml" git show "${LAST_TAG}:${SPEC_PATH}" > "$OLD_SPEC" @@ -139,18 +182,17 @@ jobs: git commit -m "Update upcoming changes" fi - # Private-preview releases don't surface a full diff in the release notes. if [ "$DRY_RUN" = "true" ]; then # Never create/push a real tag in a dry run — a stray vN tag would corrupt # the next real run's `git describe` increment. echo "Dry run: would create tag ${NEW_TAG} and push master." else - git tag "$NEW_TAG" -m "This release only includes changes to the private preview spec." + git tag "$NEW_TAG" -m "$TAG_MSG" # Push branch and tag atomically so we never leave a dangling tag. git push --atomic origin master "$NEW_TAG" fi else - echo "No private-preview spec changes; skipping release." + echo "No spec changes for mode '${MODE}'; skipping release." fi if [ "$DRY_RUN" = "true" ]; then