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 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
 
@@ -1 +1 @@
1
- 1.0.80
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.80
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.80
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++) { if (!valid_san($i)) bad = 1; if ($i == domain) found = 1 }
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
- return 0
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
- if [ ! -f "$VHOST" ] || ! grep -q "127.0.0.1:$PORT" "$VHOST"; then
77
- log "writing nginx vhost $VHOST → 127.0.0.1:$PORT"
78
- cat > "$VHOST" <<NGINX
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.80",
3
+ "version": "1.0.82",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -348,7 +348,35 @@ jobs:
348
348
  with:
349
349
  distribution: temurin
350
350
  java-version: ${{ steps.jdk.outputs.version }}
351
- cache: gradle
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