-
Notifications
You must be signed in to change notification settings - Fork 1
310 lines (293 loc) · 14.3 KB
/
Copy pathdeploy.yml
File metadata and controls
310 lines (293 loc) · 14.3 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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
# Deploy WordPress Site (v4)
#
# Previous (v3): a release deploy ran 9 jobs — Lint, Preflight (4 GitHub API
# calls on their own VM), the 5-job build fan-out, a Deploy job that only
# authenticated and set a status, a Continue Deploy job, and a Complete
# Deployment job. Lint → Preflight → Build ran serially, adding ~1 minute of
# dead time before the build started, and a published release was rebuilt from
# scratch even though create-release had already built the identical code.
# ~8 minutes wall, ~14 billable minutes.
#
# Next (v4): two jobs (build + deploy). A normal deploy builds fresh and
# deploys; pass `build_for_release` to also attach the built release.zip to an
# already-published GitHub release — the release-triggered production path
# (release-please publishes the release, this builds, deploys, and archives the
# deployable zip onto it). `release_tag` takes the other path: download the
# release.zip already attached to that release and deploy it without rebuilding,
# used for rollbacks/redeploys. Because production builds its own zip, the
# deploy never waits on a pre-attached asset — the old "no assets to download"
# race (and the draft release that worked around it) is gone. GitHub deployment
# bookkeeping is plain `gh api` steps inside the deploy job. Deploy-time lint is
# gone: code reaching deploy has already passed lint on the PR (and a tagged
# release is immutable). Success now requires a post-deploy health check, not
# just a clean rsync exit.
#
# Rollback: dispatch this workflow with the previous `release_tag`.
#
# Hosts: HOST=pressable|wpengine|cloudways (same composite-action pattern for
# each). The backup-and-continue flow is preserved from v3: with
# `do_backup: true` on Pressable, the deploy triggers an on-demand backup,
# notifies Mantle, parks the GitHub deployment as `queued`, and Mantle later
# dispatches the caller's continue workflow (deploy-continue.yml) to finish
# the deploy from the already-built artifact.
name: Deploy WordPress Site
on:
workflow_call:
inputs:
environment:
description: "The environment to deploy to"
required: false
default: "staging"
type: string
release_tag:
description: "Deploy the release.zip asset attached to this GitHub release instead of building (rollback / redeploy)"
required: false
default: ""
type: string
build_for_release:
description: "Build fresh, deploy, and attach the built release.zip to this already-published GitHub release. Use for release-triggered production deploys."
required: false
default: ""
type: string
do_backup:
description: "Pressable: trigger an on-demand backup, notify Mantle, and pause as 'queued' until the continue workflow is dispatched (v3-compatible)"
required: false
default: false
type: boolean
maintenance_mode:
description: "Enable maintenance mode during the sync (v3 always did; v4 defaults to zero-downtime)"
required: false
default: false
type: boolean
build_commands:
description: "Optional additional shell commands to run during the build (ignored when release_tag is set)"
required: false
default: ""
type: string
post_deploy_command:
description: "Optional shell commands run on the server after deployment"
required: false
default: ""
type: string
health_check:
description: "Fail the deploy if the site does not respond after the sync"
required: false
default: true
type: boolean
secrets:
PACKAGIST_COMPOSER_AUTH_JSON:
required: false
PRESSABLE_API_CLIENT_ID:
required: false
PRESSABLE_API_CLIENT_SECRET:
required: false
SSH_HOST:
required: false
SSH_USER:
required: false
SSH_KEY:
required: false
SSH_PASS:
required: false
MANTLE_API_BEARER:
required: false
jobs:
build:
name: Build
if: ${{ inputs.release_tag == '' }}
uses: ./.github/workflows/build.yml
secrets:
PACKAGIST_COMPOSER_AUTH_JSON: ${{ secrets.PACKAGIST_COMPOSER_AUTH_JSON }}
with:
build_commands: ${{ inputs.build_commands }}
# When set, build.yml attaches the built release.zip to this release as a
# deployable archive (rollback source). Empty for a plain staging build.
release_tag: ${{ inputs.build_for_release }}
deploy:
name: Deploy to ${{ inputs.environment }}
needs: [build]
if: ${{ always() && (needs.build.result == 'success' || needs.build.result == 'skipped') }}
runs-on: ubuntu-latest
timeout-minutes: 15
environment:
name: ${{ inputs.environment }}
url: ${{ vars.SITE_URL }}
concurrency:
group: deploy-${{ github.repository }}-${{ inputs.environment }}
cancel-in-progress: false
permissions:
contents: read
deployments: write
steps:
# v3's Preflight job (deployment bookkeeping on its own VM, billed a
# full minute for ~11s of API calls) is now these two steps.
- name: Create GitHub deployment
id: deployment
env:
GH_TOKEN: ${{ github.token }}
run: |
# required_contexts must be an empty JSON array, so build the payload with jq
id=$(jq -n --arg ref "${{ github.sha }}" --arg env "${{ inputs.environment }}" \
'{ref: $ref, environment: $env, auto_merge: false, required_contexts: [], description: "linchpin/actions v4 deploy"}' \
| gh api "repos/${{ github.repository }}/deployments" --method POST --input - --jq '.id')
echo "id=$id" >> "$GITHUB_OUTPUT"
- name: Set deployment status to in_progress
if: ${{ !(vars.HOST == 'pressable' && inputs.do_backup) }}
env:
GH_TOKEN: ${{ github.token }}
run: |
gh api "repos/${{ github.repository }}/deployments/${{ steps.deployment.outputs.id }}/statuses" \
--method POST \
--field state=in_progress \
--field environment="${{ inputs.environment }}" \
--field environment_url="${{ vars.SITE_URL }}" \
--field log_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
# When parking for a backup, the release stays in the run's artifact
# storage (or on the GitHub release) — the continue run fetches it.
#
# The artifact is a single release.zip (build.yml packaged it once). Land
# it at the workspace root and hand it to the host action as-is — no
# unpack here; the server entrypoint performs the only unzip.
- name: Download built release artifact
if: ${{ inputs.release_tag == '' && !(vars.HOST == 'pressable' && inputs.do_backup) }}
uses: actions/download-artifact@v8
with:
name: release
path: .
- name: Download release.zip from release ${{ inputs.release_tag }}
if: ${{ inputs.release_tag != '' && !(vars.HOST == 'pressable' && inputs.do_backup) }}
env:
GH_TOKEN: ${{ github.token }}
run: |
# Rollback/redeploy path: download the release.zip that a prior deploy
# attached to this release (build.yml's archive step) and ship it
# without rebuilding. The short retry is a safety net for GitHub asset
# eventual-consistency; it caps at ~2 min so a genuinely missing asset
# fails fast instead of billing ~10 min of idle polling.
TAG="${{ inputs.release_tag }}"
REPO="${{ github.repository }}"
for attempt in $(seq 1 8); do
# Leave release.zip at the workspace root: the host action ships it
# straight to the server (no unpack here).
if gh release download "$TAG" --pattern release.zip --repo "$REPO" --clobber 2>/dev/null; then
echo "::notice::Deploying prebuilt asset from release $TAG — no rebuild needed"
exit 0
fi
echo "release.zip not on $TAG yet (attempt $attempt/8) — retrying in 15s…"
sleep 15
done
echo "::error::release.zip never appeared on $TAG after ~2 min. Has this release been deployed yet? (a deploy with build_for_release attaches release.zip)"
exit 1
# Backup-and-continue flow (Pressable, v3-compatible): trigger the
# backup, register the deployment with Mantle, park the deployment as
# `queued`, and end this run. Mantle dispatches the caller's continue
# workflow (-> deploy-continue.yml) once the backup completes; that run
# picks up the `release` artifact from THIS run via workflow_run_id.
- name: Queue deployment pending backup (Mantle continues it)
if: ${{ vars.HOST == 'pressable' && inputs.do_backup }}
env:
GH_TOKEN: ${{ github.token }}
CLIENT_ID: ${{ secrets.PRESSABLE_API_CLIENT_ID }}
CLIENT_SECRET: ${{ secrets.PRESSABLE_API_CLIENT_SECRET }}
MANTLE_API_BEARER: ${{ secrets.MANTLE_API_BEARER }}
run: |
response=$(curl -s --location --request POST 'https://my.pressable.com/auth/token' \
--form 'grant_type="client_credentials"' \
--form "client_id=\"$CLIENT_ID\"" \
--form "client_secret=\"$CLIENT_SECRET\"")
access_token=$(echo "$response" | jq -r '.access_token')
if [ -z "$access_token" ] || [ "$access_token" = "null" ]; then
echo "::error::Failed to authenticate with the Pressable API"
exit 1
fi
echo "::add-mask::$access_token"
curl -s --location --request POST 'https://my.pressable.com/v1/sites/${{ vars.SITE_ID }}/ondemand-backups' \
--header "Authorization: Bearer $access_token" \
--header 'Content-Type: application/json'
echo "::notice::Pressable on-demand backup triggered for site ${{ vars.SITE_ID }}"
curl -s --location --request POST 'https://mantle.linchpin.com/api/v1/deployments/start' \
--header 'Content-Type: application/json' \
--header "Authorization: Bearer $MANTLE_API_BEARER" \
--data-raw '{
"pressable_site_id": "${{ vars.SITE_ID }}",
"site_url": "${{ vars.SITE_URL }}",
"environment": "${{ inputs.environment }}",
"branch": "${{ vars.BRANCH }}",
"deployment_id": ${{ steps.deployment.outputs.id }},
"workflow_run_id": ${{ github.run_id }},
"repository": "${{ github.repository }}"
}'
gh api "repos/${{ github.repository }}/deployments/${{ steps.deployment.outputs.id }}/statuses" \
--method POST \
--field state=queued \
--field environment="${{ inputs.environment }}" \
--field environment_url="${{ vars.SITE_URL }}" \
--field log_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
echo "::notice::Deployment ${{ steps.deployment.outputs.id }} queued — Mantle will dispatch the continue workflow when the backup completes"
- name: Deploy to Pressable
if: ${{ vars.HOST == 'pressable' && !inputs.do_backup }}
uses: linchpin/actions/actions/deploy-pressable@v4
with:
site-id: ${{ vars.SITE_ID }}
site-url: ${{ vars.SITE_URL }}
api-client-id: ${{ secrets.PRESSABLE_API_CLIENT_ID }}
api-client-secret: ${{ secrets.PRESSABLE_API_CLIENT_SECRET }}
ssh-host: ${{ secrets.SSH_HOST }}
ssh-user: ${{ secrets.SSH_USER }}
ssh-key: ${{ secrets.SSH_KEY }}
release-archive: ${{ github.workspace }}/release.zip
maintenance-mode: ${{ inputs.maintenance_mode }}
post-deploy-command: ${{ inputs.post_deploy_command }}
health-check: ${{ inputs.health_check }}
- name: Deploy to WP Engine
if: ${{ vars.HOST == 'wpengine' }}
uses: linchpin/actions/actions/deploy-wpengine@v4
with:
install-name: ${{ vars.INSTALL_NAME }}
site-url: ${{ vars.SITE_URL }}
ssh-key: ${{ secrets.SSH_KEY }}
release-archive: ${{ github.workspace }}/release.zip
post-deploy-command: ${{ inputs.post_deploy_command }}
health-check: ${{ inputs.health_check }}
- name: Deploy to Cloudways
if: ${{ vars.HOST == 'cloudways' }}
uses: linchpin/actions/actions/deploy-cloudways@v4
with:
auth-type: ${{ vars.DEPLOYMENT_AUTH_TYPE || 'key' }}
ssh-host: ${{ secrets.SSH_HOST }}
ssh-user: ${{ secrets.SSH_USER }}
ssh-key: ${{ secrets.SSH_KEY }}
ssh-pass: ${{ secrets.SSH_PASS }}
site-url: ${{ vars.SITE_URL }}
release-archive: ${{ github.workspace }}/release.zip
post-deploy-command: ${{ inputs.post_deploy_command }}
health-check: ${{ inputs.health_check }}
- name: Unsupported host
if: ${{ !contains(fromJSON('["pressable","wpengine","cloudways"]'), vars.HOST) }}
run: |
echo "::error::HOST='${{ vars.HOST }}' is not supported by linchpin/actions v4 (expected pressable, wpengine, or cloudways)"
exit 1
# When the deploy is parked for a backup, the deployment stays `queued`
# and the continue workflow is responsible for the final status.
- name: Mark deployment successful
if: ${{ success() && !(vars.HOST == 'pressable' && inputs.do_backup) }}
env:
GH_TOKEN: ${{ github.token }}
run: |
gh api "repos/${{ github.repository }}/deployments/${{ steps.deployment.outputs.id }}/statuses" \
--method POST \
--field state=success \
--field environment="${{ inputs.environment }}" \
--field environment_url="${{ vars.SITE_URL }}" \
--field log_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
- name: Mark deployment failed
if: ${{ failure() }}
env:
GH_TOKEN: ${{ github.token }}
run: |
gh api "repos/${{ github.repository }}/deployments/${{ steps.deployment.outputs.id }}/statuses" \
--method POST \
--field state=failure \
--field environment="${{ inputs.environment }}" \
--field environment_url="${{ vars.SITE_URL }}" \
--field log_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"