From 08eb5a1a32b2703982c77187a7b3c6577960bf4c Mon Sep 17 00:00:00 2001 From: Daniel van der Ploeg Date: Thu, 14 Aug 2025 17:06:56 +0930 Subject: [PATCH 1/5] DO-1743: add comprehensive PWA deployment workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Support S3 + CloudFront deployment with dual cache strategies - Branch-based preview environments with PR URLs - Multi-brand parallel deployment support - CloudFront invalidation with configurable paths - Node.js 16-22 support with Yarn/npm package managers - Environment-specific configurations (staging/production/preview) - Manual production deployment gates - Artifact caching and comprehensive cleanup - Extensive documentation with usage examples ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .github/workflows/pwa-deployment.yml | 492 +++++++++++++++++++++++++++ README.md | 127 +++++++ 2 files changed, 619 insertions(+) create mode 100644 .github/workflows/pwa-deployment.yml diff --git a/.github/workflows/pwa-deployment.yml b/.github/workflows/pwa-deployment.yml new file mode 100644 index 0000000..fd18fb0 --- /dev/null +++ b/.github/workflows/pwa-deployment.yml @@ -0,0 +1,492 @@ +name: ๐Ÿ“ฑ PWA Deployment + +on: + workflow_call: + inputs: + # AWS Configuration + aws-region: + description: "AWS region for deployment" + type: string + required: false + default: "ap-southeast-2" + s3-bucket: + description: "S3 bucket name for deployment" + type: string + required: true + cloudfront-distribution-id: + description: "CloudFront distribution ID for cache invalidation" + type: string + required: true + + # Environment Configuration + environment: + description: "Deployment environment (staging/production/preview)" + type: string + required: false + default: "staging" + + # Build Configuration + package-manager: + description: "Node package manager (yarn/npm)" + type: string + required: false + default: "yarn" + is-yarn-classic: + description: "Use Yarn Classic (pre-Berry) instead of modern Yarn" + type: boolean + required: false + default: false + build-command: + description: "Build command to execute" + type: string + required: false + default: "build" + build-directory: + description: "Directory containing built assets to deploy" + type: string + required: false + default: "dist" + + # Cache Strategy Configuration + cache-strategy: + description: "Cache strategy for assets (immutable/no-cache)" + type: string + required: false + default: "immutable" + + # Preview Environment Configuration + preview-mode: + description: "Enable preview mode for PR-based deployments" + type: boolean + required: false + default: false + preview-base-url: + description: "Base URL for preview deployments" + type: string + required: false + default: "" + + # Multi-brand Configuration + brand-config: + description: "JSON configuration for multi-brand deployments" + type: string + required: false + default: "" + + # Advanced Configuration + cloudfront-invalidation-paths: + description: "CloudFront invalidation paths (JSON array)" + type: string + required: false + default: '["/*"]' + extra-sync-args: + description: "Additional AWS S3 sync arguments" + type: string + required: false + default: "" + + # Debug and Control + debug: + description: "Enable verbose logging and debug output" + type: boolean + required: false + default: false + skip-build: + description: "Skip the build step (use pre-built assets)" + type: boolean + required: false + default: false + skip-tests: + description: "Skip test execution" + type: boolean + required: false + default: false + + secrets: + aws-access-key-id: + description: "AWS access key ID" + required: true + aws-secret-access-key: + description: "AWS secret access key" + required: true + cloudfront-signing-key: + description: "CloudFront signing key (optional, for signed URLs)" + required: false + + outputs: + deployment-url: + description: "URL of the deployed application" + value: ${{ jobs.deploy.outputs.deployment-url }} + preview-url: + description: "Preview URL for PR deployments" + value: ${{ jobs.deploy.outputs.preview-url }} + +jobs: + # Validate inputs and prepare deployment configuration + prepare: + name: ๐Ÿ” Prepare Deployment + runs-on: ubuntu-latest + outputs: + cache-control-static: ${{ steps.cache-config.outputs.cache-control-static }} + cache-control-html: ${{ steps.cache-config.outputs.cache-control-html }} + s3-prefix: ${{ steps.deployment-config.outputs.s3-prefix }} + deployment-url: ${{ steps.deployment-config.outputs.deployment-url }} + brand-matrix: ${{ steps.brand-config.outputs.matrix }} + invalidation-paths: ${{ steps.cache-config.outputs.invalidation-paths }} + steps: + - name: Validate required inputs + run: | + if [ -z "${{ inputs.s3-bucket }}" ]; then + echo "โŒ Error: s3-bucket is required" + exit 1 + fi + + if [ -z "${{ inputs.cloudfront-distribution-id }}" ]; then + echo "โŒ Error: cloudfront-distribution-id is required" + exit 1 + fi + + if [ "${{ inputs.environment }}" != "staging" ] && [ "${{ inputs.environment }}" != "production" ] && [ "${{ inputs.environment }}" != "preview" ]; then + echo "โŒ Error: environment must be one of: staging, production, preview" + exit 1 + fi + + if [ "${{ inputs.cache-strategy }}" != "immutable" ] && [ "${{ inputs.cache-strategy }}" != "no-cache" ]; then + echo "โŒ Error: cache-strategy must be one of: immutable, no-cache" + exit 1 + fi + + echo "โœ… All required inputs validated" + + - name: Configure cache strategy + id: cache-config + run: | + case "${{ inputs.cache-strategy }}" in + "immutable") + # Static assets with content hashing - cache for 1 year + echo "cache-control-static=public, max-age=31536000, immutable" >> $GITHUB_OUTPUT + # HTML files - cache for 1 hour with revalidation + echo "cache-control-html=public, max-age=3600, must-revalidate" >> $GITHUB_OUTPUT + ;; + "no-cache") + # Force revalidation for all assets + echo "cache-control-static=no-cache, no-store, must-revalidate" >> $GITHUB_OUTPUT + echo "cache-control-html=no-cache, no-store, must-revalidate" >> $GITHUB_OUTPUT + ;; + esac + + # Prepare invalidation paths + echo "invalidation-paths=${{ inputs.cloudfront-invalidation-paths }}" >> $GITHUB_OUTPUT + + if [ "${{ inputs.debug }}" = "true" ]; then + echo "๐Ÿ” Cache configuration:" + echo " Strategy: ${{ inputs.cache-strategy }}" + echo " Static assets: $(cat $GITHUB_OUTPUT | grep cache-control-static | cut -d'=' -f2-)" + echo " HTML files: $(cat $GITHUB_OUTPUT | grep cache-control-html | cut -d'=' -f2-)" + fi + + - name: Configure deployment paths + id: deployment-config + run: | + if [ "${{ inputs.preview-mode }}" = "true" ]; then + # Preview deployment uses PR number or branch name + if [ -n "${{ github.event.pull_request.number }}" ]; then + PREFIX="pr-${{ github.event.pull_request.number }}" + else + PREFIX="branch-$(echo '${{ github.ref_name }}' | sed 's/[^a-zA-Z0-9-]/-/g')" + fi + + echo "s3-prefix=${PREFIX}/" >> $GITHUB_OUTPUT + + if [ -n "${{ inputs.preview-base-url }}" ]; then + echo "deployment-url=${{ inputs.preview-base-url }}/${PREFIX}/" >> $GITHUB_OUTPUT + else + echo "deployment-url=https://${{ inputs.s3-bucket }}.s3.amazonaws.com/${PREFIX}/index.html" >> $GITHUB_OUTPUT + fi + else + # Production/staging deployment to root + echo "s3-prefix=" >> $GITHUB_OUTPUT + + if [ -n "${{ inputs.preview-base-url }}" ]; then + echo "deployment-url=${{ inputs.preview-base-url }}/" >> $GITHUB_OUTPUT + else + echo "deployment-url=https://${{ inputs.s3-bucket }}.s3.amazonaws.com/index.html" >> $GITHUB_OUTPUT + fi + fi + + if [ "${{ inputs.debug }}" = "true" ]; then + echo "๐Ÿ” Deployment configuration:" + echo " S3 Prefix: $(cat $GITHUB_OUTPUT | grep s3-prefix | cut -d'=' -f2-)" + echo " Deployment URL: $(cat $GITHUB_OUTPUT | grep deployment-url | cut -d'=' -f2-)" + fi + + - name: Configure multi-brand matrix + id: brand-config + run: | + if [ -n "${{ inputs.brand-config }}" ]; then + echo "matrix=${{ inputs.brand-config }}" >> $GITHUB_OUTPUT + echo "๐Ÿ“ฑ Multi-brand deployment configured" + else + echo 'matrix={"brand":["default"]}' >> $GITHUB_OUTPUT + echo "๐Ÿ“ฑ Single brand deployment" + fi + + # Build and test the application + build: + name: ๐Ÿ—๏ธ Build Application + runs-on: ubuntu-latest + if: inputs.skip-build == false + needs: [prepare] + strategy: + matrix: ${{ fromJSON(needs.prepare.outputs.brand-matrix) }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: ${{ inputs.package-manager }} + + - name: Install dependencies + run: | + debug="" + if [ "${{ inputs.debug }}" = "true" ]; then + debug="--verbose" + fi + + case "${{ inputs.package-manager }}" in + "yarn") + if [ "${{ inputs.is-yarn-classic }}" = "true" ]; then + yarn install --frozen-lockfile $debug + else + yarn install --immutable $debug + fi + ;; + "npm") + npm ci $debug + ;; + *) + echo "โŒ Unsupported package manager: ${{ inputs.package-manager }}" + exit 1 + ;; + esac + + - name: Run tests + if: inputs.skip-tests == false + run: | + debug="" + if [ "${{ inputs.debug }}" = "true" ]; then + debug="--verbose" + fi + + # Check if test script exists + if ${{ inputs.package-manager }} run | grep -q "test"; then + echo "๐Ÿงช Running tests..." + ${{ inputs.package-manager }} run test $debug + else + echo "โ„น๏ธ No test script found, skipping tests" + fi + + - name: Build application + run: | + debug="" + if [ "${{ inputs.debug }}" = "true" ]; then + debug="--verbose" + fi + + # Set brand-specific environment if multi-brand + if [ "${{ matrix.brand }}" != "default" ]; then + echo "๐Ÿท๏ธ Building for brand: ${{ matrix.brand }}" + export BRAND=${{ matrix.brand }} + fi + + echo "๐Ÿ—๏ธ Building application..." + ${{ inputs.package-manager }} run ${{ inputs.build-command }} $debug + + - name: Verify build output + run: | + if [ ! -d "${{ inputs.build-directory }}" ]; then + echo "โŒ Build directory '${{ inputs.build-directory }}' not found" + echo "Available directories:" + ls -la + exit 1 + fi + + echo "โœ… Build completed successfully" + echo "๐Ÿ“ Build directory contents:" + find "${{ inputs.build-directory }}" -type f | head -10 + + - name: Archive build artifacts + uses: actions/upload-artifact@v4 + with: + name: build-artifacts-${{ matrix.brand }} + path: ${{ inputs.build-directory }}/ + retention-days: 1 + + # Deploy to S3 and invalidate CloudFront + deploy: + name: ๐Ÿš€ Deploy to AWS + runs-on: ubuntu-latest + needs: [prepare, build] + if: always() && (needs.build.result == 'success' || inputs.skip-build == true) + environment: ${{ inputs.environment }} + strategy: + matrix: ${{ fromJSON(needs.prepare.outputs.brand-matrix) }} + outputs: + deployment-url: ${{ needs.prepare.outputs.deployment-url }} + preview-url: ${{ needs.prepare.outputs.deployment-url }} + steps: + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.aws-access-key-id }} + aws-secret-access-key: ${{ secrets.aws-secret-access-key }} + aws-region: ${{ inputs.aws-region }} + + - name: Download build artifacts + if: inputs.skip-build == false + uses: actions/download-artifact@v4 + with: + name: build-artifacts-${{ matrix.brand }} + path: ${{ inputs.build-directory }} + + - name: Configure S3 deployment paths + id: s3-config + run: | + S3_PATH="${{ needs.prepare.outputs.s3-prefix }}" + + if [ "${{ matrix.brand }}" != "default" ]; then + S3_PATH="${S3_PATH}${{ matrix.brand }}/" + fi + + echo "s3-path=${S3_PATH}" >> $GITHUB_OUTPUT + echo "๐ŸŽฏ Deploying to: s3://${{ inputs.s3-bucket }}/${S3_PATH}" + + - name: Deploy static assets to S3 + run: | + echo "๐Ÿš€ Deploying static assets..." + + # Deploy static assets with immutable cache headers + aws s3 sync "${{ inputs.build-directory }}" "s3://${{ inputs.s3-bucket }}/${{ steps.s3-config.outputs.s3-path }}" \ + --exclude "*.html" \ + --cache-control "${{ needs.prepare.outputs.cache-control-static }}" \ + --delete \ + ${{ inputs.extra-sync-args }} \ + ${{ inputs.debug == true && '--cli-read-timeout 120 --cli-connect-timeout 60' || '' }} + + - name: Deploy HTML files to S3 + run: | + echo "๐Ÿ“„ Deploying HTML files..." + + # Deploy HTML files with revalidation cache headers + aws s3 sync "${{ inputs.build-directory }}" "s3://${{ inputs.s3-bucket }}/${{ steps.s3-config.outputs.s3-path }}" \ + --include "*.html" \ + --cache-control "${{ needs.prepare.outputs.cache-control-html }}" \ + --delete \ + ${{ inputs.extra-sync-args }} \ + ${{ inputs.debug == true && '--cli-read-timeout 120 --cli-connect-timeout 60' || '' }} + + - name: Invalidate CloudFront cache + run: | + echo "๐Ÿ”„ Invalidating CloudFront cache..." + + # Parse invalidation paths + PATHS=$(echo '${{ needs.prepare.outputs.invalidation-paths }}' | jq -r '.[]') + + # Add brand prefix if multi-brand deployment + if [ "${{ matrix.brand }}" != "default" ]; then + PREFIXED_PATHS="" + for path in $PATHS; do + if [ "$path" = "/*" ]; then + PREFIXED_PATHS="$PREFIXED_PATHS /${{ steps.s3-config.outputs.s3-path }}*" + else + PREFIXED_PATHS="$PREFIXED_PATHS /${{ steps.s3-config.outputs.s3-path }}${path#/}" + fi + done + PATHS="$PREFIXED_PATHS" + fi + + echo "Invalidating paths: $PATHS" + + INVALIDATION_ID=$(aws cloudfront create-invalidation \ + --distribution-id "${{ inputs.cloudfront-distribution-id }}" \ + --paths $PATHS \ + --query 'Invalidation.Id' \ + --output text) + + echo "โœ… CloudFront invalidation created: $INVALIDATION_ID" + + if [ "${{ inputs.debug }}" = "true" ]; then + echo "๐Ÿ” Waiting for invalidation to complete..." + aws cloudfront wait invalidation-completed \ + --distribution-id "${{ inputs.cloudfront-distribution-id }}" \ + --id "$INVALIDATION_ID" + echo "โœ… CloudFront invalidation completed" + fi + + - name: Generate deployment summary + run: | + echo "## ๐Ÿš€ Deployment Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY + echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| **Environment** | ${{ inputs.environment }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Brand** | ${{ matrix.brand }} |" >> $GITHUB_STEP_SUMMARY + echo "| **S3 Bucket** | ${{ inputs.s3-bucket }} |" >> $GITHUB_STEP_SUMMARY + echo "| **S3 Path** | ${{ steps.s3-config.outputs.s3-path }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Cache Strategy** | ${{ inputs.cache-strategy }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Deployment URL** | ${{ needs.prepare.outputs.deployment-url }} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ๐Ÿ“Š Build Information" >> $GITHUB_STEP_SUMMARY + echo "- **Package Manager**: ${{ inputs.package-manager }}" >> $GITHUB_STEP_SUMMARY + echo "- **Build Command**: ${{ inputs.build-command }}" >> $GITHUB_STEP_SUMMARY + echo "- **Build Directory**: ${{ inputs.build-directory }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "${{ inputs.preview-mode }}" = "true" ]; then + echo "### ๐Ÿ” Preview Environment" >> $GITHUB_STEP_SUMMARY + echo "This is a preview deployment. The application is available at:" >> $GITHUB_STEP_SUMMARY + echo "**[${{ needs.prepare.outputs.deployment-url }}](${{ needs.prepare.outputs.deployment-url }})**" >> $GITHUB_STEP_SUMMARY + else + echo "### ๐ŸŒ Production Deployment" >> $GITHUB_STEP_SUMMARY + echo "Application deployed to ${{ inputs.environment }} environment:" >> $GITHUB_STEP_SUMMARY + echo "**[${{ needs.prepare.outputs.deployment-url }}](${{ needs.prepare.outputs.deployment-url }})**" >> $GITHUB_STEP_SUMMARY + fi + + # Cleanup artifacts to save storage space + cleanup: + name: ๐Ÿงน Cleanup + runs-on: ubuntu-latest + needs: [prepare, build, deploy] + if: always() + permissions: + actions: write + steps: + - name: Delete build artifacts + uses: actions/github-script@v7 + with: + script: | + const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: process.env.GITHUB_RUN_ID, + }); + + const buildArtifacts = artifacts.data.artifacts.filter(a => + a.name.startsWith('build-artifacts-') + ); + + if (buildArtifacts.length === 0) { + core.info("No build artifacts found to cleanup."); + } else { + for (const artifact of buildArtifacts) { + await github.rest.actions.deleteArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: artifact.id + }); + core.info(`Deleted artifact '${artifact.name}' with ID ${artifact.id}`); + } + } \ No newline at end of file diff --git a/README.md b/README.md index 4dd93a1..6fae6ca 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,133 @@ jobs: skip-format: false ``` +### PWA Deployment + +A comprehensive Progressive Web Application deployment workflow supporting S3 static hosting with CloudFront CDN, multi-environment deployments, branch-based previews, and multi-brand configurations. + +#### **Features** +- **Multi-environment support**: staging, production, and preview environments +- **Branch-based previews**: Automatic preview deployments for pull requests +- **Dual cache strategies**: Immutable caching for static assets, revalidation for HTML +- **CloudFront integration**: Automatic cache invalidation with configurable paths +- **Multi-brand deployment**: Parallel deployment support for multiple brands +- **Node.js 16-22 support**: Compatible with Yarn and npm package managers +- **Manual production gates**: Environment-based deployment protection +- **Comprehensive caching**: Build artifact optimization and cleanup + +#### **Inputs** +| Name | Required | Type | Default | Description | +|------|----------|------|---------|-------------| +| **AWS Configuration** | +| aws-region | โŒ | string | ap-southeast-2 | AWS region for deployment | +| s3-bucket | โœ… | string | | S3 bucket name for deployment | +| cloudfront-distribution-id | โœ… | string | | CloudFront distribution ID for cache invalidation | +| **Environment Configuration** | +| environment | โŒ | string | staging | Deployment environment (staging/production/preview) | +| **Build Configuration** | +| package-manager | โŒ | string | yarn | Node package manager (yarn/npm) | +| is-yarn-classic | โŒ | boolean | false | Use Yarn Classic (pre-Berry) instead of modern Yarn | +| build-command | โŒ | string | build | Build command to execute | +| build-directory | โŒ | string | dist | Directory containing built assets to deploy | +| **Cache Strategy Configuration** | +| cache-strategy | โŒ | string | immutable | Cache strategy for assets (immutable/no-cache) | +| **Preview Environment Configuration** | +| preview-mode | โŒ | boolean | false | Enable preview mode for PR-based deployments | +| preview-base-url | โŒ | string | | Base URL for preview deployments | +| **Multi-brand Configuration** | +| brand-config | โŒ | string | | JSON configuration for multi-brand deployments | +| **Advanced Configuration** | +| cloudfront-invalidation-paths | โŒ | string | ["/*"] | CloudFront invalidation paths (JSON array) | +| extra-sync-args | โŒ | string | | Additional AWS S3 sync arguments | +| **Debug and Control** | +| debug | โŒ | boolean | false | Enable verbose logging and debug output | +| skip-build | โŒ | boolean | false | Skip the build step (use pre-built assets) | +| skip-tests | โŒ | boolean | false | Skip test execution | + +#### **Secrets** +| Name | Required | Description | +|------|----------|-------------| +| aws-access-key-id | โœ… | AWS access key ID | +| aws-secret-access-key | โœ… | AWS secret access key | +| cloudfront-signing-key | โŒ | CloudFront signing key (optional, for signed URLs) | + +#### **Outputs** +| Name | Description | +|------|-------------| +| deployment-url | URL of the deployed application | +| preview-url | Preview URL for PR deployments | + +#### **Example Usage** + +**Basic Production Deployment:** +```yaml +jobs: + deploy-production: + uses: aligent/workflows/.github/workflows/pwa-deployment.yml@main + with: + s3-bucket: my-production-bucket + cloudfront-distribution-id: E1234567890ABC + environment: production + cache-strategy: immutable + secrets: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} +``` + +**Preview Environment for Pull Requests:** +```yaml +jobs: + deploy-preview: + if: github.event_name == 'pull_request' + uses: aligent/workflows/.github/workflows/pwa-deployment.yml@main + with: + s3-bucket: my-preview-bucket + cloudfront-distribution-id: E1234567890ABC + environment: preview + preview-mode: true + preview-base-url: https://preview.example.com + cache-strategy: no-cache + secrets: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} +``` + +**Multi-brand Deployment:** +```yaml +jobs: + deploy-multi-brand: + uses: aligent/workflows/.github/workflows/pwa-deployment.yml@main + with: + s3-bucket: my-multi-brand-bucket + cloudfront-distribution-id: E1234567890ABC + environment: production + brand-config: '{"brand":["brand-a","brand-b","brand-c"]}' + build-command: build:brands + secrets: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} +``` + +**Custom Build Configuration:** +```yaml +jobs: + deploy-custom: + uses: aligent/workflows/.github/workflows/pwa-deployment.yml@main + with: + s3-bucket: my-custom-bucket + cloudfront-distribution-id: E1234567890ABC + environment: staging + package-manager: npm + build-command: build:staging + build-directory: build + cloudfront-invalidation-paths: '["/*", "/api/*"]' + extra-sync-args: --exclude "*.map" + debug: true + secrets: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} +``` + ### Nx Serverless Deployment #### **Inputs** From ba6125108bd7db4cfe9695215970bfe11bb8a903 Mon Sep 17 00:00:00 2001 From: Daniel van der Ploeg Date: Fri, 15 Aug 2025 09:13:03 +0930 Subject: [PATCH 2/5] DO-1743: address PR review feedback - Update environment parameter description to clarify it accepts any GitHub environment name - Remove hardcoded environment validation, rely on GitHub's environment protection rules - Clarify that cloudfront-signing-key is already optional (no change needed) - Multi-brand matrix build already implemented (no change needed) --- .github/workflows/pwa-deployment.yml | 8 ++++---- README.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pwa-deployment.yml b/.github/workflows/pwa-deployment.yml index fd18fb0..0ab214f 100644 --- a/.github/workflows/pwa-deployment.yml +++ b/.github/workflows/pwa-deployment.yml @@ -20,7 +20,7 @@ on: # Environment Configuration environment: - description: "Deployment environment (staging/production/preview)" + description: "Deployment environment (GitHub environment name for protection rules)" type: string required: false default: "staging" @@ -146,9 +146,9 @@ jobs: exit 1 fi - if [ "${{ inputs.environment }}" != "staging" ] && [ "${{ inputs.environment }}" != "production" ] && [ "${{ inputs.environment }}" != "preview" ]; then - echo "โŒ Error: environment must be one of: staging, production, preview" - exit 1 + # Environment name is validated by GitHub's environment protection rules + if [ -n "${{ inputs.environment }}" ]; then + echo "๐Ÿ” Using GitHub environment: ${{ inputs.environment }}" fi if [ "${{ inputs.cache-strategy }}" != "immutable" ] && [ "${{ inputs.cache-strategy }}" != "no-cache" ]; then diff --git a/README.md b/README.md index 6fae6ca..82b1003 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ A comprehensive Progressive Web Application deployment workflow supporting S3 st | s3-bucket | โœ… | string | | S3 bucket name for deployment | | cloudfront-distribution-id | โœ… | string | | CloudFront distribution ID for cache invalidation | | **Environment Configuration** | -| environment | โŒ | string | staging | Deployment environment (staging/production/preview) | +| environment | โŒ | string | staging | Deployment environment (GitHub environment name for protection rules) | | **Build Configuration** | | package-manager | โŒ | string | yarn | Node package manager (yarn/npm) | | is-yarn-classic | โŒ | boolean | false | Use Yarn Classic (pre-Berry) instead of modern Yarn | From 7d3c995ee4ee6111f0d992534ab71818b46fad2b Mon Sep 17 00:00:00 2001 From: Daniel van der Ploeg Date: Fri, 15 Aug 2025 09:18:25 +0930 Subject: [PATCH 3/5] DO-1743: remove cloudfront-signing-key secret completely - Remove cloudfront-signing-key from workflow secrets - Update README documentation to remove the secret --- .github/workflows/pwa-deployment.yml | 3 --- README.md | 1 - 2 files changed, 4 deletions(-) diff --git a/.github/workflows/pwa-deployment.yml b/.github/workflows/pwa-deployment.yml index 0ab214f..d31c770 100644 --- a/.github/workflows/pwa-deployment.yml +++ b/.github/workflows/pwa-deployment.yml @@ -109,9 +109,6 @@ on: aws-secret-access-key: description: "AWS secret access key" required: true - cloudfront-signing-key: - description: "CloudFront signing key (optional, for signed URLs)" - required: false outputs: deployment-url: diff --git a/README.md b/README.md index 82b1003..e3b6b4d 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,6 @@ A comprehensive Progressive Web Application deployment workflow supporting S3 st |------|----------|-------------| | aws-access-key-id | โœ… | AWS access key ID | | aws-secret-access-key | โœ… | AWS secret access key | -| cloudfront-signing-key | โŒ | CloudFront signing key (optional, for signed URLs) | #### **Outputs** | Name | Description | From 78dd95cd9ea2bfccb099cd58a11f89cda5f5d6ab Mon Sep 17 00:00:00 2001 From: Daniel van der Ploeg Date: Fri, 20 Feb 2026 11:52:53 +1030 Subject: [PATCH 4/5] feat DO-1743: combine jobs for cost saving - also upgrade action versions --- .github/workflows/pwa-deployment.yml | 136 +++++---------------------- 1 file changed, 25 insertions(+), 111 deletions(-) diff --git a/.github/workflows/pwa-deployment.yml b/.github/workflows/pwa-deployment.yml index d31c770..9baea88 100644 --- a/.github/workflows/pwa-deployment.yml +++ b/.github/workflows/pwa-deployment.yml @@ -24,7 +24,7 @@ on: type: string required: false default: "staging" - + # Build Configuration package-manager: description: "Node package manager (yarn/npm)" @@ -53,7 +53,7 @@ on: type: string required: false default: "immutable" - + # Preview Environment Configuration preview-mode: description: "Enable preview mode for PR-based deployments" @@ -84,7 +84,7 @@ on: type: string required: false default: "" - + # Debug and Control debug: description: "Enable verbose logging and debug output" @@ -100,7 +100,7 @@ on: description: "Skip test execution" type: boolean required: false - default: false + default: true secrets: aws-access-key-id: @@ -131,30 +131,6 @@ jobs: brand-matrix: ${{ steps.brand-config.outputs.matrix }} invalidation-paths: ${{ steps.cache-config.outputs.invalidation-paths }} steps: - - name: Validate required inputs - run: | - if [ -z "${{ inputs.s3-bucket }}" ]; then - echo "โŒ Error: s3-bucket is required" - exit 1 - fi - - if [ -z "${{ inputs.cloudfront-distribution-id }}" ]; then - echo "โŒ Error: cloudfront-distribution-id is required" - exit 1 - fi - - # Environment name is validated by GitHub's environment protection rules - if [ -n "${{ inputs.environment }}" ]; then - echo "๐Ÿ” Using GitHub environment: ${{ inputs.environment }}" - fi - - if [ "${{ inputs.cache-strategy }}" != "immutable" ] && [ "${{ inputs.cache-strategy }}" != "no-cache" ]; then - echo "โŒ Error: cache-strategy must be one of: immutable, no-cache" - exit 1 - fi - - echo "โœ… All required inputs validated" - - name: Configure cache strategy id: cache-config run: | @@ -192,9 +168,9 @@ jobs: else PREFIX="branch-$(echo '${{ github.ref_name }}' | sed 's/[^a-zA-Z0-9-]/-/g')" fi - + echo "s3-prefix=${PREFIX}/" >> $GITHUB_OUTPUT - + if [ -n "${{ inputs.preview-base-url }}" ]; then echo "deployment-url=${{ inputs.preview-base-url }}/${PREFIX}/" >> $GITHUB_OUTPUT else @@ -203,7 +179,7 @@ jobs: else # Production/staging deployment to root echo "s3-prefix=" >> $GITHUB_OUTPUT - + if [ -n "${{ inputs.preview-base-url }}" ]; then echo "deployment-url=${{ inputs.preview-base-url }}/" >> $GITHUB_OUTPUT else @@ -229,8 +205,8 @@ jobs: fi # Build and test the application - build: - name: ๐Ÿ—๏ธ Build Application + build-and-deploy: + name: ๐Ÿš€ Build Application and Deploy to AWS runs-on: ubuntu-latest if: inputs.skip-build == false needs: [prepare] @@ -238,10 +214,10 @@ jobs: matrix: ${{ fromJSON(needs.prepare.outputs.brand-matrix) }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: .nvmrc cache: ${{ inputs.package-manager }} @@ -277,7 +253,7 @@ jobs: if [ "${{ inputs.debug }}" = "true" ]; then debug="--verbose" fi - + # Check if test script exists if ${{ inputs.package-manager }} run | grep -q "test"; then echo "๐Ÿงช Running tests..." @@ -315,56 +291,29 @@ jobs: echo "๐Ÿ“ Build directory contents:" find "${{ inputs.build-directory }}" -type f | head -10 - - name: Archive build artifacts - uses: actions/upload-artifact@v4 - with: - name: build-artifacts-${{ matrix.brand }} - path: ${{ inputs.build-directory }}/ - retention-days: 1 - - # Deploy to S3 and invalidate CloudFront - deploy: - name: ๐Ÿš€ Deploy to AWS - runs-on: ubuntu-latest - needs: [prepare, build] - if: always() && (needs.build.result == 'success' || inputs.skip-build == true) - environment: ${{ inputs.environment }} - strategy: - matrix: ${{ fromJSON(needs.prepare.outputs.brand-matrix) }} - outputs: - deployment-url: ${{ needs.prepare.outputs.deployment-url }} - preview-url: ${{ needs.prepare.outputs.deployment-url }} - steps: - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@v6 with: aws-access-key-id: ${{ secrets.aws-access-key-id }} aws-secret-access-key: ${{ secrets.aws-secret-access-key }} aws-region: ${{ inputs.aws-region }} - - name: Download build artifacts - if: inputs.skip-build == false - uses: actions/download-artifact@v4 - with: - name: build-artifacts-${{ matrix.brand }} - path: ${{ inputs.build-directory }} - - name: Configure S3 deployment paths id: s3-config run: | S3_PATH="${{ needs.prepare.outputs.s3-prefix }}" - + if [ "${{ matrix.brand }}" != "default" ]; then S3_PATH="${S3_PATH}${{ matrix.brand }}/" fi - + echo "s3-path=${S3_PATH}" >> $GITHUB_OUTPUT echo "๐ŸŽฏ Deploying to: s3://${{ inputs.s3-bucket }}/${S3_PATH}" - name: Deploy static assets to S3 run: | echo "๐Ÿš€ Deploying static assets..." - + # Deploy static assets with immutable cache headers aws s3 sync "${{ inputs.build-directory }}" "s3://${{ inputs.s3-bucket }}/${{ steps.s3-config.outputs.s3-path }}" \ --exclude "*.html" \ @@ -376,7 +325,7 @@ jobs: - name: Deploy HTML files to S3 run: | echo "๐Ÿ“„ Deploying HTML files..." - + # Deploy HTML files with revalidation cache headers aws s3 sync "${{ inputs.build-directory }}" "s3://${{ inputs.s3-bucket }}/${{ steps.s3-config.outputs.s3-path }}" \ --include "*.html" \ @@ -388,10 +337,10 @@ jobs: - name: Invalidate CloudFront cache run: | echo "๐Ÿ”„ Invalidating CloudFront cache..." - + # Parse invalidation paths PATHS=$(echo '${{ needs.prepare.outputs.invalidation-paths }}' | jq -r '.[]') - + # Add brand prefix if multi-brand deployment if [ "${{ matrix.brand }}" != "default" ]; then PREFIXED_PATHS="" @@ -404,17 +353,17 @@ jobs: done PATHS="$PREFIXED_PATHS" fi - + echo "Invalidating paths: $PATHS" - + INVALIDATION_ID=$(aws cloudfront create-invalidation \ --distribution-id "${{ inputs.cloudfront-distribution-id }}" \ --paths $PATHS \ --query 'Invalidation.Id' \ --output text) - + echo "โœ… CloudFront invalidation created: $INVALIDATION_ID" - + if [ "${{ inputs.debug }}" = "true" ]; then echo "๐Ÿ” Waiting for invalidation to complete..." aws cloudfront wait invalidation-completed \ @@ -441,49 +390,14 @@ jobs: echo "- **Build Command**: ${{ inputs.build-command }}" >> $GITHUB_STEP_SUMMARY echo "- **Build Directory**: ${{ inputs.build-directory }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - + if [ "${{ inputs.preview-mode }}" = "true" ]; then echo "### ๐Ÿ” Preview Environment" >> $GITHUB_STEP_SUMMARY echo "This is a preview deployment. The application is available at:" >> $GITHUB_STEP_SUMMARY echo "**[${{ needs.prepare.outputs.deployment-url }}](${{ needs.prepare.outputs.deployment-url }})**" >> $GITHUB_STEP_SUMMARY else - echo "### ๐ŸŒ Production Deployment" >> $GITHUB_STEP_SUMMARY + echo "### ๐ŸŒ Deployment" >> $GITHUB_STEP_SUMMARY echo "Application deployed to ${{ inputs.environment }} environment:" >> $GITHUB_STEP_SUMMARY echo "**[${{ needs.prepare.outputs.deployment-url }}](${{ needs.prepare.outputs.deployment-url }})**" >> $GITHUB_STEP_SUMMARY fi - # Cleanup artifacts to save storage space - cleanup: - name: ๐Ÿงน Cleanup - runs-on: ubuntu-latest - needs: [prepare, build, deploy] - if: always() - permissions: - actions: write - steps: - - name: Delete build artifacts - uses: actions/github-script@v7 - with: - script: | - const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: process.env.GITHUB_RUN_ID, - }); - - const buildArtifacts = artifacts.data.artifacts.filter(a => - a.name.startsWith('build-artifacts-') - ); - - if (buildArtifacts.length === 0) { - core.info("No build artifacts found to cleanup."); - } else { - for (const artifact of buildArtifacts) { - await github.rest.actions.deleteArtifact({ - owner: context.repo.owner, - repo: context.repo.repo, - artifact_id: artifact.id - }); - core.info(`Deleted artifact '${artifact.name}' with ID ${artifact.id}`); - } - } \ No newline at end of file From fcbaf1fbb48f8778b6a2a54fedfb1f92ba568674 Mon Sep 17 00:00:00 2001 From: Daniel van der Ploeg Date: Fri, 20 Feb 2026 15:32:44 +1030 Subject: [PATCH 5/5] feat DO-1743: few other fixes - Use environment correctly - Fix S3 caching strategy --- .github/workflows/pwa-deployment.yml | 60 ++++++++++------------------ 1 file changed, 21 insertions(+), 39 deletions(-) diff --git a/.github/workflows/pwa-deployment.yml b/.github/workflows/pwa-deployment.yml index 9baea88..d3af775 100644 --- a/.github/workflows/pwa-deployment.yml +++ b/.github/workflows/pwa-deployment.yml @@ -91,16 +91,6 @@ on: type: boolean required: false default: false - skip-build: - description: "Skip the build step (use pre-built assets)" - type: boolean - required: false - default: false - skip-tests: - description: "Skip test execution" - type: boolean - required: false - default: true secrets: aws-access-key-id: @@ -113,10 +103,10 @@ on: outputs: deployment-url: description: "URL of the deployed application" - value: ${{ jobs.deploy.outputs.deployment-url }} + value: ${{ jobs.build-and-deploy.outputs.deployment-url }} preview-url: description: "Preview URL for PR deployments" - value: ${{ jobs.deploy.outputs.preview-url }} + value: ${{ jobs.build-and-deploy.outputs.preview-url }} jobs: # Validate inputs and prepare deployment configuration @@ -146,6 +136,10 @@ jobs: echo "cache-control-static=no-cache, no-store, must-revalidate" >> $GITHUB_OUTPUT echo "cache-control-html=no-cache, no-store, must-revalidate" >> $GITHUB_OUTPUT ;; + *) + echo "โŒ Invalid cache-strategy: '${{ inputs.cache-strategy }}'. Must be 'immutable' or 'no-cache'." + exit 1 + ;; esac # Prepare invalidation paths @@ -208,8 +202,13 @@ jobs: build-and-deploy: name: ๐Ÿš€ Build Application and Deploy to AWS runs-on: ubuntu-latest - if: inputs.skip-build == false needs: [prepare] + environment: + name: ${{ inputs.environment }} + url: ${{ needs.prepare.outputs.deployment-url }} + outputs: + deployment-url: ${{ needs.prepare.outputs.deployment-url }} + preview-url: ${{ needs.prepare.outputs.deployment-url }} strategy: matrix: ${{ fromJSON(needs.prepare.outputs.brand-matrix) }} steps: @@ -246,22 +245,6 @@ jobs: ;; esac - - name: Run tests - if: inputs.skip-tests == false - run: | - debug="" - if [ "${{ inputs.debug }}" = "true" ]; then - debug="--verbose" - fi - - # Check if test script exists - if ${{ inputs.package-manager }} run | grep -q "test"; then - echo "๐Ÿงช Running tests..." - ${{ inputs.package-manager }} run test $debug - else - echo "โ„น๏ธ No test script found, skipping tests" - fi - - name: Build application run: | debug="" @@ -314,25 +297,25 @@ jobs: run: | echo "๐Ÿš€ Deploying static assets..." - # Deploy static assets with immutable cache headers + # Deploy static assets with immutable cache headers (exclude HTML and service-worker.js) aws s3 sync "${{ inputs.build-directory }}" "s3://${{ inputs.s3-bucket }}/${{ steps.s3-config.outputs.s3-path }}" \ --exclude "*.html" \ + --exclude "service-worker.js" \ --cache-control "${{ needs.prepare.outputs.cache-control-static }}" \ --delete \ - ${{ inputs.extra-sync-args }} \ - ${{ inputs.debug == true && '--cli-read-timeout 120 --cli-connect-timeout 60' || '' }} + ${{ inputs.extra-sync-args }} - - name: Deploy HTML files to S3 + - name: Deploy HTML and service worker to S3 run: | - echo "๐Ÿ“„ Deploying HTML files..." + echo "๐Ÿ“„ Deploying HTML files and service worker..." - # Deploy HTML files with revalidation cache headers + # Deploy HTML and service-worker.js with revalidation cache headers aws s3 sync "${{ inputs.build-directory }}" "s3://${{ inputs.s3-bucket }}/${{ steps.s3-config.outputs.s3-path }}" \ + --exclude "*" \ --include "*.html" \ + --include "service-worker.js" \ --cache-control "${{ needs.prepare.outputs.cache-control-html }}" \ - --delete \ - ${{ inputs.extra-sync-args }} \ - ${{ inputs.debug == true && '--cli-read-timeout 120 --cli-connect-timeout 60' || '' }} + ${{ inputs.extra-sync-args }} - name: Invalidate CloudFront cache run: | @@ -400,4 +383,3 @@ jobs: echo "Application deployed to ${{ inputs.environment }} environment:" >> $GITHUB_STEP_SUMMARY echo "**[${{ needs.prepare.outputs.deployment-url }}](${{ needs.prepare.outputs.deployment-url }})**" >> $GITHUB_STEP_SUMMARY fi -