From 8e22b81a3c6771162d915ac74bae3f69763aa581 Mon Sep 17 00:00:00 2001 From: chrisjwalk-bot <268224883+chrisjwalk-bot@users.noreply.github.com> Date: Sun, 15 Mar 2026 13:55:27 -0400 Subject: [PATCH 1/3] feat: add PR preview deployments via Azure Static Web Apps Closes #64 - Add apiBaseUrlInterceptor: prepends apiBaseUrl to /api/* requests, allowing the Angular app to call the production backend from a different origin in preview builds - Add environment files: environment.ts (empty = same-origin, used in dev and prod) and environment.preview.ts (production App Service URL) - Add preview build configuration in project.json with fileReplacements to swap in environment.preview.ts - Add staticwebapp.config.json for SPA fallback routing in Azure SWA - Add CORS policy in Program.cs allowing *.azurestaticapps.net origins - Add preview.yml workflow: builds Angular with the preview config, deploys to Azure SWA on PR open/sync, closes staging env on PR close Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/preview.yml | 56 +++++++++++++++++++ apps/api/Api/Program.cs | 13 +++++ apps/web-app/project.json | 9 +++ .../src/app/api-base-url.interceptor.ts | 10 ++++ apps/web-app/src/app/app.config.ts | 6 +- .../src/environments/environment.preview.ts | 3 + apps/web-app/src/environments/environment.ts | 3 + apps/web-app/src/staticwebapp.config.json | 6 ++ 8 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/preview.yml create mode 100644 apps/web-app/src/app/api-base-url.interceptor.ts create mode 100644 apps/web-app/src/environments/environment.preview.ts create mode 100644 apps/web-app/src/environments/environment.ts create mode 100644 apps/web-app/src/staticwebapp.config.json diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml new file mode 100644 index 00000000..cb2ad7c9 --- /dev/null +++ b/.github/workflows/preview.yml @@ -0,0 +1,56 @@ +name: Preview + +on: + pull_request: + types: [opened, synchronize, reopened, closed] + branches: [main] + +concurrency: + group: preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + deploy_preview: + if: github.event.action != 'closed' + name: Deploy Preview + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build web app (preview) + run: pnpm nx build web-app --configuration preview + + - name: Copy SWA routing config + run: cp apps/web-app/src/staticwebapp.config.json dist/apps/web-app/browser/ + + - name: Deploy to Azure Static Web Apps + uses: Azure/static-web-apps-deploy@v1 + with: + azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }} + repo_token: ${{ secrets.GITHUB_TOKEN }} + action: upload + app_location: dist/apps/web-app/browser + skip_app_build: true + + close_preview: + if: github.event.action == 'closed' + name: Close Preview + runs-on: ubuntu-latest + steps: + - name: Close staging environment + uses: Azure/static-web-apps-deploy@v1 + with: + azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }} + action: close diff --git a/apps/api/Api/Program.cs b/apps/api/Api/Program.cs index 6e3b0529..17b0b3f3 100644 --- a/apps/api/Api/Program.cs +++ b/apps/api/Api/Program.cs @@ -41,6 +41,17 @@ builder.Services.AddControllers(); +builder.Services.AddCors(options => + options.AddPolicy("SwaPreview", policy => + policy + .SetIsOriginAllowed(origin => + Uri.TryCreate(origin, UriKind.Absolute, out var uri) + && uri.Host.EndsWith(".azurestaticapps.net")) + .AllowAnyMethod() + .AllowAnyHeader() + ) +); + var app = builder.Build(); if (app.Environment.IsDevelopment()) @@ -85,6 +96,8 @@ app.UseRouting(); +app.UseCors("SwaPreview"); + app.UseAuthorization(); app.MapControllers(); diff --git a/apps/web-app/project.json b/apps/web-app/project.json index 4a79290f..b12854e1 100644 --- a/apps/web-app/project.json +++ b/apps/web-app/project.json @@ -50,6 +50,15 @@ "extractLicenses": false, "sourceMap": true, "namedChunks": true + }, + "preview": { + "fileReplacements": [ + { + "replace": "apps/web-app/src/environments/environment.ts", + "with": "apps/web-app/src/environments/environment.preview.ts" + } + ], + "outputHashing": "all" } }, "defaultConfiguration": "production" diff --git a/apps/web-app/src/app/api-base-url.interceptor.ts b/apps/web-app/src/app/api-base-url.interceptor.ts new file mode 100644 index 00000000..93620961 --- /dev/null +++ b/apps/web-app/src/app/api-base-url.interceptor.ts @@ -0,0 +1,10 @@ +import { HttpInterceptorFn } from '@angular/common/http'; + +import { environment } from '../environments/environment'; + +export const apiBaseUrlInterceptor: HttpInterceptorFn = (req, next) => { + if (!environment.apiBaseUrl || !req.url.startsWith('/api')) { + return next(req); + } + return next(req.clone({ url: environment.apiBaseUrl + req.url })); +}; diff --git a/apps/web-app/src/app/app.config.ts b/apps/web-app/src/app/app.config.ts index 1260909b..6c79d487 100644 --- a/apps/web-app/src/app/app.config.ts +++ b/apps/web-app/src/app/app.config.ts @@ -19,12 +19,16 @@ import { import { provideServiceWorker } from '@angular/service-worker'; import { authInterceptor } from '@myorg/auth'; +import { apiBaseUrlInterceptor } from './api-base-url.interceptor'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [ provideZonelessChangeDetection(), - provideHttpClient(withFetch(), withInterceptors([authInterceptor])), + provideHttpClient( + withFetch(), + withInterceptors([apiBaseUrlInterceptor, authInterceptor]), + ), provideRouter( routes, withComponentInputBinding(), diff --git a/apps/web-app/src/environments/environment.preview.ts b/apps/web-app/src/environments/environment.preview.ts new file mode 100644 index 00000000..7fc918e7 --- /dev/null +++ b/apps/web-app/src/environments/environment.preview.ts @@ -0,0 +1,3 @@ +export const environment = { + apiBaseUrl: 'https://angularclinetcorengrxstarter.azurewebsites.net', +}; diff --git a/apps/web-app/src/environments/environment.ts b/apps/web-app/src/environments/environment.ts new file mode 100644 index 00000000..81af9b33 --- /dev/null +++ b/apps/web-app/src/environments/environment.ts @@ -0,0 +1,3 @@ +export const environment = { + apiBaseUrl: '', +}; diff --git a/apps/web-app/src/staticwebapp.config.json b/apps/web-app/src/staticwebapp.config.json new file mode 100644 index 00000000..a6c11734 --- /dev/null +++ b/apps/web-app/src/staticwebapp.config.json @@ -0,0 +1,6 @@ +{ + "navigationFallback": { + "rewrite": "/index.html", + "exclude": ["/*.{css,js,png,gif,ico,jpg,svg,webmanifest,woff,woff2,txt}"] + } +} From 090a4d19908ded7b459fcf6c3f2d14ffa66087da Mon Sep 17 00:00:00 2001 From: chrisjwalk-bot <268224883+chrisjwalk-bot@users.noreply.github.com> Date: Sun, 15 Mar 2026 13:59:55 -0400 Subject: [PATCH 2/3] feat(ci): post preview URL as PR comment after SWA deploy Captures static_web_app_url output from the deploy step and posts it as a comment on the PR. Updates the existing comment on re-deploys rather than adding a new one each time. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/preview.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index cb2ad7c9..68ab81b1 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -9,6 +9,9 @@ concurrency: group: preview-${{ github.event.pull_request.number }} cancel-in-progress: true +permissions: + pull-requests: write + jobs: deploy_preview: if: github.event.action != 'closed' @@ -36,6 +39,7 @@ jobs: run: cp apps/web-app/src/staticwebapp.config.json dist/apps/web-app/browser/ - name: Deploy to Azure Static Web Apps + id: deploy uses: Azure/static-web-apps-deploy@v1 with: azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }} @@ -44,6 +48,35 @@ jobs: app_location: dist/apps/web-app/browser skip_app_build: true + - name: Comment preview URL on PR + uses: actions/github-script@v7 + with: + script: | + const url = '${{ steps.deploy.outputs.static_web_app_url }}'; + const marker = ''; + const body = `${marker}\nšŸ” **Preview deployment:** ${url}`; + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } + close_preview: if: github.event.action == 'closed' name: Close Preview From eb9ca6e367be0dc8a910db39f305f7f1a5c57edf Mon Sep 17 00:00:00 2001 From: chrisjwalk-bot <268224883+chrisjwalk-bot@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:03:04 -0400 Subject: [PATCH 3/3] fix(ci): remove duplicate preview URL comment step Azure/static-web-apps-deploy already posts a PR comment when repo_token is provided. Remove the redundant actions/github-script step. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/preview.yml | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 68ab81b1..8f9fa708 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -9,9 +9,6 @@ concurrency: group: preview-${{ github.event.pull_request.number }} cancel-in-progress: true -permissions: - pull-requests: write - jobs: deploy_preview: if: github.event.action != 'closed' @@ -48,35 +45,6 @@ jobs: app_location: dist/apps/web-app/browser skip_app_build: true - - name: Comment preview URL on PR - uses: actions/github-script@v7 - with: - script: | - const url = '${{ steps.deploy.outputs.static_web_app_url }}'; - const marker = ''; - const body = `${marker}\nšŸ” **Preview deployment:** ${url}`; - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - const existing = comments.find(c => c.body.includes(marker)); - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body, - }); - } - close_preview: if: github.event.action == 'closed' name: Close Preview