gowalk-cicd 1.0.21 → 1.0.23
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/README.md +42 -0
- package/action/.daemux-version +1 -1
- package/android-action/.daemux-version +1 -1
- package/backend-action/.daemux-version +1 -0
- package/backend-action/action.yml +98 -0
- package/backend-action/remote_deploy.sh +103 -0
- package/package.json +4 -1
- package/src/install.mjs +38 -0
- package/templates/deploy-backend.yml +41 -0
- package/templates/deploy.yml +32 -0
package/README.md
CHANGED
|
@@ -39,6 +39,36 @@ Re-run the same command anytime to pull the latest version.
|
|
|
39
39
|
5. The existing metadata-rich form
|
|
40
40
|
`*(key_id_<KEY_ID>_issuer_<UUID>_vendor_id_<ID>).p8` is also accepted.
|
|
41
41
|
|
|
42
|
+
## Backend deploy (Python + Postgres, Docker)
|
|
43
|
+
|
|
44
|
+
When a repo carries a Dockerized backend — a `backend/` (or `server/`) directory
|
|
45
|
+
with a `docker-compose.yml` — the installer also vendors a backend deploy path:
|
|
46
|
+
`.github/actions/backend-app/` and `.github/workflows/deploy-backend.yml`. A
|
|
47
|
+
mobile-only repo is unaffected (the backend path is not installed).
|
|
48
|
+
|
|
49
|
+
The backend workflow runs on a GitHub-hosted runner and, on a push touching
|
|
50
|
+
`backend/**`, deploys to the gowalk host (`138.197.36.107`, publicly reachable):
|
|
51
|
+
it rsyncs the backend dir to `/opt/gowalk-backends/<app>/`, runs
|
|
52
|
+
`docker compose up -d --build`, auto-detects the published `127.0.0.1:<port>`,
|
|
53
|
+
wires an nginx vhost + Let's Encrypt cert for the API domain, and health-checks.
|
|
54
|
+
|
|
55
|
+
Requirements on the consumer repo:
|
|
56
|
+
|
|
57
|
+
- **`backend/docker-compose.yml`** that publishes the API on a **loopback host
|
|
58
|
+
port** (`127.0.0.1:<port>:<container-port>`) so nginx can proxy it. Include a
|
|
59
|
+
Postgres service (or use a per-app database) and a `/health` endpoint.
|
|
60
|
+
- A **`Dockerfile`** the compose file builds; pin the base image, run non-root,
|
|
61
|
+
never bake secrets in. A generated `.env` (with `POSTGRES_PASSWORD`) is created
|
|
62
|
+
on the host on first deploy and preserved across deploys (rsync excludes it).
|
|
63
|
+
- Repo **secret** `BACKEND_DEPLOY_SSH_KEY` (a private key with access to the host).
|
|
64
|
+
- Repo **variables** (optional): `BACKEND_API_DOMAIN` (its DNS A record must point
|
|
65
|
+
at the host for the cert; empty = deploy the container only, skip nginx/cert),
|
|
66
|
+
`BACKEND_APP_NAME` (defaults to the repo name), `BACKEND_HEALTH_PATH`
|
|
67
|
+
(default `/health`), `BACKEND_DEPLOY_HOST` (default `138.197.36.107`).
|
|
68
|
+
|
|
69
|
+
The action directory is canonical here (like the iOS/Android actions): edit it in
|
|
70
|
+
`backend-action/`, never in a consumer's vendored copy.
|
|
71
|
+
|
|
42
72
|
## Android credentials
|
|
43
73
|
|
|
44
74
|
Place these files under `creds/`:
|
|
@@ -103,6 +133,18 @@ in Play Console; every later run detects API readiness and uploads to the
|
|
|
103
133
|
`internal` track automatically. Override the track or status with repository
|
|
104
134
|
variables `GOOGLE_PLAY_TRACK` and `GOOGLE_PLAY_STATUS`.
|
|
105
135
|
|
|
136
|
+
### Gradle daemon heap
|
|
137
|
+
|
|
138
|
+
A `gradle.properties` sized for a workstation does not fit a CI runner.
|
|
139
|
+
`-Xmx8G` with a 4 GB metaspace reserves 12 GB for the Gradle daemon alone,
|
|
140
|
+
before the Kotlin compile daemon spawns its own JVM — on a 16 GB runner the
|
|
141
|
+
build is killed mid-compile, and GitHub reports that as a **cancelled** job
|
|
142
|
+
rather than a failure, so it reads as a flake rather than an out-of-memory.
|
|
143
|
+
|
|
144
|
+
Before building, the Android job caps `-Xmx` at 4 GB and `MaxMetaspaceSize` at
|
|
145
|
+
2 GB in the checkout, with a `::warning::` naming the original value. Projects
|
|
146
|
+
already at or below those numbers are left untouched. Linux only.
|
|
147
|
+
|
|
106
148
|
### Google Play upload retry
|
|
107
149
|
|
|
108
150
|
A Play edit is a short-lived server-side object, and an AAB upload that runs
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.23
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.23
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.23
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
name: gowalk-cicd backend deploy
|
|
2
|
+
description: >
|
|
3
|
+
Build and deploy a Dockerized Python+Postgres backend to the gowalk deploy
|
|
4
|
+
host (138.197.36.107) — rsync the backend dir, `docker compose up -d --build`,
|
|
5
|
+
wire an nginx vhost + Let's Encrypt cert for the API domain, and health-check.
|
|
6
|
+
The deploy host is publicly reachable, so this runs on a GitHub-hosted runner.
|
|
7
|
+
|
|
8
|
+
inputs:
|
|
9
|
+
host:
|
|
10
|
+
description: SSH host of the deploy server.
|
|
11
|
+
required: false
|
|
12
|
+
default: "138.197.36.107"
|
|
13
|
+
ssh-user:
|
|
14
|
+
description: SSH user on the deploy server.
|
|
15
|
+
required: false
|
|
16
|
+
default: "root"
|
|
17
|
+
ssh-key:
|
|
18
|
+
description: Private SSH key with access to the deploy host (from a secret).
|
|
19
|
+
required: true
|
|
20
|
+
app-name:
|
|
21
|
+
description: Stable slug — the compose project and /opt/gowalk-backends/<app-name> dir.
|
|
22
|
+
required: true
|
|
23
|
+
api-domain:
|
|
24
|
+
description: >
|
|
25
|
+
Public domain for the API (e.g. api.myapp.gowalk.com). Its DNS A record
|
|
26
|
+
must already point at the deploy host. Empty = deploy the container only,
|
|
27
|
+
skip nginx + cert.
|
|
28
|
+
required: false
|
|
29
|
+
default: ""
|
|
30
|
+
backend-dir:
|
|
31
|
+
description: Path to the backend (holds Dockerfile + docker-compose.yml).
|
|
32
|
+
required: false
|
|
33
|
+
default: "backend"
|
|
34
|
+
health-path:
|
|
35
|
+
description: HTTP path the service answers 2xx on once healthy.
|
|
36
|
+
required: false
|
|
37
|
+
default: "/health"
|
|
38
|
+
cert-email:
|
|
39
|
+
description: Email for the Let's Encrypt registration.
|
|
40
|
+
required: false
|
|
41
|
+
default: "admin@gowalk.com"
|
|
42
|
+
|
|
43
|
+
runs:
|
|
44
|
+
using: composite
|
|
45
|
+
steps:
|
|
46
|
+
- name: Validate inputs
|
|
47
|
+
shell: bash
|
|
48
|
+
run: |
|
|
49
|
+
set -euo pipefail
|
|
50
|
+
test -f "${{ inputs.backend-dir }}/docker-compose.yml" \
|
|
51
|
+
|| { echo "::error::no ${{ inputs.backend-dir }}/docker-compose.yml"; exit 1; }
|
|
52
|
+
case "${{ inputs.app-name }}" in
|
|
53
|
+
*[!a-z0-9-]*|"") echo "::error::app-name must be [a-z0-9-]"; exit 1;;
|
|
54
|
+
esac
|
|
55
|
+
|
|
56
|
+
- name: Configure SSH
|
|
57
|
+
shell: bash
|
|
58
|
+
env:
|
|
59
|
+
DEPLOY_SSH_KEY: ${{ inputs.ssh-key }}
|
|
60
|
+
run: |
|
|
61
|
+
set -euo pipefail
|
|
62
|
+
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
|
63
|
+
printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/backend_deploy
|
|
64
|
+
chmod 600 ~/.ssh/backend_deploy
|
|
65
|
+
ssh-keyscan -t ed25519,rsa,ecdsa "${{ inputs.host }}" >> ~/.ssh/known_hosts 2>/dev/null
|
|
66
|
+
chmod 644 ~/.ssh/known_hosts
|
|
67
|
+
|
|
68
|
+
- name: Sync backend to the host
|
|
69
|
+
shell: bash
|
|
70
|
+
run: |
|
|
71
|
+
set -euo pipefail
|
|
72
|
+
SSH="ssh -i ~/.ssh/backend_deploy -o IdentitiesOnly=yes"
|
|
73
|
+
DEST="/opt/gowalk-backends/${{ inputs.app-name }}"
|
|
74
|
+
$SSH "${{ inputs.ssh-user }}@${{ inputs.host }}" "mkdir -p '$DEST'"
|
|
75
|
+
# --delete keeps the mirror true; exclude .env so server-side secrets
|
|
76
|
+
# (generated DB password, etc.) survive across deploys.
|
|
77
|
+
rsync -az --delete --exclude '.env' --exclude '.git' \
|
|
78
|
+
-e "$SSH" "${{ inputs.backend-dir }}/" \
|
|
79
|
+
"${{ inputs.ssh-user }}@${{ inputs.host }}:$DEST/"
|
|
80
|
+
# ship the remote deploy script (kept out of the app checkout)
|
|
81
|
+
rsync -az -e "$SSH" "${{ github.action_path }}/remote_deploy.sh" \
|
|
82
|
+
"${{ inputs.ssh-user }}@${{ inputs.host }}:$DEST/.remote_deploy.sh"
|
|
83
|
+
|
|
84
|
+
- name: Deploy on the host
|
|
85
|
+
shell: bash
|
|
86
|
+
run: |
|
|
87
|
+
set -euo pipefail
|
|
88
|
+
SSH="ssh -i ~/.ssh/backend_deploy -o IdentitiesOnly=yes"
|
|
89
|
+
DEST="/opt/gowalk-backends/${{ inputs.app-name }}"
|
|
90
|
+
$SSH "${{ inputs.ssh-user }}@${{ inputs.host }}" \
|
|
91
|
+
"bash '$DEST/.remote_deploy.sh' \
|
|
92
|
+
'${{ inputs.app-name }}' '${{ inputs.api-domain }}' \
|
|
93
|
+
'${{ inputs.health-path }}' '${{ inputs.cert-email }}'"
|
|
94
|
+
|
|
95
|
+
- name: Cleanup key
|
|
96
|
+
if: always()
|
|
97
|
+
shell: bash
|
|
98
|
+
run: rm -f ~/.ssh/backend_deploy
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Runs ON the deploy host, inside /opt/gowalk-backends/<app>. Brings up the
|
|
3
|
+
# compose stack, then (when a domain is given) wires an nginx vhost + LE cert
|
|
4
|
+
# and health-checks the public URL. Idempotent; safe to re-run.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
APP="$1" # slug — compose project + dir name
|
|
8
|
+
DOMAIN="${2:-}" # api.<app>.gowalk.com, or empty to skip nginx/cert
|
|
9
|
+
HEALTH="${3:-/health}"
|
|
10
|
+
CERT_EMAIL="${4:-admin@gowalk.com}"
|
|
11
|
+
DIR="/opt/gowalk-backends/$APP"
|
|
12
|
+
cd "$DIR"
|
|
13
|
+
|
|
14
|
+
log() { echo "[deploy $APP] $*"; }
|
|
15
|
+
|
|
16
|
+
# ── secrets: generate a per-app .env once, then preserve it (rsync excludes it)
|
|
17
|
+
if [ ! -f .env ]; then
|
|
18
|
+
log "first deploy — generating .env (POSTGRES_PASSWORD)"
|
|
19
|
+
{
|
|
20
|
+
echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)"
|
|
21
|
+
echo "APP_NAME=$APP"
|
|
22
|
+
} > .env
|
|
23
|
+
chmod 600 .env
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# ── bring the stack up
|
|
27
|
+
log "docker compose up -d --build"
|
|
28
|
+
docker compose --project-name "$APP" up -d --build --remove-orphans
|
|
29
|
+
|
|
30
|
+
# ── discover the host port the service published on 127.0.0.1.
|
|
31
|
+
# The text `ps` PORTS column reliably shows `127.0.0.1:<port>-><target>/tcp`.
|
|
32
|
+
detect_port() {
|
|
33
|
+
# 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 \
|
|
35
|
+
| sed -nE 's/.*127\.0\.0\.1:([0-9]+)->.*/\1/p' | head -1
|
|
36
|
+
}
|
|
37
|
+
PORT="$(detect_port || true)"
|
|
38
|
+
[ -n "${PORT:-}" ] || { log "ERROR: no 127.0.0.1:<port> publish found in compose ps"; \
|
|
39
|
+
docker compose --project-name "$APP" ps; exit 1; }
|
|
40
|
+
log "service published on 127.0.0.1:$PORT"
|
|
41
|
+
|
|
42
|
+
# ── local health-check first (fast failure, no DNS/cert dependency).
|
|
43
|
+
# Generous: a first-ever deploy cold-inits Postgres and pulls base images.
|
|
44
|
+
HEALTHY=""
|
|
45
|
+
for i in $(seq 1 90); do
|
|
46
|
+
if curl -fsS "http://127.0.0.1:$PORT$HEALTH" >/dev/null 2>&1; then
|
|
47
|
+
log "container healthy on :$PORT$HEALTH (after ${i}x2s)"; HEALTHY=1; break
|
|
48
|
+
fi
|
|
49
|
+
[ $((i % 15)) -eq 0 ] && log "waiting for health… (${i}x2s)"
|
|
50
|
+
sleep 2
|
|
51
|
+
done
|
|
52
|
+
if [ -z "$HEALTHY" ]; then
|
|
53
|
+
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
|
|
56
|
+
exit 1
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# ── container-only deploy (no domain): stop here
|
|
60
|
+
if [ -z "$DOMAIN" ]; then
|
|
61
|
+
log "no domain given — container deployed, skipping nginx/cert"
|
|
62
|
+
exit 0
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# ── nginx vhost → proxy the domain to the published port
|
|
66
|
+
VHOST="/etc/nginx/conf.d/backend-$APP.conf"
|
|
67
|
+
if [ ! -f "$VHOST" ] || ! grep -q "127.0.0.1:$PORT" "$VHOST"; then
|
|
68
|
+
log "writing nginx vhost $VHOST → 127.0.0.1:$PORT"
|
|
69
|
+
cat > "$VHOST" <<NGINX
|
|
70
|
+
server {
|
|
71
|
+
listen 80;
|
|
72
|
+
server_name $DOMAIN;
|
|
73
|
+
client_max_body_size 100m;
|
|
74
|
+
location / {
|
|
75
|
+
proxy_pass http://127.0.0.1:$PORT;
|
|
76
|
+
proxy_http_version 1.1;
|
|
77
|
+
proxy_set_header Host \$host;
|
|
78
|
+
proxy_set_header X-Real-IP \$remote_addr;
|
|
79
|
+
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
|
80
|
+
proxy_set_header X-Forwarded-Proto \$scheme;
|
|
81
|
+
proxy_read_timeout 120s;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
NGINX
|
|
85
|
+
nginx -t && systemctl reload nginx
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# ── Let's Encrypt cert (idempotent; certbot skips if current)
|
|
89
|
+
if ! certbot certificates 2>/dev/null | grep -q "Domains: $DOMAIN\b"; then
|
|
90
|
+
log "requesting LE cert for $DOMAIN"
|
|
91
|
+
certbot --nginx -d "$DOMAIN" --non-interactive --agree-tos \
|
|
92
|
+
--email "$CERT_EMAIL" --redirect || log "warn: certbot failed (DNS not pointed yet?)"
|
|
93
|
+
else
|
|
94
|
+
log "cert for $DOMAIN already present"
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# ── public health-check (best effort — cert/DNS may still be propagating)
|
|
98
|
+
if curl -fsSk "https://$DOMAIN$HEALTH" >/dev/null 2>&1; then
|
|
99
|
+
log "public https://$DOMAIN$HEALTH healthy"
|
|
100
|
+
else
|
|
101
|
+
log "warn: public URL not answering yet (DNS/cert propagation) — container is up on :$PORT"
|
|
102
|
+
fi
|
|
103
|
+
log "done"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gowalk-cicd",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.23",
|
|
4
4
|
"description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
"android-action/",
|
|
16
16
|
"!android-action/**/__pycache__/**",
|
|
17
17
|
"!android-action/**/*.py[cod]",
|
|
18
|
+
"backend-action/",
|
|
19
|
+
"!backend-action/**/__pycache__/**",
|
|
20
|
+
"!backend-action/**/*.py[cod]",
|
|
18
21
|
"templates/",
|
|
19
22
|
"README.md",
|
|
20
23
|
"CLAUDE.md"
|
package/src/install.mjs
CHANGED
|
@@ -26,16 +26,29 @@ const PACKAGE_DIR = resolve(__dirname, '..');
|
|
|
26
26
|
|
|
27
27
|
const ACTION_SRC = join(PACKAGE_DIR, 'action');
|
|
28
28
|
const ANDROID_ACTION_SRC = join(PACKAGE_DIR, 'android-action');
|
|
29
|
+
const BACKEND_ACTION_SRC = join(PACKAGE_DIR, 'backend-action');
|
|
29
30
|
const WORKFLOW_TEMPLATE = join(PACKAGE_DIR, 'templates', 'deploy.yml');
|
|
31
|
+
const BACKEND_WORKFLOW_TEMPLATE = join(PACKAGE_DIR, 'templates', 'deploy-backend.yml');
|
|
30
32
|
const PACKAGE_JSON = join(PACKAGE_DIR, 'package.json');
|
|
31
33
|
|
|
32
34
|
// Target paths inside the consumer repo. These are relative to the repo
|
|
33
35
|
// root we discover below.
|
|
34
36
|
const ACTION_DEST_REL = join('.github', 'actions', 'swift-app');
|
|
35
37
|
const ANDROID_ACTION_DEST_REL = join('.github', 'actions', 'android-app');
|
|
38
|
+
const BACKEND_ACTION_DEST_REL = join('.github', 'actions', 'backend-app');
|
|
36
39
|
const WORKFLOW_DEST_REL = join('.github', 'workflows', 'deploy.yml');
|
|
40
|
+
const BACKEND_WORKFLOW_DEST_REL = join('.github', 'workflows', 'deploy-backend.yml');
|
|
37
41
|
const VERSION_MARKER_REL = join('.github', 'actions', 'swift-app', '.daemux-version');
|
|
38
42
|
const ANDROID_VERSION_MARKER_REL = join('.github', 'actions', 'android-app', '.daemux-version');
|
|
43
|
+
const BACKEND_VERSION_MARKER_REL = join('.github', 'actions', 'backend-app', '.daemux-version');
|
|
44
|
+
|
|
45
|
+
// A repo has a backend when a backend/ or server/ dir carries a compose file.
|
|
46
|
+
function backendDir(repoRoot) {
|
|
47
|
+
for (const d of ['backend', 'server']) {
|
|
48
|
+
if (existsSync(join(repoRoot, d, 'docker-compose.yml'))) return d;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
39
52
|
|
|
40
53
|
// Lines we treat as ignoring credentials. If any appears in the consumer's
|
|
41
54
|
// .gitignore, CI will not see one or both platform keys. We warn instead of
|
|
@@ -113,6 +126,18 @@ function copyWorkflow(repoRoot, dryRun) {
|
|
|
113
126
|
return { dest, existed };
|
|
114
127
|
}
|
|
115
128
|
|
|
129
|
+
function copyBackendWorkflow(repoRoot, dryRun) {
|
|
130
|
+
const dest = join(repoRoot, BACKEND_WORKFLOW_DEST_REL);
|
|
131
|
+
const existed = existsSync(dest);
|
|
132
|
+
if (dryRun) {
|
|
133
|
+
console.log(` [dry-run] ${existed ? 'overwrite' : 'create'} ${BACKEND_WORKFLOW_DEST_REL}`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
ensureDir(dirname(dest));
|
|
137
|
+
cpSync(BACKEND_WORKFLOW_TEMPLATE, dest, { force: true });
|
|
138
|
+
console.log(` ${existed ? 'Overwrote' : 'Created'} ${BACKEND_WORKFLOW_DEST_REL}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
116
141
|
function writeVersionMarkers(repoRoot, dryRun) {
|
|
117
142
|
// Reads this package's own version from package.json and stamps it
|
|
118
143
|
// into the consumer repo as the source of truth for autoupdate's
|
|
@@ -195,6 +220,19 @@ export async function runInstall({ dryRun = false } = {}) {
|
|
|
195
220
|
writeVersionMarkers(repoRoot, dryRun);
|
|
196
221
|
copyWorkflow(repoRoot, dryRun);
|
|
197
222
|
|
|
223
|
+
// Backend deploy path — installed only when the repo has a Dockerized
|
|
224
|
+
// backend (backend/ or server/ with a docker-compose.yml). Mobile-only
|
|
225
|
+
// repos are unaffected, so this is backward compatible.
|
|
226
|
+
const bdir = backendDir(repoRoot);
|
|
227
|
+
if (bdir) {
|
|
228
|
+
console.log(` Backend detected (${bdir}/docker-compose.yml) — installing backend deploy`);
|
|
229
|
+
copyAction(BACKEND_ACTION_SRC, BACKEND_ACTION_DEST_REL, repoRoot, dryRun);
|
|
230
|
+
if (!dryRun) writeFileSync(
|
|
231
|
+
join(repoRoot, BACKEND_VERSION_MARKER_REL),
|
|
232
|
+
String(JSON.parse(readFileSync(PACKAGE_JSON, 'utf8')).version || '').trim());
|
|
233
|
+
copyBackendWorkflow(repoRoot, dryRun);
|
|
234
|
+
}
|
|
235
|
+
|
|
198
236
|
const gi = checkGitignore(repoRoot);
|
|
199
237
|
printGitignoreWarning(gi);
|
|
200
238
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: Backend Deploy
|
|
2
|
+
|
|
3
|
+
# Deploys the Dockerized Python+Postgres backend to the gowalk host. Installed
|
|
4
|
+
# by gowalk-cicd only when a backend/ (or server/) dir with a docker-compose.yml
|
|
5
|
+
# is present. Mobile deploy stays in deploy.yml — this is the backend's own path.
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
branches: [main]
|
|
9
|
+
paths:
|
|
10
|
+
- "backend/**"
|
|
11
|
+
- "server/**"
|
|
12
|
+
- ".github/workflows/deploy-backend.yml"
|
|
13
|
+
workflow_dispatch:
|
|
14
|
+
|
|
15
|
+
concurrency:
|
|
16
|
+
group: backend-deploy-${{ github.ref }}
|
|
17
|
+
cancel-in-progress: true
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
deploy:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
timeout-minutes: 30
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
|
|
26
|
+
# Pick backend/ or server/, whichever holds the compose file.
|
|
27
|
+
- id: dir
|
|
28
|
+
run: |
|
|
29
|
+
if [ -f backend/docker-compose.yml ]; then echo "path=backend" >> "$GITHUB_OUTPUT"
|
|
30
|
+
elif [ -f server/docker-compose.yml ]; then echo "path=server" >> "$GITHUB_OUTPUT"
|
|
31
|
+
else echo "::error::no backend/ or server/ docker-compose.yml"; exit 1; fi
|
|
32
|
+
|
|
33
|
+
- name: Deploy backend
|
|
34
|
+
uses: ./.github/actions/backend-app
|
|
35
|
+
with:
|
|
36
|
+
ssh-key: ${{ secrets.BACKEND_DEPLOY_SSH_KEY }}
|
|
37
|
+
host: ${{ vars.BACKEND_DEPLOY_HOST || '138.197.36.107' }}
|
|
38
|
+
app-name: ${{ vars.BACKEND_APP_NAME || github.event.repository.name }}
|
|
39
|
+
api-domain: ${{ vars.BACKEND_API_DOMAIN }}
|
|
40
|
+
backend-dir: ${{ steps.dir.outputs.path }}
|
|
41
|
+
health-path: ${{ vars.BACKEND_HEALTH_PATH || '/health' }}
|
package/templates/deploy.yml
CHANGED
|
@@ -183,6 +183,38 @@ jobs:
|
|
|
183
183
|
echo "::add-mask::$GIT_PRIVATE_TOKEN"
|
|
184
184
|
git config --global url."https://x-access-token:${GIT_PRIVATE_TOKEN}@github.com/".insteadOf "https://github.com/"
|
|
185
185
|
echo "Configured authenticated github.com remotes for pub."
|
|
186
|
+
# A Gradle daemon sized for a workstation does not fit a CI runner. Asking
|
|
187
|
+
# for -Xmx8G with a 4G metaspace reserves 12 GB before the Kotlin compile
|
|
188
|
+
# daemon spawns its own JVM, and the runner is killed mid-compile — which
|
|
189
|
+
# GitHub reports as "cancelled", not as a failure, so it reads as a flake
|
|
190
|
+
# rather than an out-of-memory. Clamp to a size that leaves room, in the
|
|
191
|
+
# checkout only.
|
|
192
|
+
- name: Cap the Gradle daemon heap for this runner
|
|
193
|
+
if: ${{ steps.mode.outputs.mode == 'local' && runner.os == 'Linux' }}
|
|
194
|
+
shell: bash
|
|
195
|
+
env:
|
|
196
|
+
MAX_HEAP_GB: '4'
|
|
197
|
+
MAX_META_GB: '2'
|
|
198
|
+
run: |
|
|
199
|
+
shopt -s nullglob
|
|
200
|
+
for f in android/gradle.properties gradle.properties; do
|
|
201
|
+
[ -f "$f" ] || continue
|
|
202
|
+
before=$(grep -m1 '^org.gradle.jvmargs' "$f" || true)
|
|
203
|
+
[ -n "$before" ] || continue
|
|
204
|
+
heap=$(sed -nE 's/.*-Xmx([0-9]+)[Gg].*/\1/p' <<<"$before")
|
|
205
|
+
meta=$(sed -nE 's/.*MaxMetaspaceSize=([0-9]+)[Gg].*/\1/p' <<<"$before")
|
|
206
|
+
changed=""
|
|
207
|
+
if [ -n "$heap" ] && [ "$heap" -gt "$MAX_HEAP_GB" ]; then
|
|
208
|
+
sed -i -E "s/-Xmx${heap}[Gg]/-Xmx${MAX_HEAP_GB}G/" "$f"; changed=1
|
|
209
|
+
fi
|
|
210
|
+
if [ -n "$meta" ] && [ "$meta" -gt "$MAX_META_GB" ]; then
|
|
211
|
+
sed -i -E "s/MaxMetaspaceSize=${meta}[Gg]/MaxMetaspaceSize=${MAX_META_GB}G/" "$f"; changed=1
|
|
212
|
+
fi
|
|
213
|
+
if [ -n "$changed" ]; then
|
|
214
|
+
echo "::warning::$f asked for more heap than this runner has; capped it for this build only ($before)"
|
|
215
|
+
grep -m1 '^org.gradle.jvmargs' "$f"
|
|
216
|
+
fi
|
|
217
|
+
done
|
|
186
218
|
# Same reason as the iOS job: apps using AppLocalizations must generate it
|
|
187
219
|
# before anything analyses or compiles them. No-op for apps without l10n.
|
|
188
220
|
- name: Generate localizations
|