diff --git a/.github/workflows/publish-v1-specs.yaml b/.github/workflows/publish-v1-specs.yaml new file mode 100644 index 000000000..3ce23483a --- /dev/null +++ b/.github/workflows/publish-v1-specs.yaml @@ -0,0 +1,172 @@ +name: Publish v1 Private Preview Specs + +# Publishes v1 private-preview OpenAPI specs for a given API version and cuts a +# release, mirroring the `beta` path of pay-server's api-codegen/tools/publish.sh. +# Specs are pulled from the Statics Commander CDN instead of a local build dir. +# +# Triggered by the zoolander LockAndReleasePrivatePreviewSpecs workflow. + +on: + workflow_dispatch: + inputs: + version: + description: 'Version name (e.g., 2026-07-08.preview)' + required: true + type: string + publish_release: + description: 'Publish to master and create a tag; otherwise push to dry-run/private-preview' + required: false + type: boolean + default: false + +permissions: {} + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: write + env: + CDN_DIR: private_preview + SPEC_FILES: spec3.private_preview.sdk.yaml spec3.private_preview.sdk.json + FIXTURE_FILES: fixtures3.private_preview.yaml fixtures3.private_preview.json + FIXTURE_MSG: Update fixture data for private preview + SPEC_MSG: Update OpenAPI specification for private preview + TAG_MSG: This release only includes changes to the private preview spec. + TEST_BRANCH: dry-run/private-preview + steps: + - name: Fetch app installation token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + id: gh-api-token + with: + app-id: ${{ secrets.GH_APP_STRIPE_OPENAPI_APP_ID }} + private-key: ${{ secrets.GH_APP_STRIPE_OPENAPI_PRIVATE_KEY }} + # The token checks out this repository and pushes the published specs + # back to it. Scope defaults to the current repository. + permission-contents: write + + - name: Checkout openapi + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + 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 + # The final step uses plain `git push`, so retain the narrowly scoped + # App token. This workflow uploads no artifacts that could contain it. + + - name: Checkout sdk-codegen (provides the diff CLI) + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: stripe/sdk-codegen + ref: 1b117c3457c094060636cdfc7936e30a86b2e3ca # v17.0.0 + path: sdk-codegen + persist-credentials: false + + - name: Setup just + uses: extractions/setup-just@dd310ad5a97d8e7b41793f8ef055398d51ad4de6 # v2 + + - name: Setup node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '24' + + - 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: + PUBLISH_RELEASE: ${{ inputs.publish_release }} + 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. `diff` delegates entirely to `main`, + # so call `main` without its build dependency for each generated diff. + cd "${GITHUB_WORKSPACE}/sdk-codegen" + just build-tools + + # `rest.md` is the language-agnostic diff (no --lang), matching publish.sh. + just --no-deps main 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 + just --no-deps main 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 [ "$PUBLISH_RELEASE" != "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 private-preview spec changes; skipping release." + fi + + if [ "$PUBLISH_RELEASE" != "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