diff --git a/.github/workflows/pwa-deployment.yml b/.github/workflows/pwa-deployment.yml index 93e4084..71cc5c5 100644 --- a/.github/workflows/pwa-deployment.yml +++ b/.github/workflows/pwa-deployment.yml @@ -12,18 +12,22 @@ on: s3-bucket: description: "S3 bucket name for deployment" type: string - required: true + required: false cloudfront-distribution-id: description: "CloudFront distribution ID for cache invalidation" type: string - required: true + required: false + role-session-name: + description: "AWS role session name for OIDC authentication (default: {repo}-{short-sha}-{run-number})" + type: string + required: false + default: "" # Environment Configuration environment: - description: "Deployment environment (GitHub environment name for protection rules)" + description: "GitHub Environment name for secrets/variables (e.g. Staging, Production)" type: string - required: false - default: "staging" + required: true # Build Configuration package-manager: @@ -92,14 +96,6 @@ on: 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 - outputs: deployment-url: description: "URL of the deployed application" @@ -113,6 +109,7 @@ jobs: prepare: name: 🔍 Prepare Deployment runs-on: ubuntu-latest + environment: ${{ inputs.environment }} outputs: cache-control-static: ${{ steps.cache-config.outputs.cache-control-static }} cache-control-html: ${{ steps.cache-config.outputs.cache-control-html }} @@ -120,7 +117,75 @@ jobs: deployment-url: ${{ steps.deployment-config.outputs.deployment-url }} brand-matrix: ${{ steps.brand-config.outputs.matrix }} invalidation-paths: ${{ steps.cache-config.outputs.invalidation-paths }} + auth-mode: ${{ steps.validate-inputs.outputs.auth-mode }} + role-session-name: ${{ steps.resolve-session-name.outputs.role-session-name }} steps: + - name: Validate required inputs + id: validate-inputs + env: + ENVIRONMENT: ${{ inputs.environment }} + S3_BUCKET: ${{ vars.S3_BUCKET }} || ${{ inputs.s3-bucket }} + CLOUDFRONT_DISTRIBUTION_ID: ${{ vars.CLOUDFRONT_DISTRIBUTION_ID }} || ${{ inputs.cloudfront-distribution-id }} + VAR_AWS_ACCESS_KEY_ID: ${{ vars.AWS_ACCESS_KEY_ID }} + SECRET_AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + VAR_AWS_ROLE_ARN: ${{ vars.AWS_ROLE_ARN }} + run: | + echo "🔍 Validating deployment configuration..." + echo "â„šī¸ Using variables from $ENVIRONMENT environment" + + if [ -z "$S3_BUCKET" ]; then + echo "❌ Error: S3_BUCKET must be set as a variable in your $ENVIRONMENT environment" + exit 1 + fi + echo "✅ S3_BUCKET: $S3_BUCKET" + + if [ -z "$CLOUDFRONT_DISTRIBUTION_ID" ]; then + echo "❌ Error: CLOUDFRONT_DISTRIBUTION_ID must be set as a variable in your $ENVIRONMENT environment" + exit 1 + fi + echo "✅ CLOUDFRONT_DISTRIBUTION_ID: $CLOUDFRONT_DISTRIBUTION_ID" + + if [ -n "$VAR_AWS_ACCESS_KEY_ID" ]; then + echo "â„šī¸ AWS_ACCESS_KEY_ID set from variable: $VAR_AWS_ACCESS_KEY_ID" + if [ -z "$SECRET_AWS_SECRET_ACCESS_KEY" ]; then + echo "❌ Error: AWS_SECRET_ACCESS_KEY is not defined as a secret in your $ENVIRONMENT environment" + exit 1 + fi + echo "✅ AWS_SECRET_ACCESS_KEY secret is configured" + echo "✅ Using static credential authentication" + echo "auth-mode=static" >> $GITHUB_OUTPUT + elif [ -n "$VAR_AWS_ROLE_ARN" ]; then + echo "â„šī¸ AWS_ROLE_ARN set from variable: $VAR_AWS_ROLE_ARN" + echo "✅ Using OIDC authentication" + echo "auth-mode=oidc" >> $GITHUB_OUTPUT + else + echo "❌ Error: No AWS credentials configured in your $ENVIRONMENT environment." + echo " Configure one of the following:" + echo " - Static credentials: AWS_ACCESS_KEY_ID (variable) + AWS_SECRET_ACCESS_KEY (secret)" + echo " - OIDC: AWS_ROLE_ARN (variable)" + exit 1 + fi + + echo "✅ All inputs validated successfully" + + - name: Resolve role session name + id: resolve-session-name + if: steps.validate-inputs.outputs.auth-mode == 'oidc' + env: + ROLE_SESSION_NAME: ${{ inputs.role-session-name }} + COMMIT_SHA: ${{ github.sha }} + REPOSITORY_NAME: ${{ github.event.repository.name }} + RUN_NUMBER: ${{ github.run_number }} + run: | + SESSION_NAME="$ROLE_SESSION_NAME" + if [ -z "$SESSION_NAME" ]; then + SHORT_SHA=$(echo "$COMMIT_SHA" | cut -c1-7) + SESSION_NAME="${REPOSITORY_NAME}-${SHORT_SHA}-${RUN_NUMBER}" + fi + # AWS session names: max 64 chars, allowed [a-zA-Z0-9=,.@-] + SESSION_NAME=$(echo "$SESSION_NAME" | tr -c '[:alnum:]=,.@-' '-' | cut -c1-64) + echo "role-session-name=$SESSION_NAME" >> $GITHUB_OUTPUT + - name: Configure cache strategy id: cache-config run: | @@ -196,7 +261,7 @@ jobs: fi env: INPUTS_PREVIEW_BASE_URL: ${{ inputs.preview-base-url }} - INPUTS_S3_BUCKET: ${{ inputs.s3-bucket }} + INPUTS_S3_BUCKET: ${{ vars.S3_BUCKET }} || ${{ inputs.s3-bucket }} - name: Configure multi-brand matrix id: brand-config @@ -212,11 +277,14 @@ jobs: env: INPUTS_BRAND_CONFIG: ${{ inputs.brand-config }} - # Build and test the application + # Build and deploy the application build-and-deploy: name: 🚀 Build Application and Deploy to AWS runs-on: ubuntu-latest needs: [prepare] + permissions: + id-token: write + contents: read environment: name: ${{ inputs.environment }} url: ${{ needs.prepare.outputs.deployment-url }} @@ -301,9 +369,17 @@ jobs: - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 #v6.0.0 with: - aws-access-key-id: ${{ secrets.aws-access-key-id }} - aws-secret-access-key: ${{ secrets.aws-secret-access-key }} - aws-region: ${{ inputs.aws-region }} + aws-access-key-id: ${{ vars.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ vars.AWS_REGION || inputs.aws-region }} + + - name: Configure AWS credentials (OIDC) + if: needs.prepare.outputs.auth-mode == 'oidc' + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 #v6.0.0 + with: + role-to-assume: ${{ vars.AWS_ROLE_ARN }} + role-session-name: ${{ needs.prepare.outputs.role-session-name }} + aws-region: ${{ vars.AWS_REGION || inputs.aws-region }} - name: Configure S3 deployment paths id: s3-config @@ -319,7 +395,7 @@ jobs: env: NEEDS_PREPARE_OUTPUTS_S3_PREFIX: ${{ needs.prepare.outputs.s3-prefix }} MATRIX_BRAND: ${{ matrix.brand }} - INPUTS_S3_BUCKET: ${{ inputs.s3-bucket }} + INPUTS_S3_BUCKET: ${{ vars.S3_BUCKET }} || ${{ inputs.s3-bucket }} - name: Deploy static assets to S3 run: | @@ -336,7 +412,7 @@ jobs: ${INPUTS_EXTRA_SYNC_ARGS} env: INPUTS_BUILD_DIRECTORY: ${{ inputs.build-directory }} - INPUTS_S3_BUCKET: ${{ inputs.s3-bucket }} + INPUTS_S3_BUCKET: ${{ vars.S3_BUCKET }} || ${{ inputs.s3-bucket }} STEPS_S3_CONFIG_OUTPUTS_S3_PATH: ${{ steps.s3-config.outputs.s3-path }} NEEDS_PREPARE_OUTPUTS_CACHE_CONTROL_STATIC: ${{ needs.prepare.outputs.cache-control-static }} INPUTS_EXTRA_SYNC_ARGS: ${{ inputs.extra-sync-args }} @@ -356,7 +432,7 @@ jobs: ${INPUTS_EXTRA_SYNC_ARGS} env: INPUTS_BUILD_DIRECTORY: ${{ inputs.build-directory }} - INPUTS_S3_BUCKET: ${{ inputs.s3-bucket }} + INPUTS_S3_BUCKET: ${{ vars.S3_BUCKET }} || ${{ inputs.s3-bucket }} STEPS_S3_CONFIG_OUTPUTS_S3_PATH: ${{ steps.s3-config.outputs.s3-path }} NEEDS_PREPARE_OUTPUTS_CACHE_CONTROL_HTML: ${{ needs.prepare.outputs.cache-control-html }} INPUTS_EXTRA_SYNC_ARGS: ${{ inputs.extra-sync-args }} @@ -384,7 +460,7 @@ jobs: echo "Invalidating paths: $PATHS" INVALIDATION_ID=$(aws cloudfront create-invalidation \ - --distribution-id "${INPUTS_CLOUDFRONT_DISTRIBUTION_ID}" \ + --distribution-id "${CLOUDFRONT_DISTRIBUTION_ID}" \ --paths $PATHS \ --query 'Invalidation.Id' \ --output text) @@ -394,7 +470,7 @@ jobs: if [ "${{ inputs.debug }}" = "true" ]; then echo "🔍 Waiting for invalidation to complete..." aws cloudfront wait invalidation-completed \ - --distribution-id "${INPUTS_CLOUDFRONT_DISTRIBUTION_ID}" \ + --distribution-id "${CLOUDFRONT_DISTRIBUTION_ID}" \ --id "$INVALIDATION_ID" echo "✅ CloudFront invalidation completed" fi @@ -402,8 +478,7 @@ jobs: NEEDS_PREPARE_OUTPUTS_INVALIDATION_PATHS: ${{ needs.prepare.outputs.invalidation-paths }} MATRIX_BRAND: ${{ matrix.brand }} STEPS_S3_CONFIG_OUTPUTS_S3_PATH: ${{ steps.s3-config.outputs.s3-path }} - INPUTS_CLOUDFRONT_DISTRIBUTION_ID: ${{ inputs.cloudfront-distribution-id }} - + CLOUDFRONT_DISTRIBUTION_ID: ${{ vars.CLOUDFRONT_DISTRIBUTION_ID }} || ${{ inputs.cloudfront-distribution-id }} - name: Generate deployment summary run: | echo "## 🚀 Deployment Summary" >> $GITHUB_STEP_SUMMARY @@ -437,7 +512,7 @@ jobs: env: INPUTS_ENVIRONMENT: ${{ inputs.environment }} MATRIX_BRAND: ${{ matrix.brand }} - INPUTS_S3_BUCKET: ${{ inputs.s3-bucket }} + INPUTS_S3_BUCKET: ${{ vars.S3_BUCKET }} || ${{ inputs.s3-bucket }} STEPS_S3_CONFIG_OUTPUTS_S3_PATH: ${{ steps.s3-config.outputs.s3-path }} INPUTS_CACHE_STRATEGY: ${{ inputs.cache-strategy }} NEEDS_PREPARE_OUTPUTS_DEPLOYMENT_URL: ${{ needs.prepare.outputs.deployment-url }} diff --git a/docs/pwa-deployment.md b/docs/pwa-deployment.md index c048308..3726e3f 100644 --- a/docs/pwa-deployment.md +++ b/docs/pwa-deployment.md @@ -12,15 +12,31 @@ A comprehensive Progressive Web Application deployment workflow supporting S3 st - **Manual production gates**: Environment-based deployment protection - **Comprehensive caching**: Build artifact optimisation and cleanup +#### **GitHub Environment Variables and Secrets** + +Environment-specific values are read directly from the GitHub Environment (set via `github-environment`), rather than being passed as workflow inputs. Configure the following on each environment: + +| Name | Type | Required | Description | +|------|------|----------|-------------| +| `S3_BUCKET` | variable | :white_check_mark: | S3 bucket name for deployment | +| `CLOUDFRONT_DISTRIBUTION_ID` | variable | :white_check_mark: | CloudFront distribution ID for cache invalidation | +| `AWS_REGION` | variable | :x: | AWS region (falls back to `aws-region` input) | +| **Static credentials** | | | | +| `AWS_ACCESS_KEY_ID` | variable | :white_check_mark: | AWS access key ID (required if not using OIDC) | +| `AWS_SECRET_ACCESS_KEY` | secret | :white_check_mark: | AWS secret access key (required if not using OIDC) | +| **OIDC** | | | | +| `AWS_ROLE_ARN` | variable | :white_check_mark: | IAM role ARN to assume via OIDC (alternative to static credentials) | + +Either `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` **or** `AWS_ROLE_ARN` must be configured. The workflow detects which to use automatically. + #### **Inputs** | Name | Required | Type | Default | Description | |------|----------|------|---------|-------------| -| **AWS Configuration** | -| aws-region | :x: | string | ap-southeast-2 | AWS region for deployment | -| s3-bucket | :white_check_mark: | string | | S3 bucket name for deployment | -| cloudfront-distribution-id | :white_check_mark: | string | | CloudFront distribution ID for cache invalidation | | **Environment Configuration** | -| environment | :x: | string | staging | Deployment environment (GitHub environment name for protection rules) | +| github-environment | :white_check_mark: | string | | GitHub Environment name for secrets/variables (e.g. Staging, Production) | +| **AWS Configuration** | +| aws-region | :x: | string | ap-southeast-2 | AWS region fallback (overridden by `AWS_REGION` environment variable if set) | +| role-session-name | :x: | string | | AWS role session name for OIDC (default: `{repo}-{short-sha}-{run-number}`) | | **Build Configuration** | | package-manager | :x: | string | yarn | Node package manager (yarn/npm) | | is-yarn-classic | :x: | boolean | false | Use Yarn Classic (pre-Berry) instead of modern Yarn | @@ -38,14 +54,6 @@ A comprehensive Progressive Web Application deployment workflow supporting S3 st | extra-sync-args | :x: | string | | Additional AWS S3 sync arguments | | **Debug and Control** | | debug | :x: | boolean | false | Enable verbose logging and debug output | -| skip-build | :x: | boolean | false | Skip the build step (use pre-built assets) | -| skip-tests | :x: | boolean | false | Skip test execution | - -#### **Secrets** -| Name | Required | Description | -|------|----------|-------------| -| aws-access-key-id | :white_check_mark: | AWS access key ID | -| aws-secret-access-key | :white_check_mark: | AWS secret access key | #### **Outputs** | Name | Description | @@ -55,21 +63,30 @@ A comprehensive Progressive Web Application deployment workflow supporting S3 st #### **Example Usage** -**Basic Production Deployment:** +**Basic Deployment (Static Credentials):** +```yaml +jobs: + deploy-staging: + uses: aligent/workflows/.github/workflows/pwa-deployment.yml@main + with: + github-environment: Staging + secrets: inherit +``` + +The `Staging` GitHub Environment must have `S3_BUCKET`, `CLOUDFRONT_DISTRIBUTION_ID`, `AWS_ACCESS_KEY_ID`, and `AWS_SECRET_ACCESS_KEY` configured. + +**Basic Deployment (OIDC):** ```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 }} + github-environment: Production + secrets: inherit ``` +The `Production` GitHub Environment must have `S3_BUCKET`, `CLOUDFRONT_DISTRIBUTION_ID`, and `AWS_ROLE_ARN` configured. + **Preview Environment for Pull Requests:** ```yaml jobs: @@ -77,15 +94,11 @@ jobs: 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 + github-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 }} + secrets: inherit ``` **Multi-brand Deployment:** @@ -94,14 +107,10 @@ 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 + github-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 }} + secrets: inherit ``` **Custom Build Configuration:** @@ -110,13 +119,12 @@ jobs: deploy-custom: uses: aligent/workflows/.github/workflows/pwa-deployment.yml@main with: - s3-bucket: my-custom-bucket - cloudfront-distribution-id: E1234567890ABC - environment: staging + github-environment: Staging package-manager: npm build-command: build:staging build-directory: build cloudfront-invalidation-paths: '["/*", "/api/*"]' extra-sync-args: --exclude "*.map" debug: true + secrets: inherit ```