gowalk-cicd 1.0.80 → 1.0.82
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 +7 -0
- package/README.md +2 -0
- package/action/.daemux-version +1 -1
- package/action/scripts/cache_scope.cjs +2 -0
- package/android-action/.daemux-version +1 -1
- package/android-action/scripts/cache_scope.cjs +2 -0
- package/backend-action/.daemux-version +1 -1
- package/backend-action/action.yml +1 -0
- package/backend-action/configure_vhost.py +64 -0
- package/backend-action/ensure_certificate.sh +22 -5
- package/backend-action/remote_deploy.sh +3 -18
- package/package.json +1 -1
- package/templates/deploy.yml +45 -1
package/CLAUDE.md
CHANGED
|
@@ -158,6 +158,10 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
158
158
|
errors still fail the job. Never use `continue-on-error` or a timeout around all SDK setup
|
|
159
159
|
to hide an optional cache failure. Do not alter cache keys or disable caches as part of
|
|
160
160
|
this timeout handling.
|
|
161
|
+
- Gradle caches use explicit checksum-pinned restore/save actions, each optional and bounded
|
|
162
|
+
to two minutes. Preserve setup-java's key inputs and absolute cache paths to reuse existing
|
|
163
|
+
caches. Java setup, builds and store uploads remain mandatory; save runs after delivery.
|
|
164
|
+
The cache-only preloader also admits the pinned `restore-only`/`save-only` entrypoints.
|
|
161
165
|
- GitHub artifact uploads have their own checksum-pinned `artifact_env.cjs` process scope.
|
|
162
166
|
It reuses the cache transport's approved HTTPS request guard, then removes inherited proxy
|
|
163
167
|
variables only inside the official `upload-artifact` process. Unknown programs/origins refuse;
|
|
@@ -189,6 +193,9 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
189
193
|
no-certificate sentinel. Empty/malformed output and arbitrary errors never mean absence. Raw command
|
|
190
194
|
output stays in an owned private temporary directory removed on exit. Do not remove
|
|
191
195
|
Certbot locks, stop other processes, force renewal or bypass the public HTTPS health gate.
|
|
196
|
+
Docker port changes update only the managed nginx upstream, preserving existing TLS directives.
|
|
197
|
+
Existing certificate inventory is followed by domain-scoped `certbot install`, so a legacy
|
|
198
|
+
HTTP-only vhost also recovers without reissuance. Installation shares the bounded lock-only retry.
|
|
192
199
|
- The Android action serves two build systems. `android_config.project_kind()`
|
|
193
200
|
is the only place that decides which; every downstream step branches on the
|
|
194
201
|
`project_kind` output rather than re-sniffing the repo. Flutter wins the tie
|
package/README.md
CHANGED
|
@@ -97,6 +97,8 @@ it rsyncs the backend dir to `/opt/gowalk-backends/<app>/`, runs
|
|
|
97
97
|
wires an nginx vhost + Let's Encrypt cert for the API domain, and health-checks.
|
|
98
98
|
Hosts with several existing Let's Encrypt accounts select one deterministically,
|
|
99
99
|
and a domain deploy fails unless its public HTTPS certificate and health route validate.
|
|
100
|
+
Changing a container's published port preserves its existing HTTPS configuration. An existing
|
|
101
|
+
certificate is installed for the requested domain on redeploy, repairing older HTTP-only vhosts too.
|
|
100
102
|
|
|
101
103
|
Requirements on the consumer repo:
|
|
102
104
|
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.82
|
|
@@ -5,6 +5,8 @@ const {basename, dirname, join} = require('node:path');
|
|
|
5
5
|
const hashes = new Map([
|
|
6
6
|
['restore', 'a49301428b4c5477522c00f6464566457ed327d64ae5f80f8c12e762fb2ea9a2'],
|
|
7
7
|
['save', 'b969065795aaf5c6d20ba158e910be5d353eefb02eb2785263eb32e13090c066'],
|
|
8
|
+
['restore-only', '6255afaa3956351b8cfefc1e82f026b4db418a10678a8eaddf3d6c55f81744de'],
|
|
9
|
+
['save-only', '7fb63f90f06ce6a10f39d40a113f99791bffdedad5394cdad5b4dfaa644559cb'],
|
|
8
10
|
]);
|
|
9
11
|
const artifactHash = '168c44946cba03564808c19b83a72601e6d1d6f57081dbd2d6e2ea1cedc522f5';
|
|
10
12
|
const proxyKeys = ['HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy', 'ALL_PROXY', 'all_proxy'];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.82
|
|
@@ -5,6 +5,8 @@ const {basename, dirname, join} = require('node:path');
|
|
|
5
5
|
const hashes = new Map([
|
|
6
6
|
['restore', 'a49301428b4c5477522c00f6464566457ed327d64ae5f80f8c12e762fb2ea9a2'],
|
|
7
7
|
['save', 'b969065795aaf5c6d20ba158e910be5d353eefb02eb2785263eb32e13090c066'],
|
|
8
|
+
['restore-only', '6255afaa3956351b8cfefc1e82f026b4db418a10678a8eaddf3d6c55f81744de'],
|
|
9
|
+
['save-only', '7fb63f90f06ce6a10f39d40a113f99791bffdedad5394cdad5b4dfaa644559cb'],
|
|
8
10
|
]);
|
|
9
11
|
const artifactHash = '168c44946cba03564808c19b83a72601e6d1d6f57081dbd2d6e2ea1cedc522f5';
|
|
10
12
|
const proxyKeys = ['HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy', 'ALL_PROXY', 'all_proxy'];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.82
|
|
@@ -128,6 +128,7 @@ runs:
|
|
|
128
128
|
"${{ github.action_path }}/remote_deploy.sh" \
|
|
129
129
|
"${{ github.action_path }}/select_certbot_account.sh" \
|
|
130
130
|
"${{ github.action_path }}/ensure_certificate.sh" \
|
|
131
|
+
"${{ github.action_path }}/configure_vhost.py" \
|
|
131
132
|
"${{ inputs.ssh-user }}@${{ inputs.host }}:$DEST/"
|
|
132
133
|
|
|
133
134
|
- name: Deploy on the host
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Update one managed nginx upstream without discarding Certbot's HTTPS configuration."""
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import re
|
|
5
|
+
import stat
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def content(previous, domain, port):
|
|
11
|
+
if not re.fullmatch(r"[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?", domain) or not 0 < port < 65536:
|
|
12
|
+
raise ValueError("invalid domain or port")
|
|
13
|
+
if previous:
|
|
14
|
+
names = re.findall(r"(?m)^\s*server_name\s+([^;]+);", previous)
|
|
15
|
+
if not names or any(name.strip() != domain for name in names):
|
|
16
|
+
raise ValueError("vhost belongs to another domain")
|
|
17
|
+
pattern = r"(?m)^(\s*proxy_pass\s+http://127\.0\.0\.1:)\d+(;[^\n]*$)"
|
|
18
|
+
result, count = re.subn(pattern, lambda match: f"{match[1]}{port}{match[2]}", previous)
|
|
19
|
+
if count != 1:
|
|
20
|
+
raise ValueError("managed upstream is not unique")
|
|
21
|
+
return result
|
|
22
|
+
return f"""server {{
|
|
23
|
+
listen 80;
|
|
24
|
+
server_name {domain};
|
|
25
|
+
client_max_body_size 100m;
|
|
26
|
+
location / {{
|
|
27
|
+
proxy_pass http://127.0.0.1:{port};
|
|
28
|
+
proxy_http_version 1.1;
|
|
29
|
+
proxy_set_header Host $host;
|
|
30
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
31
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
32
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
33
|
+
proxy_read_timeout 120s;
|
|
34
|
+
}}
|
|
35
|
+
}}
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def configure(path, domain, port):
|
|
40
|
+
if path.is_symlink():
|
|
41
|
+
raise ValueError("managed vhost must not be a symlink")
|
|
42
|
+
previous = path.read_text() if path.exists() else ""
|
|
43
|
+
updated = content(previous, domain, port)
|
|
44
|
+
if previous == updated:
|
|
45
|
+
return "unchanged"
|
|
46
|
+
mode = stat.S_IMODE(path.stat().st_mode) if path.exists() else 0o644
|
|
47
|
+
fd, temporary = tempfile.mkstemp(prefix=path.name + ".", dir=path.parent)
|
|
48
|
+
try:
|
|
49
|
+
with os.fdopen(fd, "w") as output:
|
|
50
|
+
os.fchmod(output.fileno(), mode)
|
|
51
|
+
output.write(updated)
|
|
52
|
+
output.flush()
|
|
53
|
+
os.fsync(output.fileno())
|
|
54
|
+
os.replace(temporary, path)
|
|
55
|
+
finally:
|
|
56
|
+
Path(temporary).unlink(missing_ok=True)
|
|
57
|
+
return "changed"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
try:
|
|
62
|
+
print(configure(Path(sys.argv[1]), sys.argv[2], int(sys.argv[3])))
|
|
63
|
+
except (OSError, ValueError):
|
|
64
|
+
sys.exit("Cannot update the managed nginx vhost; its domain and upstream must be unambiguous")
|
|
@@ -25,7 +25,7 @@ certbot_attempt() {
|
|
|
25
25
|
|
|
26
26
|
certificate_present() {
|
|
27
27
|
# 0=present, 1=confirmed absent, 2=unrecognized/partial inventory; preserve other awk failures.
|
|
28
|
-
awk -v domain="$1" '
|
|
28
|
+
awk -v domain="$1" -v emit_name="${2:-}" '
|
|
29
29
|
function valid_san(value, labels, count, i, compressed, groups) {
|
|
30
30
|
if (index(value, ":")) {
|
|
31
31
|
count = split(value, labels, "::"); if (count > 2) return 0
|
|
@@ -49,12 +49,20 @@ certificate_present() {
|
|
|
49
49
|
/^Found the following certs:$/ { if (mode) bad = 1; mode = 1; next }
|
|
50
50
|
$1 == "Certificate" && $2 == "Name:" {
|
|
51
51
|
if (mode != 1 || NF < 3 || (records && sans != 1)) bad = 1
|
|
52
|
+
name = $0; sub(/^[[:space:]]*Certificate Name:[[:space:]]*/, "", name)
|
|
53
|
+
if (index(name, "/") || index(name, "\\") || name == "." || name == "..") bad = 1
|
|
52
54
|
records++; sans = 0; next
|
|
53
55
|
}
|
|
54
56
|
$1 == "Domains:" || $1 == "Identifiers:" {
|
|
55
57
|
if (mode != 1 || !records || sans || NF < 2) bad = 1
|
|
56
58
|
sans++
|
|
57
|
-
for (i = 2; i <= NF; i++) {
|
|
59
|
+
for (i = 2; i <= NF; i++) {
|
|
60
|
+
if (!valid_san($i)) bad = 1
|
|
61
|
+
if ($i == domain) {
|
|
62
|
+
if (!found || name == domain || (selected != domain && name < selected)) selected = name
|
|
63
|
+
found = 1
|
|
64
|
+
}
|
|
65
|
+
}
|
|
58
66
|
next
|
|
59
67
|
}
|
|
60
68
|
/^[[:space:]]+(Serial Number|Key Type|Expiry Date|Certificate Path|Private Key Path):/ {
|
|
@@ -64,6 +72,7 @@ certificate_present() {
|
|
|
64
72
|
{ bad = 1 }
|
|
65
73
|
END {
|
|
66
74
|
if (bad || !mode || (mode == 1 && (!records || sans != 1))) exit 2
|
|
75
|
+
if (found && emit_name) print selected
|
|
67
76
|
exit !found
|
|
68
77
|
}
|
|
69
78
|
' "$CERTBOT_WORK/stdout"
|
|
@@ -81,7 +90,7 @@ request_certificate() {
|
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
ensure_certificate() (
|
|
84
|
-
local domain="$1" email="$2" delay attempt=0 status=1 CERTBOT_WORK
|
|
93
|
+
local domain="$1" email="$2" delay name attempt=0 status=1 CERTBOT_WORK
|
|
85
94
|
CERTBOT_WORK="$(mktemp -d "${TMPDIR:-/tmp}/gowalk-certbot.XXXXXXXX")" || return $?
|
|
86
95
|
trap 'rm -rf -- "$CERTBOT_WORK"' EXIT
|
|
87
96
|
trap 'exit 130' INT
|
|
@@ -94,9 +103,17 @@ ensure_certificate() (
|
|
|
94
103
|
sleep "$delay" || return $?
|
|
95
104
|
fi
|
|
96
105
|
if certbot_attempt inventory "$attempt" certificates; then
|
|
97
|
-
if certificate_present "$domain"; then
|
|
106
|
+
if name="$(certificate_present "$domain" name)"; then
|
|
98
107
|
log "cert for $domain already present"
|
|
99
|
-
|
|
108
|
+
# Inventory proves possession, not nginx installation. Repair an HTTP-only legacy vhost too.
|
|
109
|
+
if certbot_attempt installation "$attempt" install --nginx --cert-name "$name" \
|
|
110
|
+
-d "$domain" --non-interactive --redirect; then
|
|
111
|
+
return 0
|
|
112
|
+
else
|
|
113
|
+
status=$?
|
|
114
|
+
certbot_lock_busy && continue
|
|
115
|
+
return "$status"
|
|
116
|
+
fi
|
|
100
117
|
else
|
|
101
118
|
status=$?
|
|
102
119
|
if [ "$status" -ne 1 ]; then
|
|
@@ -73,24 +73,9 @@ fi
|
|
|
73
73
|
|
|
74
74
|
# ── nginx vhost → proxy the domain to the published port
|
|
75
75
|
VHOST="/etc/nginx/conf.d/backend-$APP.conf"
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
server {
|
|
80
|
-
listen 80;
|
|
81
|
-
server_name $DOMAIN;
|
|
82
|
-
client_max_body_size 100m;
|
|
83
|
-
location / {
|
|
84
|
-
proxy_pass http://127.0.0.1:$PORT;
|
|
85
|
-
proxy_http_version 1.1;
|
|
86
|
-
proxy_set_header Host \$host;
|
|
87
|
-
proxy_set_header X-Real-IP \$remote_addr;
|
|
88
|
-
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
|
89
|
-
proxy_set_header X-Forwarded-Proto \$scheme;
|
|
90
|
-
proxy_read_timeout 120s;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
NGINX
|
|
76
|
+
VHOST_STATE="$(python3 "$DIR/configure_vhost.py" "$VHOST" "$DOMAIN" "$PORT")"
|
|
77
|
+
if [ "$VHOST_STATE" = changed ]; then
|
|
78
|
+
log "updated nginx upstream for $DOMAIN → 127.0.0.1:$PORT"
|
|
94
79
|
nginx -t && systemctl reload nginx
|
|
95
80
|
fi
|
|
96
81
|
|
package/package.json
CHANGED
package/templates/deploy.yml
CHANGED
|
@@ -348,7 +348,35 @@ jobs:
|
|
|
348
348
|
with:
|
|
349
349
|
distribution: temurin
|
|
350
350
|
java-version: ${{ steps.jdk.outputs.version }}
|
|
351
|
-
|
|
351
|
+
# Keep Java setup mandatory; optional caches have no unbounded post step.
|
|
352
|
+
- name: Retain Gradle cache scope
|
|
353
|
+
id: gradle_scope
|
|
354
|
+
if: ${{ steps.mode.outputs.mode == 'local' }}
|
|
355
|
+
continue-on-error: true
|
|
356
|
+
shell: bash
|
|
357
|
+
run: |
|
|
358
|
+
node .github/actions/android-app/scripts/cache_scope.cjs --snapshot
|
|
359
|
+
node - <<'NODE'
|
|
360
|
+
require('fs').appendFileSync(process.env.GITHUB_OUTPUT,
|
|
361
|
+
`home=${require('os').homedir()}\narch=${process.arch}\n`);
|
|
362
|
+
NODE
|
|
363
|
+
- name: Restore Gradle dependencies
|
|
364
|
+
id: gradle_cache
|
|
365
|
+
if: ${{ steps.mode.outputs.mode == 'local' && steps.gradle_scope.outcome == 'success' }}
|
|
366
|
+
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25
|
|
367
|
+
continue-on-error: true
|
|
368
|
+
timeout-minutes: 2
|
|
369
|
+
env:
|
|
370
|
+
NODE_OPTIONS: --require "${{ steps.gradle_scope.outputs.preloader }}"
|
|
371
|
+
SEGMENT_DOWNLOAD_TIMEOUT_MINS: '1'
|
|
372
|
+
with:
|
|
373
|
+
path: |
|
|
374
|
+
${{ steps.gradle_scope.outputs.home }}/.gradle/caches
|
|
375
|
+
${{ steps.gradle_scope.outputs.home }}/.gradle/wrapper
|
|
376
|
+
key: >-
|
|
377
|
+
${{ format('setup-java-{0}-{1}-gradle-{2}', runner.os, steps.gradle_scope.outputs.arch,
|
|
378
|
+
hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'buildSrc/**/Versions.kt',
|
|
379
|
+
'buildSrc/**/Dependencies.kt', 'gradle/*.versions.toml', '**/versions.properties')) }}
|
|
352
380
|
- name: Set up Flutter
|
|
353
381
|
if: ${{ steps.mode.outputs.mode == 'local' && steps.flutter.outputs.enabled == 'true' }}
|
|
354
382
|
uses: ./.github/actions/android-app/flutter-setup
|
|
@@ -518,6 +546,22 @@ jobs:
|
|
|
518
546
|
run: |
|
|
519
547
|
echo "::notice title=play_review_pending::{\"schema\":\"gowalk-cicd/play-review-pending.v1\",\"package\":\"${PACKAGE}\",\"track\":\"${TRACK}\",\"version_code\":\"${VERSION_CODE}\"}"
|
|
520
548
|
|
|
549
|
+
- name: Save Gradle dependencies
|
|
550
|
+
if: >-
|
|
551
|
+
${{ success() && steps.mode.outputs.mode == 'local'
|
|
552
|
+
&& steps.gradle_cache.outputs.cache-primary-key != ''
|
|
553
|
+
&& steps.gradle_cache.outputs.cache-hit != 'true' }}
|
|
554
|
+
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25
|
|
555
|
+
continue-on-error: true
|
|
556
|
+
timeout-minutes: 2
|
|
557
|
+
env:
|
|
558
|
+
NODE_OPTIONS: --require "${{ steps.gradle_scope.outputs.preloader }}"
|
|
559
|
+
with:
|
|
560
|
+
path: |
|
|
561
|
+
${{ steps.gradle_scope.outputs.home }}/.gradle/caches
|
|
562
|
+
${{ steps.gradle_scope.outputs.home }}/.gradle/wrapper
|
|
563
|
+
key: ${{ steps.gradle_cache.outputs.cache-primary-key }}
|
|
564
|
+
|
|
521
565
|
# GitHub only registers a newly added workflow after it reaches the default
|
|
522
566
|
# branch. A brand-new app's feature branch therefore cannot dispatch the
|
|
523
567
|
# separate Backend Deploy workflow yet. Reuse this already-registered mobile
|