gowalk-cicd 1.0.42 → 1.0.44

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
@@ -64,11 +64,6 @@ workflow (`.github/workflows/publish.yml`):
64
64
  attach the `publish.yml` trusted-publisher relationship; the workflow
65
65
  supplies that token as a fallback and uses OIDC after trust is configured.
66
66
 
67
- An explicitly dispatched run may publish a tested feature ref by setting its
68
- `release` boolean input. The input defaults to false, so ordinary manual runs
69
- remain test-only. Use this path when a deploy repair must ship without pushing
70
- the feature branch directly to `main`.
71
-
72
67
  Manual publish (from repo root):
73
68
 
74
69
  ```bash
package/README.md CHANGED
@@ -58,22 +58,25 @@ account or domain.
58
58
 
59
59
  ## Backend deploy (Python + Postgres, Docker)
60
60
 
61
- When a repo carries a Dockerized backend a `backend/` (or `server/`) directory
62
- with a `docker-compose.yml` — the installer also vendors a backend deploy path:
61
+ When a repo carries a Dockerized backend, the installer also vendors a backend deploy path:
63
62
  `.github/actions/backend-app/` and `.github/workflows/deploy-backend.yml`. A
64
63
  mobile-only repo is unaffected (the backend path is not installed).
65
64
 
65
+ Both supported layouts keep application source under `backend/` or `server/`:
66
+
67
+ - the compose file can live beside that source as `backend/docker-compose.yml`; or
68
+ - it can live at the repository root as `docker-compose.yml` and build `./backend`
69
+ (equivalently for `server/`).
70
+
66
71
  The backend workflow runs on a GitHub-hosted runner and, on a push touching
67
72
  `backend/**`, deploys to the gowalk host (`138.197.36.107`, publicly reachable):
68
73
  it rsyncs the backend dir to `/opt/gowalk-backends/<app>/`, runs
69
74
  `docker compose up -d --build`, auto-detects the published `127.0.0.1:<port>`,
70
75
  wires an nginx vhost + Let's Encrypt cert for the API domain, and health-checks.
71
- Certificate issuance and the public health check are strict: a deploy fails if
72
- the hostname cannot present a trusted matching certificate and reach the API.
73
76
 
74
77
  Requirements on the consumer repo:
75
78
 
76
- - **`backend/docker-compose.yml`** that publishes the API on a **loopback host
79
+ - A supported **`docker-compose.yml`** that publishes the API on a **loopback host
77
80
  port** (`127.0.0.1:<port>:<container-port>`) so nginx can proxy it. Include a
78
81
  Postgres service (or use a per-app database) and a `/health` endpoint.
79
82
  - A **`Dockerfile`** the compose file builds; pin the base image, run non-root,
@@ -86,9 +89,7 @@ Requirements on the consumer repo:
86
89
  - Repo **variables** (optional): `BACKEND_API_DOMAIN` (its DNS A record must point
87
90
  at the host for the cert; empty = deploy the container only, skip nginx/cert),
88
91
  `BACKEND_APP_NAME` (defaults to the repo name), `BACKEND_HEALTH_PATH`
89
- (default `/health`), `BACKEND_DEPLOY_HOST` (default `138.197.36.107`), and
90
- `BACKEND_CERTBOT_ACCOUNT` (optional account ID for hosts with multiple ACME
91
- accounts; otherwise the sole or host-created account is selected).
92
+ (default `/health`), `BACKEND_DEPLOY_HOST` (default `138.197.36.107`).
92
93
 
93
94
  The action directory is canonical here (like the iOS/Android actions): edit it in
94
95
  `backend-action/`, never in a consumer's vendored copy.
@@ -1 +1 @@
1
- 1.0.42
1
+ 1.0.44
@@ -1 +1 @@
1
- 1.0.42
1
+ 1.0.44
@@ -1 +1 @@
1
- 1.0.42
1
+ 1.0.44
@@ -28,9 +28,15 @@ inputs:
28
28
  required: false
29
29
  default: ""
30
30
  backend-dir:
31
- description: Path to the backend (holds Dockerfile + docker-compose.yml).
31
+ description: Path to the backend source directory.
32
32
  required: false
33
33
  default: "backend"
34
+ compose-file:
35
+ description: >
36
+ Compose file path. Empty defaults to <backend-dir>/docker-compose.yml;
37
+ docker-compose.yml supports a root compose file that builds ./backend or ./server.
38
+ required: false
39
+ default: ""
34
40
  health-path:
35
41
  description: HTTP path the service answers 2xx on once healthy.
36
42
  required: false
@@ -39,12 +45,6 @@ inputs:
39
45
  description: Email for the Let's Encrypt registration.
40
46
  required: false
41
47
  default: "admin@gowalk.com"
42
- certbot-account:
43
- description: >
44
- Optional Certbot account ID. When omitted, the deploy selects the sole
45
- account or the account created on the current host.
46
- required: false
47
- default: ""
48
48
  runtime-env:
49
49
  description: >
50
50
  Optional newline-delimited KEY=value secrets written to a mode-0600
@@ -57,14 +57,33 @@ runs:
57
57
  using: composite
58
58
  steps:
59
59
  - name: Validate inputs
60
+ id: layout
60
61
  shell: bash
62
+ env:
63
+ BACKEND_DIR: ${{ inputs.backend-dir }}
64
+ COMPOSE_FILE_INPUT: ${{ inputs.compose-file }}
61
65
  run: |
62
66
  set -euo pipefail
63
- test -f "${{ inputs.backend-dir }}/docker-compose.yml" \
64
- || { echo "::error::no ${{ inputs.backend-dir }}/docker-compose.yml"; exit 1; }
67
+ COMPOSE_FILE="${COMPOSE_FILE_INPUT:-$BACKEND_DIR/docker-compose.yml}"
68
+ test -d "$BACKEND_DIR" \
69
+ || { echo "::error::no backend directory $BACKEND_DIR"; exit 1; }
70
+ test -f "$COMPOSE_FILE" \
71
+ || { echo "::error::no compose file $COMPOSE_FILE"; exit 1; }
72
+ case "$BACKEND_DIR" in
73
+ /*|../*|*/../*|*/..|.) echo "::error::backend-dir must be a safe relative path"; exit 1;;
74
+ esac
75
+ case "$COMPOSE_FILE" in
76
+ /*|../*|*/../*|*/..) echo "::error::compose-file must be a safe relative path"; exit 1;;
77
+ esac
78
+ if [ "$COMPOSE_FILE" != "$BACKEND_DIR/docker-compose.yml" ] \
79
+ && [ "$COMPOSE_FILE" != "docker-compose.yml" ]; then
80
+ echo "::error::compose-file must be docker-compose.yml or $BACKEND_DIR/docker-compose.yml"
81
+ exit 1
82
+ fi
65
83
  case "${{ inputs.app-name }}" in
66
84
  *[!a-z0-9-]*|"") echo "::error::app-name must be [a-z0-9-]"; exit 1;;
67
85
  esac
86
+ echo "compose-file=$COMPOSE_FILE" >> "$GITHUB_OUTPUT"
68
87
 
69
88
  - name: Configure SSH
70
89
  shell: bash
@@ -80,6 +99,9 @@ runs:
80
99
 
81
100
  - name: Sync backend to the host
82
101
  shell: bash
102
+ env:
103
+ BACKEND_DIR: ${{ inputs.backend-dir }}
104
+ COMPOSE_FILE: ${{ steps.layout.outputs.compose-file }}
83
105
  run: |
84
106
  set -euo pipefail
85
107
  SSH="ssh -i ~/.ssh/backend_deploy -o IdentitiesOnly=yes"
@@ -87,9 +109,19 @@ runs:
87
109
  $SSH "${{ inputs.ssh-user }}@${{ inputs.host }}" "mkdir -p '$DEST'"
88
110
  # --delete keeps the mirror true; exclude .env so server-side secrets
89
111
  # (generated DB password, etc.) survive across deploys.
90
- rsync -az --delete --exclude '.env' --exclude '.git' \
91
- -e "$SSH" "${{ inputs.backend-dir }}/" \
92
- "${{ inputs.ssh-user }}@${{ inputs.host }}:$DEST/"
112
+ if [ "$COMPOSE_FILE" = "$BACKEND_DIR/docker-compose.yml" ]; then
113
+ rsync -az --delete --exclude '.env' --exclude '.git' \
114
+ -e "$SSH" "$BACKEND_DIR/" \
115
+ "${{ inputs.ssh-user }}@${{ inputs.host }}:$DEST/"
116
+ else
117
+ $SSH "${{ inputs.ssh-user }}@${{ inputs.host }}" \
118
+ "mkdir -p '$DEST/$BACKEND_DIR'"
119
+ rsync -az --delete --exclude '.env' --exclude '.git' \
120
+ -e "$SSH" "$BACKEND_DIR/" \
121
+ "${{ inputs.ssh-user }}@${{ inputs.host }}:$DEST/$BACKEND_DIR/"
122
+ rsync -az -e "$SSH" "$COMPOSE_FILE" \
123
+ "${{ inputs.ssh-user }}@${{ inputs.host }}:$DEST/docker-compose.yml"
124
+ fi
93
125
  # ship the remote deploy script (kept out of the app checkout)
94
126
  rsync -az -e "$SSH" "${{ github.action_path }}/remote_deploy.sh" \
95
127
  "${{ inputs.ssh-user }}@${{ inputs.host }}:$DEST/.remote_deploy.sh"
@@ -110,8 +142,7 @@ runs:
110
142
  $SSH "${{ inputs.ssh-user }}@${{ inputs.host }}" \
111
143
  "bash '$DEST/.remote_deploy.sh' \
112
144
  '${{ inputs.app-name }}' '${{ inputs.api-domain }}' \
113
- '${{ inputs.health-path }}' '${{ inputs.cert-email }}' \
114
- '${{ inputs.certbot-account }}'"
145
+ '${{ inputs.health-path }}' '${{ inputs.cert-email }}'"
115
146
 
116
147
  - name: Cleanup key
117
148
  if: always()
@@ -8,37 +8,11 @@ APP="$1" # slug — compose project + dir name
8
8
  DOMAIN="${2:-}" # api.<app>.gowalk.com, or empty to skip nginx/cert
9
9
  HEALTH="${3:-/health}"
10
10
  CERT_EMAIL="${4:-admin@gowalk.com}"
11
- CERT_ACCOUNT="${5:-}"
12
11
  DIR="/opt/gowalk-backends/$APP"
13
12
  cd "$DIR"
14
13
 
15
14
  log() { echo "[deploy $APP] $*"; }
16
15
 
17
- select_certbot_account() {
18
- [ -n "$CERT_ACCOUNT" ] && return
19
- local accounts_root host account_dir creation_host
20
- local -a accounts=()
21
- accounts_root="/etc/letsencrypt/accounts/acme-v02.api.letsencrypt.org/directory"
22
- [ -d "$accounts_root" ] || return
23
- host="$(hostname)"
24
- while IFS= read -r account_dir; do
25
- accounts+=("$(basename "$account_dir")")
26
- creation_host="$(sed -nE \
27
- 's/.*"creation_host"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' \
28
- "$account_dir/meta.json" 2>/dev/null | head -1)"
29
- if [ "$creation_host" = "$host" ]; then
30
- CERT_ACCOUNT="$(basename "$account_dir")"
31
- fi
32
- done < <(find "$accounts_root" -mindepth 1 -maxdepth 1 -type d | sort)
33
- if [ -z "$CERT_ACCOUNT" ] && [ "${#accounts[@]}" -eq 1 ]; then
34
- CERT_ACCOUNT="${accounts[0]}"
35
- fi
36
- if [ -z "$CERT_ACCOUNT" ] && [ "${#accounts[@]}" -gt 1 ]; then
37
- log "ERROR: multiple Certbot accounts; set BACKEND_CERTBOT_ACCOUNT"
38
- return 1
39
- fi
40
- }
41
-
42
16
  # ── secrets: generate a per-app .env once, then preserve it (rsync excludes it)
43
17
  if [ ! -f .env ]; then
44
18
  log "first deploy — generating .env (POSTGRES_PASSWORD)"
@@ -121,20 +95,16 @@ fi
121
95
  # ── Let's Encrypt cert (idempotent; certbot skips if current)
122
96
  if ! certbot certificates 2>/dev/null | grep -q "Domains: $DOMAIN\b"; then
123
97
  log "requesting LE cert for $DOMAIN"
124
- select_certbot_account
125
- CERTBOT_ARGS=()
126
- if [ -n "$CERT_ACCOUNT" ]; then
127
- log "using Certbot account ${CERT_ACCOUNT:0:4}…"
128
- CERTBOT_ARGS+=(--account "$CERT_ACCOUNT")
129
- fi
130
98
  certbot --nginx -d "$DOMAIN" --non-interactive --agree-tos \
131
- --email "$CERT_EMAIL" --redirect "${CERTBOT_ARGS[@]}"
99
+ --email "$CERT_EMAIL" --redirect || log "warn: certbot failed (DNS not pointed yet?)"
132
100
  else
133
101
  log "cert for $DOMAIN already present"
134
102
  fi
135
103
 
136
- # ── public health-check: validates DNS, certificate/SNI, routing and service.
137
- curl -fsS --retry 6 --retry-all-errors --retry-delay 2 \
138
- "https://$DOMAIN$HEALTH" >/dev/null
139
- log "public https://$DOMAIN$HEALTH healthy with verified TLS"
104
+ # ── public health-check (best effort — cert/DNS may still be propagating)
105
+ if curl -fsSk "https://$DOMAIN$HEALTH" >/dev/null 2>&1; then
106
+ log "public https://$DOMAIN$HEALTH healthy"
107
+ else
108
+ log "warn: public URL not answering yet (DNS/cert propagation) — container is up on :$PORT"
109
+ fi
140
110
  log "done"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.42",
3
+ "version": "1.0.44",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/install.mjs CHANGED
@@ -44,10 +44,22 @@ const VERSION_MARKER_REL = join('.github', 'actions', 'swift-app', '.daemux-vers
44
44
  const ANDROID_VERSION_MARKER_REL = join('.github', 'actions', 'android-app', '.daemux-version');
45
45
  const BACKEND_VERSION_MARKER_REL = join('.github', 'actions', 'backend-app', '.daemux-version');
46
46
 
47
- // A repo has a backend when a backend/ or server/ dir carries a compose file.
48
- function backendDir(repoRoot) {
47
+ // A repo has a backend when backend/ or server/ either carries the compose file
48
+ // or is referenced by a root compose file. The latter is Flutter's common
49
+ // full-stack layout: docker-compose.yml stays at the repo root so it can build
50
+ // ./backend while the deploy action still syncs only backend-owned files.
51
+ function backendLayout(repoRoot) {
49
52
  for (const d of ['backend', 'server']) {
50
- if (existsSync(join(repoRoot, d, 'docker-compose.yml'))) return d;
53
+ if (existsSync(join(repoRoot, d, 'docker-compose.yml'))) {
54
+ return { backendDir: d, composeFile: join(d, 'docker-compose.yml') };
55
+ }
56
+ }
57
+ if (existsSync(join(repoRoot, 'docker-compose.yml'))) {
58
+ for (const d of ['backend', 'server']) {
59
+ if (existsSync(join(repoRoot, d, 'Dockerfile'))) {
60
+ return { backendDir: d, composeFile: 'docker-compose.yml' };
61
+ }
62
+ }
51
63
  }
52
64
  return null;
53
65
  }
@@ -235,12 +247,12 @@ export async function runInstall({ dryRun = false } = {}) {
235
247
  }
236
248
  }
237
249
 
238
- // Backend deploy path — installed only when the repo has a Dockerized
239
- // backend (backend/ or server/ with a docker-compose.yml). Mobile-only
240
- // repos are unaffected, so this is backward compatible.
241
- const bdir = backendDir(repoRoot);
242
- if (bdir) {
243
- console.log(` Backend detected (${bdir}/docker-compose.yml) — installing backend deploy`);
250
+ // Backend deploy path — installed only for a supported nested or root
251
+ // compose layout. Mobile-only repos are unaffected, so this is backward
252
+ // compatible.
253
+ const layout = backendLayout(repoRoot);
254
+ if (layout) {
255
+ console.log(` Backend detected (${layout.composeFile}) — installing backend deploy`);
244
256
  copyAction(BACKEND_ACTION_SRC, BACKEND_ACTION_DEST_REL, repoRoot, dryRun);
245
257
  if (!dryRun) writeFileSync(
246
258
  join(repoRoot, BACKEND_VERSION_MARKER_REL),
@@ -1,17 +1,21 @@
1
1
  name: Backend Deploy
2
2
 
3
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.
4
+ # by gowalk-cicd when backend/ or server/ has a compose file, or when a root
5
+ # compose file builds one of those directories. Mobile deploy stays in deploy.yml.
6
6
  on:
7
7
  push:
8
8
  branches: [main]
9
9
  paths:
10
10
  - "backend/**"
11
11
  - "server/**"
12
+ - "docker-compose.yml"
12
13
  - ".github/workflows/deploy-backend.yml"
13
14
  workflow_dispatch:
14
15
 
16
+ permissions:
17
+ contents: read
18
+
15
19
  concurrency:
16
20
  group: backend-deploy-${{ github.ref }}
17
21
  cancel-in-progress: true
@@ -21,14 +25,27 @@ jobs:
21
25
  runs-on: ubuntu-latest
22
26
  timeout-minutes: 30
23
27
  steps:
24
- - uses: actions/checkout@v4
28
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
25
29
 
26
- # Pick backend/ or server/, whichever holds the compose file.
30
+ # Pick the source dir and either its nested compose file or a root one.
27
31
  - id: dir
28
32
  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
33
+ if [ -f backend/docker-compose.yml ]; then
34
+ echo "path=backend" >> "$GITHUB_OUTPUT"
35
+ echo "compose=backend/docker-compose.yml" >> "$GITHUB_OUTPUT"
36
+ elif [ -f server/docker-compose.yml ]; then
37
+ echo "path=server" >> "$GITHUB_OUTPUT"
38
+ echo "compose=server/docker-compose.yml" >> "$GITHUB_OUTPUT"
39
+ elif [ -f docker-compose.yml ] && [ -f backend/Dockerfile ]; then
40
+ echo "path=backend" >> "$GITHUB_OUTPUT"
41
+ echo "compose=docker-compose.yml" >> "$GITHUB_OUTPUT"
42
+ elif [ -f docker-compose.yml ] && [ -f server/Dockerfile ]; then
43
+ echo "path=server" >> "$GITHUB_OUTPUT"
44
+ echo "compose=docker-compose.yml" >> "$GITHUB_OUTPUT"
45
+ else
46
+ echo "::error::no supported backend compose layout"
47
+ exit 1
48
+ fi
32
49
 
33
50
  - name: Deploy backend
34
51
  uses: ./.github/actions/backend-app
@@ -38,6 +55,6 @@ jobs:
38
55
  app-name: ${{ vars.BACKEND_APP_NAME || github.event.repository.name }}
39
56
  api-domain: ${{ vars.BACKEND_API_DOMAIN }}
40
57
  backend-dir: ${{ steps.dir.outputs.path }}
58
+ compose-file: ${{ steps.dir.outputs.compose }}
41
59
  health-path: ${{ vars.BACKEND_HEALTH_PATH || '/health' }}
42
- certbot-account: ${{ vars.BACKEND_CERTBOT_ACCOUNT }}
43
60
  runtime-env: ${{ secrets.BACKEND_RUNTIME_ENV }}
@@ -410,14 +410,29 @@ jobs:
410
410
  && github.ref_name != github.event.repository.default_branch }}
411
411
  runs-on: ubuntu-latest
412
412
  timeout-minutes: 30
413
+ permissions:
414
+ contents: read
413
415
  steps:
414
416
  - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
415
417
  - id: dir
416
418
  shell: bash
417
419
  run: |
418
- if [ -f backend/docker-compose.yml ]; then echo "path=backend" >> "$GITHUB_OUTPUT"
419
- elif [ -f server/docker-compose.yml ]; then echo "path=server" >> "$GITHUB_OUTPUT"
420
- else echo "path=" >> "$GITHUB_OUTPUT"; fi
420
+ if [ -f backend/docker-compose.yml ]; then
421
+ echo "path=backend" >> "$GITHUB_OUTPUT"
422
+ echo "compose=backend/docker-compose.yml" >> "$GITHUB_OUTPUT"
423
+ elif [ -f server/docker-compose.yml ]; then
424
+ echo "path=server" >> "$GITHUB_OUTPUT"
425
+ echo "compose=server/docker-compose.yml" >> "$GITHUB_OUTPUT"
426
+ elif [ -f docker-compose.yml ] && [ -f backend/Dockerfile ]; then
427
+ echo "path=backend" >> "$GITHUB_OUTPUT"
428
+ echo "compose=docker-compose.yml" >> "$GITHUB_OUTPUT"
429
+ elif [ -f docker-compose.yml ] && [ -f server/Dockerfile ]; then
430
+ echo "path=server" >> "$GITHUB_OUTPUT"
431
+ echo "compose=docker-compose.yml" >> "$GITHUB_OUTPUT"
432
+ else
433
+ echo "path=" >> "$GITHUB_OUTPUT"
434
+ echo "compose=" >> "$GITHUB_OUTPUT"
435
+ fi
421
436
  - name: Deploy backend from feature branch
422
437
  if: ${{ steps.dir.outputs.path != '' }}
423
438
  uses: ./.github/actions/backend-app
@@ -427,6 +442,6 @@ jobs:
427
442
  app-name: ${{ vars.BACKEND_APP_NAME || github.event.repository.name }}
428
443
  api-domain: ${{ vars.BACKEND_API_DOMAIN }}
429
444
  backend-dir: ${{ steps.dir.outputs.path }}
445
+ compose-file: ${{ steps.dir.outputs.compose }}
430
446
  health-path: ${{ vars.BACKEND_HEALTH_PATH || '/health' }}
431
- certbot-account: ${{ vars.BACKEND_CERTBOT_ACCOUNT }}
432
447
  runtime-env: ${{ secrets.BACKEND_RUNTIME_ENV }}