gowalk-cicd 1.0.37 → 1.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +8 -0
- package/README.md +15 -15
- package/action/.daemux-version +1 -1
- package/android-action/.daemux-version +1 -1
- package/backend-action/.daemux-version +1 -1
- package/backend-action/action.yml +14 -0
- package/backend-action/remote_deploy.sh +11 -4
- package/package.json +1 -1
- package/templates/deploy-backend.yml +1 -0
- package/templates/deploy.yml +42 -54
package/CLAUDE.md
CHANGED
|
@@ -25,6 +25,8 @@ gowalk-cicd/ ← repo root IS the gowalk-cicd package
|
|
|
25
25
|
├── bin/cli.mjs ← CLI entrypoint (parses flags, calls runInstall)
|
|
26
26
|
├── src/install.mjs ← copy logic, gitignore-check, summary print
|
|
27
27
|
├── templates/deploy.yml ← dual-platform workflow copied verbatim
|
|
28
|
+
├── backend-action/ ← backend deploy action + remote host script
|
|
29
|
+
├── templates/deploy-backend.yml ← backend workflow copied when compose exists
|
|
28
30
|
├── scripts/auto-version.mjs ← patch-bump-from-npm used by the publish workflow
|
|
29
31
|
├── .github/workflows/publish.yml ← auto-publish to npm on push to main
|
|
30
32
|
├── README.md ← consumer docs
|
|
@@ -96,6 +98,12 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
96
98
|
materializes `ASC_KEY_P8` + its ID/issuer and the Android keystore,
|
|
97
99
|
properties and Play service account into an ephemeral runner checkout.
|
|
98
100
|
Tracked `creds/` files remain a backward-compatible fallback.
|
|
101
|
+
- Backend runtime credentials use the single encrypted `BACKEND_RUNTIME_ENV`
|
|
102
|
+
secret. The action writes it to host-side `.runtime.env` with mode 0600 and
|
|
103
|
+
supplies it to Compose after the generated `.env`; never print its content.
|
|
104
|
+
- The mobile workflow's feature-branch-only `backend-preview` job bootstraps a
|
|
105
|
+
new app before GitHub registers `deploy-backend.yml` on the default branch.
|
|
106
|
+
Default-branch deploys continue through the separate backend workflow.
|
|
99
107
|
- The Android action serves two build systems. `android_config.project_kind()`
|
|
100
108
|
is the only place that decides which; every downstream step branches on the
|
|
101
109
|
`project_kind` output rather than re-sniffing the repo. Flutter wins the tie
|
package/README.md
CHANGED
|
@@ -21,12 +21,6 @@ Writes three things into your repo:
|
|
|
21
21
|
|
|
22
22
|
Re-run the same command anytime to pull the latest version.
|
|
23
23
|
|
|
24
|
-
Flutter map apps may also provide `GOOGLE_MAPS_IOS_KEY` and
|
|
25
|
-
`GOOGLE_MAPS_ANDROID_KEY` as Actions secrets. When present, the workflow writes
|
|
26
|
-
the iOS key to `ios/Flutter/MapsKeys.xcconfig` and the Android key to
|
|
27
|
-
`android/app/src/main/res/values/google_maps_api.xml` as mode-restricted runner
|
|
28
|
-
files. Apps without either secret are unchanged.
|
|
29
|
-
|
|
30
24
|
## iOS credentials
|
|
31
25
|
|
|
32
26
|
1. Create an **App Store Connect API key** with the **App Manager** role
|
|
@@ -67,6 +61,9 @@ Requirements on the consumer repo:
|
|
|
67
61
|
never bake secrets in. A generated `.env` (with `POSTGRES_PASSWORD`) is created
|
|
68
62
|
on the host on first deploy and preserved across deploys (rsync excludes it).
|
|
69
63
|
- Repo **secret** `BACKEND_DEPLOY_SSH_KEY` (a private key with access to the host).
|
|
64
|
+
- Repo **secret** `BACKEND_RUNTIME_ENV` (optional): newline-delimited `KEY=value`
|
|
65
|
+
runtime secrets. The action writes it as mode 0600 on the host and passes it
|
|
66
|
+
to Compose without copying values into the repository or action log.
|
|
70
67
|
- Repo **variables** (optional): `BACKEND_API_DOMAIN` (its DNS A record must point
|
|
71
68
|
at the host for the cert; empty = deploy the container only, skip nginx/cert),
|
|
72
69
|
`BACKEND_APP_NAME` (defaults to the repo name), `BACKEND_HEALTH_PATH`
|
|
@@ -75,6 +72,11 @@ Requirements on the consumer repo:
|
|
|
75
72
|
The action directory is canonical here (like the iOS/Android actions): edit it in
|
|
76
73
|
`backend-action/`, never in a consumer's vendored copy.
|
|
77
74
|
|
|
75
|
+
For a new app whose separate backend workflow is not registered on the default
|
|
76
|
+
branch yet, manually dispatching **Mobile Deploy** on the feature branch runs
|
|
77
|
+
the same backend action as a `backend-preview` job. The job is feature-branch
|
|
78
|
+
only; normal default-branch backend deploys remain in `deploy-backend.yml`.
|
|
79
|
+
|
|
78
80
|
## Android credentials
|
|
79
81
|
|
|
80
82
|
Place these files under `creds/`:
|
|
@@ -190,11 +192,11 @@ sent for review from the Google Play Console UI.
|
|
|
190
192
|
```
|
|
191
193
|
|
|
192
194
|
That fails the whole upload, so the bundle you just built never reaches Play at
|
|
193
|
-
all.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
195
|
+
all. The retry re-uploads with `changesNotSentForReview`, which lands the bundle
|
|
196
|
+
and leaves it waiting, then emits a `::warning::` naming the package. Someone
|
|
197
|
+
then presses **Send changes for review** on that app's Publishing overview page.
|
|
198
|
+
There is no API for that press — it is the one step Google deliberately reserves
|
|
199
|
+
for a human, and no amount of tooling removes it.
|
|
198
200
|
|
|
199
201
|
In the ordinary case the first upload submits normally and the retry is skipped.
|
|
200
202
|
|
|
@@ -210,10 +212,8 @@ long enough outlives one:
|
|
|
210
212
|
|
|
211
213
|
Neither says anything about the build, which is signed and retained by that
|
|
212
214
|
point. The upload step therefore runs with `continue-on-error` and a second
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
third attempt use `changesNotSentForReview`; setting that flag on an ordinary
|
|
216
|
-
app is itself a Play validation error.
|
|
215
|
+
attempt follows when the first fails — a fresh edit succeeds. Both attempts
|
|
216
|
+
failing still fails the job.
|
|
217
217
|
|
|
218
218
|
One caveat this makes explicit: **nothing else should hold an edit open on the
|
|
219
219
|
same package while a deploy runs.** A script that polls `edits.insert` /
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.39
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.39
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.39
|
|
@@ -39,6 +39,13 @@ inputs:
|
|
|
39
39
|
description: Email for the Let's Encrypt registration.
|
|
40
40
|
required: false
|
|
41
41
|
default: "admin@gowalk.com"
|
|
42
|
+
runtime-env:
|
|
43
|
+
description: >
|
|
44
|
+
Optional newline-delimited KEY=value secrets written to a mode-0600
|
|
45
|
+
host-side env file and supplied to Docker Compose. Pass from one
|
|
46
|
+
encrypted secret such as BACKEND_RUNTIME_ENV; never commit the values.
|
|
47
|
+
required: false
|
|
48
|
+
default: ""
|
|
42
49
|
|
|
43
50
|
runs:
|
|
44
51
|
using: composite
|
|
@@ -83,10 +90,17 @@ runs:
|
|
|
83
90
|
|
|
84
91
|
- name: Deploy on the host
|
|
85
92
|
shell: bash
|
|
93
|
+
env:
|
|
94
|
+
RUNTIME_ENV: ${{ inputs.runtime-env }}
|
|
86
95
|
run: |
|
|
87
96
|
set -euo pipefail
|
|
88
97
|
SSH="ssh -i ~/.ssh/backend_deploy -o IdentitiesOnly=yes"
|
|
89
98
|
DEST="/opt/gowalk-backends/${{ inputs.app-name }}"
|
|
99
|
+
if [ -n "${RUNTIME_ENV:-}" ]; then
|
|
100
|
+
printf '%s\n' "$RUNTIME_ENV" | $SSH \
|
|
101
|
+
"${{ inputs.ssh-user }}@${{ inputs.host }}" \
|
|
102
|
+
"umask 077; cat > '$DEST/.runtime.env'"
|
|
103
|
+
fi
|
|
90
104
|
$SSH "${{ inputs.ssh-user }}@${{ inputs.host }}" \
|
|
91
105
|
"bash '$DEST/.remote_deploy.sh' \
|
|
92
106
|
'${{ inputs.app-name }}' '${{ inputs.api-domain }}' \
|
|
@@ -23,15 +23,22 @@ if [ ! -f .env ]; then
|
|
|
23
23
|
chmod 600 .env
|
|
24
24
|
fi
|
|
25
25
|
|
|
26
|
+
COMPOSE_ENV=(--env-file .env)
|
|
27
|
+
if [ -s .runtime.env ]; then
|
|
28
|
+
chmod 600 .runtime.env
|
|
29
|
+
COMPOSE_ENV+=(--env-file .runtime.env)
|
|
30
|
+
fi
|
|
31
|
+
|
|
26
32
|
# ── bring the stack up
|
|
27
33
|
log "docker compose up -d --build"
|
|
28
|
-
docker compose --project-name "$APP"
|
|
34
|
+
docker compose "${COMPOSE_ENV[@]}" --project-name "$APP" \
|
|
35
|
+
up -d --build --remove-orphans
|
|
29
36
|
|
|
30
37
|
# ── discover the host port the service published on 127.0.0.1.
|
|
31
38
|
# The text `ps` PORTS column reliably shows `127.0.0.1:<port>-><target>/tcp`.
|
|
32
39
|
detect_port() {
|
|
33
40
|
# Capture ONLY the published port after 127.0.0.1: (not the IP's own digits).
|
|
34
|
-
docker compose --project-name "$APP" ps 2>/dev/null \
|
|
41
|
+
docker compose "${COMPOSE_ENV[@]}" --project-name "$APP" ps 2>/dev/null \
|
|
35
42
|
| sed -nE 's/.*127\.0\.0\.1:([0-9]+)->.*/\1/p' | head -1
|
|
36
43
|
}
|
|
37
44
|
PORT="$(detect_port || true)"
|
|
@@ -51,8 +58,8 @@ for i in $(seq 1 90); do
|
|
|
51
58
|
done
|
|
52
59
|
if [ -z "$HEALTHY" ]; then
|
|
53
60
|
log "ERROR: container never became healthy on :$PORT$HEALTH"
|
|
54
|
-
docker compose --project-name "$APP" ps
|
|
55
|
-
docker compose --project-name "$APP" logs --tail 60
|
|
61
|
+
docker compose "${COMPOSE_ENV[@]}" --project-name "$APP" ps
|
|
62
|
+
docker compose "${COMPOSE_ENV[@]}" --project-name "$APP" logs --tail 60
|
|
56
63
|
exit 1
|
|
57
64
|
fi
|
|
58
65
|
|
package/package.json
CHANGED
package/templates/deploy.yml
CHANGED
|
@@ -57,15 +57,6 @@ jobs:
|
|
|
57
57
|
else
|
|
58
58
|
echo "enabled=false" >> "$GITHUB_OUTPUT"
|
|
59
59
|
fi
|
|
60
|
-
- name: Materialize restricted iOS Maps key
|
|
61
|
-
if: ${{ steps.flutter.outputs.enabled == 'true' }}
|
|
62
|
-
shell: bash
|
|
63
|
-
env:
|
|
64
|
-
GOOGLE_MAPS_IOS_KEY: ${{ secrets.GOOGLE_MAPS_IOS_KEY }}
|
|
65
|
-
run: |
|
|
66
|
-
[ -n "${GOOGLE_MAPS_IOS_KEY:-}" ] || exit 0
|
|
67
|
-
umask 077
|
|
68
|
-
printf 'GOOGLE_MAPS_IOS_KEY=%s\n' "$GOOGLE_MAPS_IOS_KEY" > ios/Flutter/MapsKeys.xcconfig
|
|
69
60
|
- name: Set up Flutter
|
|
70
61
|
if: ${{ steps.flutter.outputs.enabled == 'true' }}
|
|
71
62
|
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0
|
|
@@ -315,21 +306,6 @@ jobs:
|
|
|
315
306
|
flutter pub get
|
|
316
307
|
flutter gen-l10n
|
|
317
308
|
fi
|
|
318
|
-
- name: Materialize restricted Android Maps key
|
|
319
|
-
if: >-
|
|
320
|
-
${{ steps.mode.outputs.mode == 'local'
|
|
321
|
-
&& steps.flutter.outputs.enabled == 'true' }}
|
|
322
|
-
shell: bash
|
|
323
|
-
env:
|
|
324
|
-
GOOGLE_MAPS_ANDROID_KEY: ${{ secrets.GOOGLE_MAPS_ANDROID_KEY }}
|
|
325
|
-
run: |
|
|
326
|
-
[ -n "${GOOGLE_MAPS_ANDROID_KEY:-}" ] || exit 0
|
|
327
|
-
umask 077
|
|
328
|
-
mkdir -p android/app/src/main/res/values
|
|
329
|
-
printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>' \
|
|
330
|
-
'<resources>' \
|
|
331
|
-
" <string name=\"google_maps_key\" translatable=\"false\">$GOOGLE_MAPS_ANDROID_KEY</string>" \
|
|
332
|
-
'</resources>' > android/app/src/main/res/values/google_maps_api.xml
|
|
333
309
|
- name: Build and sign Android release
|
|
334
310
|
id: android
|
|
335
311
|
if: ${{ steps.mode.outputs.mode == 'local' }}
|
|
@@ -387,39 +363,19 @@ jobs:
|
|
|
387
363
|
${{ vars.GOOGLE_PLAY_STATUS == 'inProgress' && (vars.GOOGLE_PLAY_USER_FRACTION || '0.2') || '' }}
|
|
388
364
|
whatsNewDirectory: ${{ hashFiles('distribution/whatsnew/**') != '' && 'distribution/whatsnew' || '' }}
|
|
389
365
|
|
|
390
|
-
#
|
|
391
|
-
#
|
|
392
|
-
#
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
${{ steps.mode.outputs.mode == 'local'
|
|
398
|
-
&& steps.play.outputs.ready == 'true'
|
|
399
|
-
&& steps.play-upload.outcome == 'failure' }}
|
|
400
|
-
uses: r0adkll/upload-google-play@e738b9dd8f2476ea806d921b64aacd24f34515a5 # v1.1.5
|
|
401
|
-
with:
|
|
402
|
-
serviceAccountJson: ${{ steps.android.outputs.play-service-account }}
|
|
403
|
-
packageName: ${{ steps.android.outputs.package-name }}
|
|
404
|
-
releaseFiles: ${{ steps.android.outputs.bundle-path }}
|
|
405
|
-
tracks: ${{ vars.GOOGLE_PLAY_TRACK || 'internal' }}
|
|
406
|
-
status: ${{ vars.GOOGLE_PLAY_STATUS || 'completed' }}
|
|
407
|
-
userFraction: >-
|
|
408
|
-
${{ vars.GOOGLE_PLAY_STATUS == 'inProgress' && (vars.GOOGLE_PLAY_USER_FRACTION || '0.2') || '' }}
|
|
409
|
-
whatsNewDirectory: >-
|
|
410
|
-
${{ hashFiles('distribution/whatsnew/**') != '' && 'distribution/whatsnew' || '' }}
|
|
411
|
-
|
|
412
|
-
# If the ordinary fresh-edit retry also fails, Play may be requiring the
|
|
413
|
-
# changesNotSentForReview mode used after a policy rejection. A third, fresh edit
|
|
414
|
-
# is intentionally reserved for that case. Setting the flag on the edit-expiry
|
|
415
|
-
# retry is invalid for apps whose changes are submitted automatically.
|
|
366
|
+
# The retry serves two different failures, and both need a fresh edit:
|
|
367
|
+
# * the edit expired mid-upload (a large bundle on a slow runner), and
|
|
368
|
+
# * Play refused to auto-submit: "Changes cannot be sent for review
|
|
369
|
+
# automatically. Please set changesNotSentForReview to true." Play returns
|
|
370
|
+
# this once an app has changes a human must submit — after a policy
|
|
371
|
+
# rejection, for instance — and it fails the whole upload, so the built
|
|
372
|
+
# bundle never lands at all.
|
|
416
373
|
- name: Retry Google Play upload without auto-submit
|
|
417
|
-
id: play-upload-
|
|
374
|
+
id: play-upload-retry
|
|
418
375
|
if: >-
|
|
419
376
|
${{ steps.mode.outputs.mode == 'local'
|
|
420
377
|
&& steps.play.outputs.ready == 'true'
|
|
421
|
-
&& steps.play-upload.outcome == 'failure'
|
|
422
|
-
&& steps.play-upload-fresh-retry.outcome == 'failure' }}
|
|
378
|
+
&& steps.play-upload.outcome == 'failure' }}
|
|
423
379
|
uses: r0adkll/upload-google-play@e738b9dd8f2476ea806d921b64aacd24f34515a5 # v1.1.5
|
|
424
380
|
with:
|
|
425
381
|
serviceAccountJson: ${{ steps.android.outputs.play-service-account }}
|
|
@@ -434,10 +390,42 @@ jobs:
|
|
|
434
390
|
changesNotSentForReview: true
|
|
435
391
|
|
|
436
392
|
- name: Say what still needs a human
|
|
437
|
-
if: ${{ always() && steps.play-upload-
|
|
393
|
+
if: ${{ always() && steps.play-upload-retry.outcome == 'success' }}
|
|
438
394
|
shell: bash
|
|
439
395
|
run: |
|
|
440
396
|
echo "::warning::Play would not accept this release for review automatically, so it was \
|
|
441
397
|
uploaded without being submitted. Open Play Console for \
|
|
442
398
|
${{ steps.android.outputs.package-name }} and press 'Send changes for review' on the \
|
|
443
399
|
Publishing overview page. This is normal after a policy rejection."
|
|
400
|
+
|
|
401
|
+
# GitHub only registers a newly added workflow after it reaches the default
|
|
402
|
+
# branch. A brand-new app's feature branch therefore cannot dispatch the
|
|
403
|
+
# separate Backend Deploy workflow yet. Reuse this already-registered mobile
|
|
404
|
+
# workflow for the first feature-branch deploy; default-branch pushes keep
|
|
405
|
+
# using deploy-backend.yml and never run this job twice.
|
|
406
|
+
backend-preview:
|
|
407
|
+
name: Backend Deploy
|
|
408
|
+
if: >-
|
|
409
|
+
${{ github.event_name == 'workflow_dispatch'
|
|
410
|
+
&& github.ref_name != github.event.repository.default_branch }}
|
|
411
|
+
runs-on: ubuntu-latest
|
|
412
|
+
timeout-minutes: 30
|
|
413
|
+
steps:
|
|
414
|
+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
415
|
+
- id: dir
|
|
416
|
+
shell: bash
|
|
417
|
+
run: |
|
|
418
|
+
if [ -f backend/docker-compose.yml ]; then echo "path=backend" >> "$GITHUB_OUTPUT"
|
|
419
|
+
elif [ -f server/docker-compose.yml ]; then echo "path=server" >> "$GITHUB_OUTPUT"
|
|
420
|
+
else echo "path=" >> "$GITHUB_OUTPUT"; fi
|
|
421
|
+
- name: Deploy backend from feature branch
|
|
422
|
+
if: ${{ steps.dir.outputs.path != '' }}
|
|
423
|
+
uses: ./.github/actions/backend-app
|
|
424
|
+
with:
|
|
425
|
+
ssh-key: ${{ secrets.BACKEND_DEPLOY_SSH_KEY }}
|
|
426
|
+
host: ${{ vars.BACKEND_DEPLOY_HOST || '138.197.36.107' }}
|
|
427
|
+
app-name: ${{ vars.BACKEND_APP_NAME || github.event.repository.name }}
|
|
428
|
+
api-domain: ${{ vars.BACKEND_API_DOMAIN }}
|
|
429
|
+
backend-dir: ${{ steps.dir.outputs.path }}
|
|
430
|
+
health-path: ${{ vars.BACKEND_HEALTH_PATH || '/health' }}
|
|
431
|
+
runtime-env: ${{ secrets.BACKEND_RUNTIME_ENV }}
|