1+ name : NPM FRONTEND DEPLOYMENT
2+
3+ on :
4+ workflow_call :
5+ inputs :
6+ node_version :
7+ description : ' Node.js version to use for the build (default: lts/*)'
8+ default : ' lts/*'
9+ required : false
10+ type : string
11+ aws_default_region :
12+ description : ' AWS region to use for deployment (default: eu-central-1)'
13+ default : ' eu-central-1'
14+ required : false
15+ type : string
16+ aws_oidc_role_arn :
17+ description : ' AWS OIDC IAM role ARN to assume for deployment (e.g. arn:aws:iam::111111111111:role/oidc-deployment-role)'
18+ required : true
19+ type : string
20+ domain_name :
21+ description : ' Domain name used for the frontend application (e.g. docs.nuvibit.com)'
22+ required : true
23+ type : string
24+ s3_deployment_dir :
25+ description : ' S3 directory where the build artifacts will be uploaded (default: build)'
26+ default : ' build'
27+ required : false
28+ type : string
29+ local_build_dir :
30+ description : ' Local directory where the build artifacts are located (default: build)'
31+ default : ' build'
32+ required : false
33+ type : string
34+ cloudfront_invalidate_paths :
35+ description : ' Paths to invalidate in CloudFront after deployment (ALL, UPDATED, NONE)'
36+ required : true
37+ type : string
38+ # ALL: Invalidate all paths (default)
39+ # UPDATED: Invalidate only paths that were updated
40+ # NONE: Do not invalidate any paths
41+ default : ' ALL'
42+
43+ jobs :
44+ npm-frontend-deploy :
45+ name : Deploy Frontend
46+ runs-on : ubuntu-latest
47+ permissions :
48+ id-token : write
49+ contents : read
50+ steps :
51+ - uses : actions/checkout@v4
52+ with :
53+ fetch-depth : 0
54+
55+ - uses : actions/setup-node@v4
56+ with :
57+ node-version : ${{ inputs.node_version }}
58+
59+ - name : Install dependencies
60+ run : npm install
61+
62+ - name : Build website
63+ run : npm run build
64+
65+ - name : Configure AWS credentials
66+ uses : aws-actions/configure-aws-credentials@v4
67+ with :
68+ role-to-assume : ${{ inputs.aws_oidc_role_arn }}
69+ aws-region : ${{ inputs.aws_default_region }}
70+
71+ - name : Upload to S3 and track changed files
72+ id : sync
73+ run : |
74+ # First identify which files will be synced
75+ SYNC_OUTPUT=$(aws s3 sync ./${{ inputs.local_build_dir }} s3://${{ inputs.domain_name }}/${{ inputs.s3_deployment_dir }} --dryrun)
76+ echo "Found changes to sync."
77+
78+ # Extract file paths that will be uploaded/updated
79+ PATHS_TO_INVALIDATE=$(echo "$SYNC_OUTPUT" | grep -E 'upload:|update:' | sed -E 's/^\(dryrun\) (upload:|update:) (.*) to ([^ ]+).*/\3/' | sed 's|s3://${{ inputs.domain_name }}/${{ inputs.s3_deployment_dir }}||')
80+ echo "Paths to be updated:"
81+ echo "$PATHS_TO_INVALIDATE"
82+
83+ # Save paths for invalidation, adjust format for JSON array
84+ if [ -n "$PATHS_TO_INVALIDATE" ]; then
85+ PATHS_JSON=$(echo "$PATHS_TO_INVALIDATE" | sed 's/^/"/' | sed 's/$/"/' | paste -sd, -)
86+ echo "paths_json=[$PATHS_JSON]" >> $GITHUB_OUTPUT
87+ echo "has_changes=true" >> $GITHUB_OUTPUT
88+ else
89+ echo "No changes to invalidate"
90+ echo "has_changes=false" >> $GITHUB_OUTPUT
91+ fi
92+
93+ # Perform the actual sync
94+ aws s3 sync ./${{ inputs.local_build_dir }} s3://${{ inputs.domain_name }}/${{ inputs.s3_deployment_dir }}
95+
96+ - name : Find CloudFront distribution ID
97+ id : cloudfront
98+ if : steps.sync.outputs.has_changes == 'true'
99+ run : |
100+ # Find the CloudFront distribution that has our S3 bucket as origin
101+ DISTRIBUTION_ID=$(aws cloudfront list-distributions --query "DistributionList.Items[?contains(Origins.Items[].DomainName, '${{ inputs.domain_name }}.s3.${{ inputs.aws_default_region }}.amazonaws.com')].Id" --output text)
102+
103+ if [ -z "$DISTRIBUTION_ID" ]; then
104+ echo "::error::Could not find CloudFront distribution for ${{ inputs.domain_name }}"
105+ exit 1
106+ fi
107+
108+ echo "Found CloudFront distribution: $DISTRIBUTION_ID"
109+ echo "distribution_id=$DISTRIBUTION_ID" >> $GITHUB_OUTPUT
110+
111+ - name : Invalidate CloudFront cache for ALL PATHS
112+ if : steps.sync.outputs.has_changes == 'true' && inputs.cloudfront_invalidate_paths == 'ALL'
113+ run : |
114+ # Create CloudFront invalidation for all paths
115+ DISTRIBUTION_ID=${{ steps.cloudfront.outputs.distribution_id }}
116+
117+ echo "Creating CloudFront invalidation for paths: /*"
118+ aws cloudfront create-invalidation --distribution-id $DISTRIBUTION_ID --paths "/*"
119+
120+ - name : Invalidate CloudFront cache for UPDATED PATHS
121+ if : steps.sync.outputs.has_changes == 'true' && inputs.cloudfront_invalidate_paths == 'UPDATED'
122+ run : |
123+ # Create CloudFront invalidation for the paths that were updated
124+ PATHS=${{ toJSON(steps.sync.outputs.paths_json) }}
125+ DISTRIBUTION_ID=${{ steps.cloudfront.outputs.distribution_id }}
126+
127+ echo "Creating Invalidation batch file"
128+ echo "{\"Paths\" : {\"Quantity\": $(echo $PATHS | jq length), \"Items\": $PATHS}, \"CallerReference\": \"$(date +%s)\"}" > inv-batch.json
129+ echo "Creating CloudFront invalidation for paths : $PATHS"
130+ aws cloudfront create-invalidation --distribution-id $DISTRIBUTION_ID --invalidation-batch file://inv-batch.json
0 commit comments