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 +6 -0
- 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 +2 -1
- package/backend-action/ensure_certificate.sh +123 -0
- package/backend-action/remote_deploy.sh +7 -15
- package/package.json +1 -1
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
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.75
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.75
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
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.
|
|
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
|
|
97
|
-
if
|
|
98
|
-
log "
|
|
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
|
-
|
|
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,
|