From 4d66c2d78f45c123dc7e413f2d36b8e565db2165 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 5 Nov 2026 04:50:25 -0800 Subject: [PATCH 1/6] Deploy previews with CLI tools and isolated OIDC Preview deployment should not depend on the stale Azure action release or a deployment token stored in GitHub. An isolated preview identity keeps deployment access separate from package-signing permissions. Artifact-provided CLI configuration must not be able to redirect uploads to privileged runner files. --- .github/workflows/close-preview.yml | 43 +++++++++++++++-- .github/workflows/deploy-preview.yml | 69 ++++++++++++++++++++++++---- 2 files changed, 97 insertions(+), 15 deletions(-) diff --git a/.github/workflows/close-preview.yml b/.github/workflows/close-preview.yml index 4d1ed526512b..1ffa6968dcca 100644 --- a/.github/workflows/close-preview.yml +++ b/.github/workflows/close-preview.yml @@ -9,15 +9,48 @@ on: pull_request_target: types: [closed] -permissions: {} +permissions: + id-token: write jobs: close: runs-on: ubuntu-latest if: github.repository == 'microsoft/TypeScript-Website' + environment: + name: azure-preview + deployment: false + env: + AZURE_STATIC_WEB_APP_NAME: typescriptlang-org-preview + AZURE_RESOURCE_GROUP: typescriptlang-org-preview steps: - - uses: Azure/static-web-apps-deploy@1a947af9992250f3bc2e68ad0754c0b0c11566c9 # v1 + - name: Log in to Azure + uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3.1.0 with: - azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_PREVIEW }} - action: close - app_location: /dev/null + client-id: ${{ vars.AZURE_CLIENT_ID }} + tenant-id: ${{ vars.AZURE_TENANT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + + - name: Close preview environment + env: + PR_NUMBER: ${{ inputs.pr || github.event.pull_request.number }} + run: | + set -euo pipefail + if [[ ! "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then + echo "::error::Invalid preview PR number" + exit 1 + fi + + environment="$(az staticwebapp environment list \ + --name "$AZURE_STATIC_WEB_APP_NAME" \ + --resource-group "$AZURE_RESOURCE_GROUP" \ + --query "[?name=='$PR_NUMBER'].name" --output tsv)" + if [[ -z "$environment" ]]; then + echo "No preview environment exists for PR $PR_NUMBER" + exit 0 + fi + + az staticwebapp environment delete \ + --name "$AZURE_STATIC_WEB_APP_NAME" \ + --resource-group "$AZURE_RESOURCE_GROUP" \ + --environment-name "$PR_NUMBER" \ + --yes diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml index c4e5a5168691..f2fc1baf685f 100644 --- a/.github/workflows/deploy-preview.yml +++ b/.github/workflows/deploy-preview.yml @@ -16,11 +16,18 @@ permissions: contents: read pull-requests: write actions: read + id-token: write jobs: deploy: runs-on: ubuntu-latest if: github.repository == 'microsoft/TypeScript-Website' + environment: + name: azure-preview + deployment: false + env: + AZURE_STATIC_WEB_APP_NAME: typescriptlang-org-preview + AZURE_RESOURCE_GROUP: typescriptlang-org-preview steps: - name: Get PR/workflow run info id: get-info @@ -139,19 +146,61 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ fromJson(steps.get-info.outputs.result).runId }} + - name: Set up Node.js + if: ${{ steps.get-info.outputs.result != 'null' }} + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: "22.x" + + - name: Log in to Azure + if: ${{ steps.get-info.outputs.result != 'null' }} + uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3.1.0 + with: + client-id: ${{ vars.AZURE_CLIENT_ID }} + tenant-id: ${{ vars.AZURE_TENANT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + - name: Deploy id: deploy if: ${{ steps.get-info.outputs.result != 'null' }} - uses: Azure/static-web-apps-deploy@1a947af9992250f3bc2e68ad0754c0b0c11566c9 # v1 - with: - azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_PREVIEW }} - # Unsetting the repo token so we can control commenting ourselves. - # repo_token: ${{ secrets.GITHUB_TOKEN }} - action: "upload" - app_location: "site" - skip_app_build: true - production_branch: v2 - deployment_environment: ${{ fromJson(steps.get-info.outputs.result).pr }} + env: + PR_NUMBER: ${{ fromJson(steps.get-info.outputs.result).pr }} + run: | + set -euo pipefail + if [[ ! "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then + echo "::error::Invalid preview PR number" + exit 1 + fi + + deployment_token="$(az staticwebapp secrets list \ + --name "$AZURE_STATIC_WEB_APP_NAME" \ + --resource-group "$AZURE_RESOURCE_GROUP" \ + --query properties.apiKey --output tsv)" + if [[ -z "$deployment_token" ]]; then + echo "::error::Azure returned no preview deployment token" + exit 1 + fi + echo "::add-mask::$deployment_token" + # Keep CLI configuration outside the untrusted artifact. + printf '{"configurations":{"preview":{}}}\n' > "$RUNNER_TEMP/swa-cli.config.json" + SWA_CLI_DEPLOYMENT_TOKEN="$deployment_token" \ + npx --yes --package=@azure/static-web-apps-cli@2.1.0 swa \ + --config "$RUNNER_TEMP/swa-cli.config.json" deploy \ + --app-location "$GITHUB_WORKSPACE" \ + --output-location "$GITHUB_WORKSPACE/site" \ + --swa-config-location "$GITHUB_WORKSPACE/site" \ + --env "$PR_NUMBER" + + hostname="$(az staticwebapp environment show \ + --name "$AZURE_STATIC_WEB_APP_NAME" \ + --resource-group "$AZURE_RESOURCE_GROUP" \ + --environment-name "$PR_NUMBER" \ + --query hostname --output tsv)" + if [[ ! "$hostname" =~ ^[A-Za-z0-9.-]+\.azurestaticapps\.net$ ]]; then + echo "::error::Azure returned an invalid hostname for preview $PR_NUMBER" + exit 1 + fi + printf 'static_web_app_url=https://%s\n' "$hostname" >> "$GITHUB_OUTPUT" - name: Comment on PR if: ${{ steps.get-info.outputs.result != 'null' }} From 068733faae49ac86c51fe4a7a0d561d759608410 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 5 Nov 2026 04:50:25 -0800 Subject: [PATCH 2/6] Call deployed sites previews in bot comments These deployments are PR previews, not staging sites. Keep recognizing the old wording so redeployments do not leave duplicate bot comments. --- .github/workflows/deploy-preview.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml index f2fc1baf685f..11078dbb7ad4 100644 --- a/.github/workflows/deploy-preview.yml +++ b/.github/workflows/deploy-preview.yml @@ -211,7 +211,8 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const prefix = "Azure Static Web Apps: Your stage site is ready!"; + const prefix = "Azure Static Web Apps: Your preview site is ready!"; + const legacyPrefix = "Azure Static Web Apps: Your stage site is ready!"; const comments = await github.paginate(github.rest.issues.listComments, { owner: context.repo.owner, repo: context.repo.repo, @@ -220,7 +221,8 @@ jobs: }); for (const comment of comments) { - if (comment.user?.login === "github-actions[bot]" && comment.body?.startsWith(prefix)) { + if (comment.user?.login === "github-actions[bot]" && + (comment.body?.startsWith(prefix) || comment.body?.startsWith(legacyPrefix))) { console.log(`Deleting comment ${comment.id}`); await github.rest.issues.deleteComment({ owner: context.repo.owner, From 83ab44361199c37fd392b209773c1af3246765a8 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 5 Nov 2026 04:53:26 -0800 Subject: [PATCH 3/6] Configure preview resources through environment variables Keep Azure resource selection with the isolated environment's identity settings instead of hardcoding it in both workflows. Missing settings must fail before deployment or cleanup. --- .github/workflows/close-preview.yml | 6 ++++-- .github/workflows/deploy-preview.yml | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/close-preview.yml b/.github/workflows/close-preview.yml index 1ffa6968dcca..627d5e7f6d2b 100644 --- a/.github/workflows/close-preview.yml +++ b/.github/workflows/close-preview.yml @@ -20,8 +20,8 @@ jobs: name: azure-preview deployment: false env: - AZURE_STATIC_WEB_APP_NAME: typescriptlang-org-preview - AZURE_RESOURCE_GROUP: typescriptlang-org-preview + AZURE_STATIC_WEB_APP_NAME: ${{ vars.AZURE_STATIC_WEB_APP_NAME }} + AZURE_RESOURCE_GROUP: ${{ vars.AZURE_RESOURCE_GROUP }} steps: - name: Log in to Azure uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3.1.0 @@ -35,6 +35,8 @@ jobs: PR_NUMBER: ${{ inputs.pr || github.event.pull_request.number }} run: | set -euo pipefail + : "${AZURE_STATIC_WEB_APP_NAME:?Missing AZURE_STATIC_WEB_APP_NAME environment variable}" + : "${AZURE_RESOURCE_GROUP:?Missing AZURE_RESOURCE_GROUP environment variable}" if [[ ! "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then echo "::error::Invalid preview PR number" exit 1 diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml index 11078dbb7ad4..6ee844f8a2a9 100644 --- a/.github/workflows/deploy-preview.yml +++ b/.github/workflows/deploy-preview.yml @@ -26,8 +26,8 @@ jobs: name: azure-preview deployment: false env: - AZURE_STATIC_WEB_APP_NAME: typescriptlang-org-preview - AZURE_RESOURCE_GROUP: typescriptlang-org-preview + AZURE_STATIC_WEB_APP_NAME: ${{ vars.AZURE_STATIC_WEB_APP_NAME }} + AZURE_RESOURCE_GROUP: ${{ vars.AZURE_RESOURCE_GROUP }} steps: - name: Get PR/workflow run info id: get-info @@ -167,6 +167,8 @@ jobs: PR_NUMBER: ${{ fromJson(steps.get-info.outputs.result).pr }} run: | set -euo pipefail + : "${AZURE_STATIC_WEB_APP_NAME:?Missing AZURE_STATIC_WEB_APP_NAME environment variable}" + : "${AZURE_RESOURCE_GROUP:?Missing AZURE_RESOURCE_GROUP environment variable}" if [[ ! "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then echo "::error::Invalid preview PR number" exit 1 From 4de2947f8dd63d7777448bced660b69b51567629 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 5 Nov 2026 05:19:09 -0800 Subject: [PATCH 4/6] Keep preview cleanup polling within the app resource Azure's delete command polls a subscription-scoped operation endpoint that the preview identity cannot read. Confirm deletion through the resource-scoped environment list without broadening its permissions. --- .github/workflows/close-preview.yml | 33 +++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/.github/workflows/close-preview.yml b/.github/workflows/close-preview.yml index 627d5e7f6d2b..a1db6432c5f7 100644 --- a/.github/workflows/close-preview.yml +++ b/.github/workflows/close-preview.yml @@ -42,17 +42,32 @@ jobs: exit 1 fi - environment="$(az staticwebapp environment list \ - --name "$AZURE_STATIC_WEB_APP_NAME" \ - --resource-group "$AZURE_RESOURCE_GROUP" \ - --query "[?name=='$PR_NUMBER'].name" --output tsv)" + get_preview_environment() { + az staticwebapp environment list \ + --name "$AZURE_STATIC_WEB_APP_NAME" \ + --resource-group "$AZURE_RESOURCE_GROUP" \ + --query "[?name=='$PR_NUMBER'].name" --output tsv + } + + environment="$(get_preview_environment)" if [[ -z "$environment" ]]; then echo "No preview environment exists for PR $PR_NUMBER" exit 0 fi - az staticwebapp environment delete \ - --name "$AZURE_STATIC_WEB_APP_NAME" \ - --resource-group "$AZURE_RESOURCE_GROUP" \ - --environment-name "$PR_NUMBER" \ - --yes + # Avoid Azure CLI's subscription-scoped long-running-operation polling. + resource_id="/subscriptions/{subscriptionId}/resourceGroups/$AZURE_RESOURCE_GROUP" + resource_id+="/providers/Microsoft.Web/staticSites/$AZURE_STATIC_WEB_APP_NAME" + az rest --method delete --url "$resource_id/builds/$PR_NUMBER?api-version=2024-11-01" + + for _ in {1..30}; do + environment="$(get_preview_environment)" + if [[ -z "$environment" ]]; then + echo "Deleted preview environment for PR $PR_NUMBER" + exit 0 + fi + sleep 5 + done + + echo "::error::Timed out waiting for preview $PR_NUMBER to be deleted" + exit 1 From 9d9a2ec201d5950dbf5235ec6d8c141addeabb46 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 5 Nov 2026 05:19:09 -0800 Subject: [PATCH 5/6] Use only the root preview runtime configuration The SWA CLI searches configuration directories recursively, so a nested fixture can override the intended root configuration. Isolate that root file before passing its directory to the deployment client. --- .github/workflows/deploy-preview.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml index 6ee844f8a2a9..0af6e5e2073a 100644 --- a/.github/workflows/deploy-preview.yml +++ b/.github/workflows/deploy-preview.yml @@ -185,12 +185,14 @@ jobs: echo "::add-mask::$deployment_token" # Keep CLI configuration outside the untrusted artifact. printf '{"configurations":{"preview":{}}}\n' > "$RUNNER_TEMP/swa-cli.config.json" + runtime_config_dir="$(mktemp -d "$RUNNER_TEMP/swa-runtime-config.XXXXXX")" + cp "$GITHUB_WORKSPACE/site/staticwebapp.config.json" "$runtime_config_dir/" SWA_CLI_DEPLOYMENT_TOKEN="$deployment_token" \ npx --yes --package=@azure/static-web-apps-cli@2.1.0 swa \ --config "$RUNNER_TEMP/swa-cli.config.json" deploy \ --app-location "$GITHUB_WORKSPACE" \ --output-location "$GITHUB_WORKSPACE/site" \ - --swa-config-location "$GITHUB_WORKSPACE/site" \ + --swa-config-location "$runtime_config_dir" \ --env "$PR_NUMBER" hostname="$(az staticwebapp environment show \ From ec32acdcd3d941feb18befcab47658a015374a0a Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 5 Nov 2026 09:08:30 -0800 Subject: [PATCH 6/6] Allow preview artifacts without runtime configuration Normal Gatsby builds do not include staticwebapp.config.json. Treat its absence as an empty configuration while keeping nested artifact files out of runtime configuration discovery. --- .github/workflows/deploy-preview.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml index 0af6e5e2073a..c6835228d9b0 100644 --- a/.github/workflows/deploy-preview.yml +++ b/.github/workflows/deploy-preview.yml @@ -186,7 +186,12 @@ jobs: # Keep CLI configuration outside the untrusted artifact. printf '{"configurations":{"preview":{}}}\n' > "$RUNNER_TEMP/swa-cli.config.json" runtime_config_dir="$(mktemp -d "$RUNNER_TEMP/swa-runtime-config.XXXXXX")" - cp "$GITHUB_WORKSPACE/site/staticwebapp.config.json" "$runtime_config_dir/" + if [[ -e "$GITHUB_WORKSPACE/site/staticwebapp.config.json" ]]; then + cp "$GITHUB_WORKSPACE/site/staticwebapp.config.json" "$runtime_config_dir/" + else + echo "No root Static Web Apps config; using an empty runtime configuration" + printf '{}\n' > "$runtime_config_dir/staticwebapp.config.json" + fi SWA_CLI_DEPLOYMENT_TOKEN="$deployment_token" \ npx --yes --package=@azure/static-web-apps-cli@2.1.0 swa \ --config "$RUNNER_TEMP/swa-cli.config.json" deploy \