-
Notifications
You must be signed in to change notification settings - Fork 27
225 lines (197 loc) · 8.8 KB
/
deploy-preview.yml
File metadata and controls
225 lines (197 loc) · 8.8 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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
name: Deploy App Preview
on:
# Triggered by harness when issue is complete
workflow_dispatch:
inputs:
issue_number:
description: 'Issue number that was completed'
required: false
default: ''
# Deploy on push to agent-runtime branch
push:
branches:
- 'agent-runtime'
- 'issue-*' # Keep legacy support
paths:
- 'generated-app/**'
env:
AWS_REGION: us-west-2
NODE_VERSION: '20'
# Cancel any in-progress deploy for the same branch
concurrency:
group: preview-deploy-${{ github.ref_name }}
cancel-in-progress: true
permissions:
issues: write
contents: read
jobs:
deploy-preview:
runs-on: ubuntu-latest
outputs:
preview_url: ${{ steps.deploy.outputs.preview_url }}
issue_number: ${{ steps.extract.outputs.issue_number }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Extract issue number
id: extract
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
INPUT_ISSUE_NUMBER: ${{ github.event.inputs.issue_number }}
run: |
# Check if triggered via workflow_dispatch with issue_number input
if [ -n "$INPUT_ISSUE_NUMBER" ]; then
ISSUE_NUMBER="$INPUT_ISSUE_NUMBER"
echo "Issue number from workflow_dispatch: $ISSUE_NUMBER"
else
# Extract from branch name (legacy: issue-27 -> 27)
BRANCH_NAME="${GITHUB_REF_NAME}"
if [[ "$BRANCH_NAME" == issue-* ]]; then
ISSUE_NUMBER="${BRANCH_NAME#issue-}"
else
# agent-runtime branch - find the most recent issue with agent-building or agent-complete label
# Check agent-building first (still in progress)
ISSUE_NUMBER=$(gh issue list --label "agent-building" --state open --limit 1 --json number --jq '.[0].number // empty')
if [ -z "$ISSUE_NUMBER" ]; then
# Check agent-complete (just finished - may have removed agent-building label)
ISSUE_NUMBER=$(gh issue list --label "agent-complete" --state open --limit 1 --json number --jq '.[0].number // empty')
fi
if [ -z "$ISSUE_NUMBER" ]; then
# Fallback: find most recent closed issue with agent-complete label
ISSUE_NUMBER=$(gh issue list --label "agent-complete" --state closed --limit 1 --json number --jq '.[0].number // empty')
fi
if [ -z "$ISSUE_NUMBER" ]; then
echo "No issue found with agent-building label, using 'latest' for S3 path only"
ISSUE_NUMBER="latest"
echo "skip_comment=true" >> $GITHUB_OUTPUT
fi
fi
echo "Issue number from branch: $ISSUE_NUMBER"
fi
echo "issue_number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT
- name: Find generated app directory
id: check
run: |
if [ -d "generated-app" ] && [ -f "generated-app/package.json" ]; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "app_dir=generated-app" >> $GITHUB_OUTPUT
echo "Found app in generated-app/"
else
echo "exists=false" >> $GITHUB_OUTPUT
echo "No generated app found, skipping deployment"
fi
- name: Setup Node.js
if: steps.check.outputs.exists == 'true'
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
if: steps.check.outputs.exists == 'true'
working-directory: ${{ steps.check.outputs.app_dir }}
run: |
# Use npm ci if lockfile exists, otherwise npm install
if [ -f "package-lock.json" ]; then
npm ci
else
npm install
fi
- name: Configure for subdirectory deployment
if: steps.check.outputs.exists == 'true'
working-directory: ${{ steps.check.outputs.app_dir }}
run: |
ISSUE_NUMBER="${{ steps.extract.outputs.issue_number }}"
BASE_PATH="/previews/issue-${ISSUE_NUMBER}/"
# Update vite base path for assets
if [ -f "vite.config.ts" ]; then
sed -i "s|base: '\\./'|base: '${BASE_PATH}'|g" vite.config.ts
echo "Updated vite.config.ts base path to ${BASE_PATH}"
fi
# Update BrowserRouter basename for React Router
if [ -f "src/App.tsx" ]; then
sed -i "s|<BrowserRouter>|<BrowserRouter basename=\"${BASE_PATH}\">|g" src/App.tsx
echo "Updated BrowserRouter basename"
fi
# Also handle main.tsx in case router is there
if [ -f "src/main.tsx" ]; then
sed -i "s|<BrowserRouter>|<BrowserRouter basename=\"${BASE_PATH}\">|g" src/main.tsx
fi
- name: Build application
if: steps.check.outputs.exists == 'true'
working-directory: ${{ steps.check.outputs.app_dir }}
run: npm run build
- name: Configure AWS credentials
if: steps.check.outputs.exists == 'true'
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: ${{ env.AWS_REGION }}
role-to-assume: ${{ secrets.AWS_PREVIEW_DEPLOY_ROLE_ARN }}
role-duration-seconds: 3600
role-skip-session-tagging: true
- name: Deploy to S3
if: steps.check.outputs.exists == 'true'
id: deploy
env:
ISSUE_NUMBER: ${{ steps.extract.outputs.issue_number }}
S3_BUCKET: ${{ vars.PREVIEWS_BUCKET_NAME }}
CLOUDFRONT_DOMAIN: ${{ vars.PREVIEWS_CDN_DOMAIN }}
CLOUDFRONT_DISTRIBUTION_ID: ${{ vars.PREVIEWS_DISTRIBUTION_ID }}
APP_DIR: ${{ steps.check.outputs.app_dir }}
run: |
S3_PATH="s3://${S3_BUCKET}/previews/issue-${ISSUE_NUMBER}/"
echo "Deploying from: ${APP_DIR}/dist/"
echo "Deploying to: $S3_PATH"
# Sync HTML files with no-cache headers
aws s3 sync "${APP_DIR}/dist/" "$S3_PATH" \
--delete \
--cache-control "no-cache, no-store, must-revalidate" \
--content-type "text/html" \
--exclude "*" \
--include "*.html"
# Sync other assets with long cache (content-hashed by Vite)
aws s3 sync "${APP_DIR}/dist/" "$S3_PATH" \
--delete \
--cache-control "public, max-age=31536000, immutable" \
--exclude "*.html"
# Create CloudFront invalidation
echo "Creating CloudFront invalidation..."
INVALIDATION_ID=$(aws cloudfront create-invalidation \
--distribution-id "${CLOUDFRONT_DISTRIBUTION_ID}" \
--paths "/previews/issue-${ISSUE_NUMBER}/*" \
--query 'Invalidation.Id' \
--output text)
echo "Invalidation created: $INVALIDATION_ID"
# Set preview URL output
PREVIEW_URL="https://${CLOUDFRONT_DOMAIN}/previews/issue-${ISSUE_NUMBER}/"
echo "preview_url=$PREVIEW_URL" >> $GITHUB_OUTPUT
echo "Preview URL: $PREVIEW_URL"
- name: Update pinned preview comment on issue
if: steps.check.outputs.exists == 'true' && steps.deploy.outputs.preview_url != '' && steps.extract.outputs.skip_comment != 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_NUMBER: ${{ steps.extract.outputs.issue_number }}
PREVIEW_URL: ${{ steps.deploy.outputs.preview_url }}
COMMIT_SHA: ${{ github.sha }}
run: |
SHORT_SHA="${COMMIT_SHA:0:7}"
COMMIT_URL="https://github.com/${GITHUB_REPOSITORY}/commit/${COMMIT_SHA}"
TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
# Build comment body (using printf to avoid YAML issues)
BODY=$(printf '## Live Preview\n\n| Field | Value |\n|-------|-------|\n| **URL** | [%s](%s) |\n| **Last Deploy** | %s |\n| **Commit** | [`%s`](%s) |\n\n---\n*Auto-updates on each push to `agent-runtime` branch*' \
"$PREVIEW_URL" "$PREVIEW_URL" "$TIMESTAMP" "$SHORT_SHA" "$COMMIT_URL")
# Find existing preview comment
COMMENT_ID=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${ISSUE_NUMBER}/comments" \
--jq '.[] | select(.body | contains("Live Preview")) | .id' | head -1)
if [ -n "$COMMENT_ID" ]; then
echo "Updating existing comment: $COMMENT_ID"
gh api "repos/${GITHUB_REPOSITORY}/issues/comments/${COMMENT_ID}" \
-X PATCH \
-f body="$BODY"
else
echo "Creating new preview comment"
gh issue comment "${ISSUE_NUMBER}" \
--repo "${GITHUB_REPOSITORY}" \
--body "$BODY"
fi
echo "Posted preview link to issue #${ISSUE_NUMBER}"