gowalk-cicd 1.0.74 → 1.0.75

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
@@ -176,6 +176,12 @@ node /path/to/gowalk-cicd/bin/cli.mjs
176
176
  public HTTPS health probe. `select_certbot_account.sh` chooses one existing
177
177
  Let's Encrypt ACME v2 account deterministically so multi-account hosts stay
178
178
  non-interactive; legacy v1 account directories are not valid candidates.
179
+ `ensure_certificate.sh` preserves failed inventory status and retries only Certbot's
180
+ explicit lock-busy response (six attempts, 75 seconds total backoff). Every retry reads
181
+ inventory before issuance; absence needs recognized complete SAN records or Certbot's explicit
182
+ no-certificate sentinel. Empty/malformed output and arbitrary errors never mean absence. Raw command
183
+ output stays in an owned private temporary directory removed on exit. Do not remove
184
+ Certbot locks, stop other processes, force renewal or bypass the public HTTPS health gate.
179
185
  - The Android action serves two build systems. `android_config.project_kind()`
180
186
  is the only place that decides which; every downstream step branches on the
181
187
  `project_kind` output rather than re-sniffing the repo. Flutter wins the tie
@@ -1 +1 @@
1
- 1.0.74
1
+ 1.0.75
@@ -1 +1 @@
1
- 1.0.74
1
+ 1.0.75
@@ -1 +1 @@
1
- 1.0.74
1
+ 1.0.75
@@ -122,11 +122,12 @@ runs:
122
122
  rsync -az -e "$SSH" "$COMPOSE_FILE" \
123
123
  "${{ inputs.ssh-user }}@${{ inputs.host }}:$DEST/docker-compose.yml"
124
124
  fi
125
- # Ship remote helpers after the application mirror. Both are package
125
+ # Ship remote helpers after the application mirror. They are package
126
126
  # owned and kept out of the consumer's source tree.
127
127
  rsync -az -e "$SSH" \
128
128
  "${{ github.action_path }}/remote_deploy.sh" \
129
129
  "${{ github.action_path }}/select_certbot_account.sh" \
130
+ "${{ github.action_path }}/ensure_certificate.sh" \
130
131
  "${{ inputs.ssh-user }}@${{ inputs.host }}:$DEST/"
131
132
 
132
133
  - name: Deploy on the host
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env bash
2
+ # Sourced by remote_deploy.sh. Only a proven Certbot lock conflict may be retried.
3
+
4
+ certbot_lock_busy() {
5
+ grep -Fxq 'Another instance of Certbot is already running.' \
6
+ "$CERTBOT_WORK/stdout" "$CERTBOT_WORK/stderr"
7
+ }
8
+
9
+ certbot_attempt() {
10
+ local phase="$1" attempt="$2" status
11
+ shift 2
12
+ if certbot "$@" > "$CERTBOT_WORK/stdout" 2> "$CERTBOT_WORK/stderr"; then
13
+ log "certificate phase=$phase attempt=$attempt state=ok exit=0"
14
+ return 0
15
+ else
16
+ status=$?
17
+ fi
18
+ if certbot_lock_busy; then
19
+ log "certificate phase=$phase attempt=$attempt state=lock_busy exit=$status"
20
+ else
21
+ log "certificate phase=$phase attempt=$attempt state=error exit=$status"
22
+ fi
23
+ return "$status"
24
+ }
25
+
26
+ certificate_present() {
27
+ # 0=present, 1=confirmed absent, 2=unrecognized/partial inventory; preserve other awk failures.
28
+ awk -v domain="$1" '
29
+ function valid_san(value, labels, count, i, compressed, groups) {
30
+ if (index(value, ":")) {
31
+ count = split(value, labels, "::"); if (count > 2) return 0
32
+ compressed = count == 2
33
+ if (value ~ /^:[^:]/ || value ~ /[^:]:$/ || value ~ /:::/) return 0
34
+ count = split(value, labels, ":"); groups = 0
35
+ for (i = 1; i <= count; i++) if (labels[i] != "") {
36
+ if (labels[i] !~ /^[0-9A-Fa-f]+$/ || length(labels[i]) > 4) return 0
37
+ groups++
38
+ }
39
+ return compressed ? groups < 8 : groups == 8
40
+ }
41
+ sub(/^[*][.]/, "", value); count = split(value, labels, ".")
42
+ if (count < 2 || length(value) > 253) return 0
43
+ for (i = 1; i <= count; i++)
44
+ if (labels[i] !~ /^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?$/ || length(labels[i]) > 63) return 0
45
+ return 1
46
+ }
47
+ /^[[:space:]-]*$/ { next }
48
+ /^No certificates found[.]$/ { if (mode) bad = 1; mode = 2; next }
49
+ /^Found the following certs:$/ { if (mode) bad = 1; mode = 1; next }
50
+ $1 == "Certificate" && $2 == "Name:" {
51
+ if (mode != 1 || NF < 3 || (records && sans != 1)) bad = 1
52
+ records++; sans = 0; next
53
+ }
54
+ $1 == "Domains:" || $1 == "Identifiers:" {
55
+ if (mode != 1 || !records || sans || NF < 2) bad = 1
56
+ sans++
57
+ for (i = 2; i <= NF; i++) { if (!valid_san($i)) bad = 1; if ($i == domain) found = 1 }
58
+ next
59
+ }
60
+ /^[[:space:]]+(Serial Number|Key Type|Expiry Date|Certificate Path|Private Key Path):/ {
61
+ if (mode != 1 || !records) bad = 1
62
+ next
63
+ }
64
+ { bad = 1 }
65
+ END {
66
+ if (bad || !mode || (mode == 1 && (!records || sans != 1))) exit 2
67
+ exit !found
68
+ }
69
+ ' "$CERTBOT_WORK/stdout"
70
+ }
71
+
72
+ request_certificate() {
73
+ local domain="$1" email="$2" attempt="$3" account
74
+ set -- --nginx -d "$domain" --non-interactive --agree-tos --email "$email" --redirect
75
+ account="$(select_certbot_account)" || return $?
76
+ if [ -n "$account" ]; then
77
+ log "using an existing Let's Encrypt account"
78
+ set -- "$@" --account "$account"
79
+ fi
80
+ certbot_attempt issuance "$attempt" "$@"
81
+ }
82
+
83
+ ensure_certificate() (
84
+ local domain="$1" email="$2" delay attempt=0 status=1 CERTBOT_WORK
85
+ CERTBOT_WORK="$(mktemp -d "${TMPDIR:-/tmp}/gowalk-certbot.XXXXXXXX")" || return $?
86
+ trap 'rm -rf -- "$CERTBOT_WORK"' EXIT
87
+ trap 'exit 130' INT
88
+ trap 'exit 143' TERM
89
+ # Six attempts, 75 seconds total backoff. Commands retain the deployment job's bound.
90
+ for delay in 0 5 10 15 20 25; do
91
+ attempt=$((attempt + 1))
92
+ if [ "$delay" -gt 0 ]; then
93
+ log "certificate state=lock_retry attempt=$attempt backoff_seconds=$delay"
94
+ sleep "$delay" || return $?
95
+ fi
96
+ if certbot_attempt inventory "$attempt" certificates; then
97
+ if certificate_present "$domain"; then
98
+ log "cert for $domain already present"
99
+ return 0
100
+ else
101
+ status=$?
102
+ if [ "$status" -ne 1 ]; then
103
+ log "certificate phase=inventory_parse state=error exit=$status"
104
+ return "$status"
105
+ fi
106
+ fi
107
+ else
108
+ status=$?
109
+ certbot_lock_busy && continue
110
+ return "$status"
111
+ fi
112
+ log "requesting LE cert for $domain"
113
+ if request_certificate "$domain" "$email" "$attempt"; then
114
+ return 0
115
+ else
116
+ status=$?
117
+ certbot_lock_busy || return "$status"
118
+ fi
119
+ # A contender may have created this certificate. Re-read before another issuance.
120
+ done
121
+ log "certificate state=lock_retry_exhausted attempts=$attempt exit=$status"
122
+ return "$status"
123
+ )
@@ -11,6 +11,7 @@ CERT_EMAIL="${4:-admin@gowalk.com}"
11
11
  DIR="/opt/gowalk-backends/$APP"
12
12
  cd "$DIR"
13
13
  source "$DIR/select_certbot_account.sh"
14
+ source "$DIR/ensure_certificate.sh"
14
15
 
15
16
  log() { echo "[deploy $APP] $*"; }
16
17
 
@@ -93,22 +94,13 @@ NGINX
93
94
  nginx -t && systemctl reload nginx
94
95
  fi
95
96
 
96
- # ── Let's Encrypt cert (idempotent; certbot skips if current)
97
- if ! certbot certificates 2>/dev/null | grep -q "Domains: $DOMAIN\b"; then
98
- log "requesting LE cert for $DOMAIN"
99
- CERTBOT_ACCOUNT="$(select_certbot_account)"
100
- CERTBOT_ARGS=()
101
- if [ -n "$CERTBOT_ACCOUNT" ]; then
102
- log "using an existing Let's Encrypt account"
103
- CERTBOT_ARGS+=(--account "$CERTBOT_ACCOUNT")
104
- fi
105
- if ! certbot --nginx -d "$DOMAIN" --non-interactive --agree-tos \
106
- --email "$CERT_EMAIL" --redirect "${CERTBOT_ARGS[@]}"; then
107
- log "ERROR: certbot failed for $DOMAIN"
108
- exit 1
109
- fi
97
+ # ── Let's Encrypt cert: failed inventory is never absence; only lock contention retries.
98
+ if ensure_certificate "$DOMAIN" "$CERT_EMAIL"; then
99
+ log "certificate ready for $DOMAIN"
110
100
  else
111
- log "cert for $DOMAIN already present"
101
+ status=$?
102
+ log "ERROR: certbot failed for $DOMAIN (exit=$status)"
103
+ exit "$status"
112
104
  fi
113
105
 
114
106
  # ── public health-check. This validates DNS, the certificate, nginx routing,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.74",
3
+ "version": "1.0.75",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {