diff --git a/.github/workflows/publish-v1-specs.yaml b/.github/workflows/publish-v1-specs.yaml new file mode 100644 index 000000000..c9b4eee29 --- /dev/null +++ b/.github/workflows/publish-v1-specs.yaml @@ -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 + - all + dry_run: + 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 + 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: 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" + 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