Skip to content
Open
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
207 changes: 207 additions & 0 deletions .github/workflows/publish-v1-specs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
name: Publish v1 Specs

# 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.
#
# 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.

on:
workflow_dispatch:
inputs:
version:
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is uppercase but the rest are lowercase

- all
dry_run:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is fine, but should we invert it? Like, push_changes or something? dry_run: false is sort of a double negative, so it's a pet peeve of mine (especially if true is the default)

description: 'Dry run: fetch, build, and diff but do not push to master or tag'
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This (and all other actions) should be using the new action and pinned hashes from #194

I'm actually surprised there wasn't a new CI job that failed on this branch because you're not pinning a version. so maybe double check why it didn't yell at you 😅

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 <tag>:...` 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this need to pin a specific codegen version?

path: sdk-codegen

- name: Setup just
uses: extractions/setup-just@v2

- name: Setup node
uses: actions/setup-node@v4
with:
node-version: '24'

- 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}/${CDN_DIR}"

# Required specs — fail the job if absent.
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 $FIXTURE_FILES; 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:
MODE: ${{ inputs.mode }}
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 $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 "$FIXTURE_MSG"
fi

# --- Commit spec (if changed); only cut a release when the spec moved ---
for f in $SPEC_FILES; do
git add "openapi/$f"
done
if [ -n "$(git diff --name-only --staged)" ]; then
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}"

# 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"

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"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I would use just --no-deps diff ... instead of reaching into main.js directly

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

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 "$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 spec changes for mode '${MODE}'; skipping release."
fi

if [ "$DRY_RUN" = "true" ]; then
# 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