-
Notifications
You must be signed in to change notification settings - Fork 29
191 lines (176 loc) · 8.29 KB
/
Copy pathdeploy.yml
File metadata and controls
191 lines (176 loc) · 8.29 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
name: Deploy
# Publishes to R2 and deploys the Worker.
#
# Two things about how this is triggered are load-bearing:
#
# 1. It is NEVER triggered by `on: push`. A push made with the default GITHUB_TOKEN does not
# create a workflow run, so a sync that pushes a commit and expects a push-triggered deploy
# would silently never publish. Syncs call this as a reusable workflow in the SAME run.
#
# 2. Callers MUST pass the SHA they just pushed as `ref`. A reusable workflow inherits the
# caller's github.sha, which for a `schedule` trigger is the head at run-creation time --
# i.e. BEFORE the sync's own commit. Without `ref`, every publish would ship the previous
# cycle's data, forever, off by one, with the next run masking it just enough to be hard to
# notice.
on:
workflow_call:
inputs:
ref:
type: string
required: false
default: ""
environment:
type: string
required: false
default: staging
meta_source:
type: string
required: false
default: deploy
workflow_dispatch:
inputs:
ref:
description: "Commit SHA to publish (defaults to the triggering ref)"
type: string
required: false
environment:
type: choice
options: [staging, production]
default: staging
push:
# master publishes to PRODUCTION, v2 to staging -- see the environment expression below.
# Before this, the only push trigger was v2, so merging a device-registry PR to master
# deployed nothing and, because the five documents are compiled into the Worker bundle, the
# change never reached production at all. The hourly deviceLinks sync deploys production but
# is gated on `changed == 'true'`, so it only ever swept such a merge in by accident.
branches: [master, v2]
concurrency:
group: deploy-${{ inputs.environment || (github.ref == 'refs/heads/master' && 'production' || 'staging') }}
cancel-in-progress: false
permissions:
contents: read
# The failure-reporting step opens or comments on a deduplicated issue, which needs issues:
# write. Without it that step fails with "Resource not accessible by integration" -- so a failed
# deploy produced TWO failures and no notification, which is the one moment the notification
# actually matters.
issues: write
env:
R2_BUCKET: meshtastic-api-v1
AWS_DEFAULT_REGION: auto
# Newer AWS CLI versions send checksum algorithms R2 can reject (InvalidDigest / BadDigest).
# Prophylactic; harmless if the CLI in use never needed it.
AWS_REQUEST_CHECKSUM_CALCULATION: when_required
AWS_RESPONSE_CHECKSUM_VALIDATION: when_required
jobs:
deploy:
runs-on: ubuntu-latest
# A production publish may only ever come from master. Without this, a workflow_dispatch from
# an experimental branch could republish production from an untested tree.
if: >-
(inputs.environment || (github.ref == 'refs/heads/master' && 'production' || 'staging')) != 'production'
|| github.ref == 'refs/heads/master'
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.sha }}
# build-data reads `git log` for deviceLinks' generatedAt, so it needs real history.
fetch-depth: 0
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: pnpm
- run: pnpm install --frozen-lockfile --ignore-scripts=false
- run: pnpm build:data
- run: pnpm build:static
- run: pnpm validate
- name: Configure R2 credentials
run: |
echo "R2_ENDPOINT=https://${{ vars.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com" >> "$GITHUB_ENV"
env:
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
# ---- Upload order is deliberate -----------------------------------------------------
# Binaries first, because the manifests that follow reference them by sha256. Publishing a
# manifest before its binary would leave a window where a client reads a digest for a file
# that is not there yet -- on the one flow that ends in an irreversible bootloader write.
- name: Upload binaries (explicit content-type per class)
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
run: |
set -euo pipefail
# Never rely on inferred content-types: extensionless keys become
# binary/octet-stream, which breaks strict JSON clients such as the c-sharp CLI's
# GetFromJsonAsync. Every class is uploaded with its type stated.
for attempt in 1 2 3; do
aws s3 sync dist-static/ "s3://$R2_BUCKET/v1/" \
--endpoint-url "$R2_ENDPOINT" \
--exclude '*' --include '*.png' \
--content-type image/png --no-progress && break
echo "retry $attempt"; sleep $((attempt * 5))
done
for attempt in 1 2 3; do
aws s3 sync dist-static/ "s3://$R2_BUCKET/v1/" \
--endpoint-url "$R2_ENDPOINT" \
--exclude '*' --include '*.uf2' \
--content-type application/octet-stream --no-progress && break
echo "retry $attempt"; sleep $((attempt * 5))
done
aws s3 cp dist-static/favicon.ico "s3://$R2_BUCKET/v1/favicon.ico" \
--endpoint-url "$R2_ENDPOINT" --content-type image/x-icon --no-progress
- name: Write _meta.json
env:
META_SOURCE: ${{ inputs.meta_source || 'deploy' }}
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
run: |
set -euo pipefail
node tools/write-meta.mjs
aws s3 cp dist-meta/_meta.json "s3://$R2_BUCKET/v1/_meta.json" \
--endpoint-url "$R2_ENDPOINT" --content-type application/json --no-progress
- name: Verify every object landed in R2
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
run: node tools/verify-r2.mjs
# ---- Worker last, once every key it can reference exists ----------------------------
- name: Deploy Worker
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --env ${{ inputs.environment || (github.ref == 'refs/heads/master' && 'production' || 'staging') }}
# ---- Prune last, never before the deploy --------------------------------------------
# Deleting first would pull keys out from under the still-live previous Worker version.
# github/* is excluded: those objects are written by sync-firmware-list.yml and are not
# present in dist-static/, so --delete would remove them on every run.
- name: Prune orphaned objects
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
run: |
aws s3 sync dist-static/ "s3://$R2_BUCKET/v1/" \
--endpoint-url "$R2_ENDPOINT" \
--delete --size-only --no-progress \
--exclude 'github/*' --exclude '_meta.json' --exclude 'manifest.json'
- name: Report failure
if: failure()
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const title = '[pipeline] Deploy failed';
const body = `Run: ${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
// List first and check the call itself succeeded -- a transient API error here would
// otherwise open a fresh duplicate issue on every run.
const { data: open } = await github.rest.issues.listForRepo({
owner, repo, state: 'open', labels: 'pipeline-alert',
});
const existing = open.find(i => i.title === title);
if (existing) {
await github.rest.issues.createComment({ owner, repo, issue_number: existing.number, body });
} else {
await github.rest.issues.create({ owner, repo, title, body, labels: ['pipeline-alert'] });
}