-
Notifications
You must be signed in to change notification settings - Fork 0
161 lines (143 loc) · 6.42 KB
/
Copy pathdeploy.yml
File metadata and controls
161 lines (143 loc) · 6.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
name: Deploy
# Static-site deploy for robosystems-holon-viewer → S3 + CloudFront.
#
# Mirrors the RoboSystems app deploy conventions (OIDC via vars.AWS_ROLE_ARN,
# CloudFormation-managed infra) but collapsed to a single stack because the
# viewer is a pure-static SPA — no App Runner / ECR. A custom domain is optional
# (the VIEWER_DOMAIN repo var): when set, the stack adds an ACM cert + Route53 alias.
#
# PRECONDITIONS (one-time, before the first successful run):
# 1. @robosystems/report-components is PUBLISHED to npm, and this app's
# package.json depends on the published version (e.g. "^0.1.1"), NOT the
# local "file:../robosystems-report-components" link — otherwise `npm ci`
# fails in CI (the sibling repo isn't checked out here).
# 2. Repo variables set: AWS_ROLE_ARN, AWS_ACCOUNT_ID, AWS_REGION.
# 3. The IAM role behind AWS_ROLE_ARN trusts this repo via GitHub OIDC and may
# run CloudFormation + S3 + CloudFront.
#
# Manual only — deployments are an explicit action, never automatic on push.
on:
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy'
required: true
default: 'prod'
type: choice
options:
- prod
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 30 # CloudFront + ACM cert validation can be slow on first create
env:
AWS_REGION: ${{ vars.AWS_REGION || 'us-east-1' }}
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Resolve stack name
# PascalCase, brand-cased, no hyphen — matches the app convention
# (e.g. RoboLedgerAppProd) → RoboSystemsHolonViewer{Prod,Staging,Dev}.
run: |
ENV="${{ inputs.environment }}"
echo "STACK_NAME=RoboSystemsHolonViewer${ENV^}" >> "$GITHUB_ENV"
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
- name: Install dependencies
run: npm ci
- name: Build static site
run: npm run build # vite build → dist/
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
- name: Resolve custom domain
# Domain comes from the VIEWER_DOMAIN repo var (set once, no per-run
# override). Unset → serves on the CloudFront URL.
run: |
DOMAIN="${{ vars.VIEWER_DOMAIN }}"
if [ -z "$DOMAIN" ]; then
echo "VIEWER_DOMAIN unset — serving on the CloudFront URL."
{ echo "DOMAIN_NAME="; echo "HOSTED_ZONE_ID="; } >> "$GITHUB_ENV"
exit 0
fi
ROOT=$(echo "$DOMAIN" | awk -F. '{print $(NF-1)"."$NF}')
ZONE_ID=$(aws route53 list-hosted-zones \
--query "HostedZones[?Name=='${ROOT}.'].Id | [0]" --output text | sed 's#/hostedzone/##')
if [ -z "$ZONE_ID" ] || [ "$ZONE_ID" = "None" ]; then
echo "ERROR: no Route53 hosted zone found for ${ROOT}"; exit 1
fi
echo "Domain ${DOMAIN} → zone ${ROOT} (${ZONE_ID})"
{ echo "DOMAIN_NAME=${DOMAIN}"; echo "HOSTED_ZONE_ID=${ZONE_ID}"; } >> "$GITHUB_ENV"
- name: Deploy CloudFormation stack
run: |
if aws cloudformation describe-stacks --stack-name "$STACK_NAME" 2>&1 | grep -q "does not exist"; then
STACK_ACTION="create-stack"
echo "Creating new stack $STACK_NAME"
else
STACK_ACTION="update-stack"
echo "Updating existing stack $STACK_NAME"
fi
OUTPUT=$(aws cloudformation $STACK_ACTION \
--stack-name "$STACK_NAME" \
--template-body file://cloudformation/template.yaml \
--parameters \
ParameterKey=Environment,ParameterValue=${{ inputs.environment }} \
ParameterKey=Namespace,ParameterValue=${{ vars.AWS_ACCOUNT_ID }} \
ParameterKey=DomainName,ParameterValue=$DOMAIN_NAME \
ParameterKey=HostedZoneId,ParameterValue=$HOSTED_ZONE_ID \
2>&1) || true
if echo "$OUTPUT" | grep -q "No updates are to be performed"; then
echo "Stack already up to date."
elif echo "$OUTPUT" | grep -qi "error"; then
echo "Stack deploy failed: $OUTPUT"
exit 1
else
echo "Stack $STACK_ACTION initiated."
fi
- name: Wait for stack to settle
run: |
aws cloudformation wait stack-create-complete --stack-name "$STACK_NAME" 2>/dev/null \
|| aws cloudformation wait stack-update-complete --stack-name "$STACK_NAME" 2>/dev/null \
|| true
- name: Read stack outputs
id: outputs
run: |
get() {
aws cloudformation describe-stacks --stack-name "$STACK_NAME" \
--query "Stacks[0].Outputs[?OutputKey=='$1'].OutputValue" --output text
}
BUCKET=$(get StaticAssetsBucketName)
DIST=$(get CloudFrontDistributionId)
URL=$(get ViewerUrl)
echo "bucket=$BUCKET" >> "$GITHUB_OUTPUT"
echo "distribution=$DIST" >> "$GITHUB_OUTPUT"
echo "url=$URL" >> "$GITHUB_OUTPUT"
echo "Bucket: $BUCKET · Distribution: $DIST · URL: $URL"
- name: Sync build to S3
run: |
BUCKET="${{ steps.outputs.outputs.bucket }}"
# Hashed assets are immutable → long cache. index.html must revalidate.
aws s3 sync dist/ "s3://$BUCKET" --delete \
--cache-control "public,max-age=31536000,immutable" \
--exclude index.html
aws s3 cp dist/index.html "s3://$BUCKET/index.html" \
--cache-control "no-cache" --content-type "text/html"
- name: Invalidate CloudFront
run: |
aws cloudfront create-invalidation \
--distribution-id "${{ steps.outputs.outputs.distribution }}" \
--paths "/*"
- name: Summary
run: |
echo "## 🚀 Holon Viewer deployed (${{ inputs.environment }})" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "**URL:** ${{ steps.outputs.outputs.url }}" >> "$GITHUB_STEP_SUMMARY"
echo "**Bucket:** \`${{ steps.outputs.outputs.bucket }}\`" >> "$GITHUB_STEP_SUMMARY"
echo "**Distribution:** \`${{ steps.outputs.outputs.distribution }}\`" >> "$GITHUB_STEP_SUMMARY"