-
Notifications
You must be signed in to change notification settings - Fork 1
186 lines (180 loc) · 6.99 KB
/
deploy-web.yaml
File metadata and controls
186 lines (180 loc) · 6.99 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
name: Deploy Web
on:
push:
branches: [main, develop]
paths:
- "apps/web/**"
- "packages/**"
- "bun.lock"
- "package.json"
- ".github/workflows/deploy-web.yaml"
pull_request:
types: [opened, reopened, synchronize, closed]
paths:
- "apps/web/**"
- "packages/**"
- "bun.lock"
- "package.json"
- ".github/workflows/deploy-web.yaml"
workflow_dispatch:
inputs:
stage:
description: "Stage to deploy (e.g. production, staging, pr-42)"
required: true
type: string
concurrency:
group: deploy-web-${{ github.ref }}
cancel-in-progress: false
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
jobs:
stage:
runs-on: ubuntu-latest
outputs:
stage: ${{ steps.resolve.outputs.stage }}
action: ${{ steps.resolve.outputs.action }}
steps:
- id: resolve
run: |
set -euo pipefail
event="${{ github.event_name }}"
if [[ "$event" == "workflow_dispatch" ]]; then
stage="${{ inputs.stage }}"
action="deploy"
elif [[ "$event" == "push" ]]; then
case "${{ github.ref_name }}" in
main) stage="production" ;;
develop) stage="staging" ;;
*) echo "Unexpected ref ${{ github.ref_name }}" >&2; exit 1 ;;
esac
action="deploy"
elif [[ "$event" == "pull_request" ]]; then
stage="pr-${{ github.event.pull_request.number }}"
if [[ "${{ github.event.action }}" == "closed" ]]; then
action="destroy"
else
action="deploy"
fi
fi
echo "stage=$stage" >> "$GITHUB_OUTPUT"
echo "action=$action" >> "$GITHUB_OUTPUT"
echo "Resolved: $action stage=$stage"
deploy:
needs: stage
if: needs.stage.outputs.action == 'deploy'
runs-on: ubuntu-latest
environment:
name: ${{ needs.stage.outputs.stage }}
url: ${{ steps.deploy.outputs.url }}
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --frozen-lockfile
# Persist alchemy's LocalState (.alchemy/state/<stack>/<stage>/) across
# runners so the destroy job on PR close can find the resources to tear down.
# alchemy currently ships only a filesystem state backend; without this
# cache, every runner starts blind and orphans Workers + Neon projects.
- name: Restore alchemy state
uses: actions/cache@v4
with:
path: apps/web/.alchemy
key: alchemy-state-web-${{ needs.stage.outputs.stage }}
# Invalidate Cloudflare.Vite's content-hash memo on every push.
#
# `Cloudflare.Vite` hashes the Worker's working directory (`apps/web/`)
# to decide whether a rebuild + upload is needed. That scope does *not*
# include workspace dependencies under `packages/**` (`@stackpanel/api`,
# `@stackpanel/auth`, `@stackpanel/db`, …), so a push-to-`main` that
# only touches those packages hashes identically to the last deploy and
# alchemy short-circuits with "no change" — silently skipping the Vite
# build and the Worker upload. The apex then stays frozen on whichever
# commit last happened to touch `apps/web/`.
#
# Stamping a commit SHA (+ refs) file inside `apps/web/` forces a fresh
# memo hash on every run, so production tracks `main` even for diffs
# that live entirely in `packages/**`. The file must NOT be gitignored
# (the default memo `exclude` is the merged gitignore rules, which
# would otherwise filter it out). It's written only in CI — runners
# are ephemeral, so there's nothing to clean up — and contains no
# secrets.
- name: Stamp build info for memo invalidation
working-directory: apps/web
run: |
set -euo pipefail
cat > .build-info <<EOF
sha=${{ github.sha }}
ref=${{ github.ref }}
event=${{ github.event_name }}
run_id=${{ github.run_id }}
EOF
echo "--- .build-info ---"
cat .build-info
- name: Deploy
id: deploy
working-directory: apps/web
env:
STAGE: ${{ needs.stage.outputs.stage }}
# SOPS AGE key used by `loadAppEnv` (sops-age) to decrypt the
# generated per-app payloads. `production` uses the prod key;
# everything else (staging, pr-*, dev) uses the dev key.
# All stages encrypt with the github_actions age recipient
# (SECRETS_AGE_KEY_DEV's pubkey). The previous prod/dev split
# was stale — SECRETS_AGE_KEY_PROD's pubkey isn't on the prod
# payloads, so production deploys failed at decrypt time.
SOPS_AGE_KEY: ${{ secrets.SECRETS_AGE_KEY_DEV }}
run: |
set -euo pipefail
bunx alchemy deploy --stage ${{ needs.stage.outputs.stage }} --yes
- name: Comment preview URL on PR
if: github.event_name == 'pull_request'
uses: marocchino/sticky-pull-request-comment@v2
with:
header: preview-web
message: |
Preview deployed to `${{ needs.stage.outputs.stage }}` — https://${{ needs.stage.outputs.stage }}.stackpanel.com
destroy:
needs: stage
if: needs.stage.outputs.action == 'destroy'
runs-on: ubuntu-latest
permissions:
actions: write # gh cache delete
contents: read
pull-requests: write # sticky comment
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --frozen-lockfile
# Restore-only — we never want the destroy job's post-step to write the
# (now-empty) state back to the cache.
- name: Restore alchemy state
uses: actions/cache/restore@v4
with:
path: apps/web/.alchemy
key: alchemy-state-web-${{ needs.stage.outputs.stage }}
- name: Destroy PR preview
working-directory: apps/web
env:
STAGE: ${{ needs.stage.outputs.stage }}
# PR previews always use the dev key.
SOPS_AGE_KEY: ${{ secrets.SECRETS_AGE_KEY_DEV }}
run: |
set -euo pipefail
bunx alchemy destroy --stage ${{ needs.stage.outputs.stage }} --yes
- name: Delete cached alchemy state
if: always()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh cache delete "alchemy-state-web-${{ needs.stage.outputs.stage }}" \
--repo "${{ github.repository }}" || true
- name: Mark preview comment as torn down
uses: marocchino/sticky-pull-request-comment@v2
with:
header: preview-web
message: |
Preview `${{ needs.stage.outputs.stage }}` has been destroyed.