gowalk-cicd 1.0.156 → 1.0.157

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
@@ -205,6 +205,12 @@ node /path/to/gowalk-cicd/bin/cli.mjs
205
205
  `cache_scope.cjs` and `artifact_env.cjs` copies match the Android distribution byte for byte,
206
206
  enforced by `test/artifact-wiring.test.mjs`; update both when changing that common guard.
207
207
  Update existing consumer workflow artifact steps as well as action directories on adoption.
208
+ - Backend layout selection belongs in `backend-action/layout.cjs`, reused by the installer and both
209
+ workflow entrypoints. Support the four standard Compose names without source renames. Multiple
210
+ matches require explicit existing `backend-dir`/`compose-file` configuration before writes or SSH.
211
+ Keep the chosen basename through transfer and remote `COMPOSE_FILE`; stale host files must not win
212
+ Docker's implicit filename precedence. Root source directories remain unsupported. Cover isolated
213
+ consumer installs, action validation/transfer and remote command selection in focused tests.
208
214
  - Backend runtime credentials use the single encrypted `BACKEND_RUNTIME_ENV`
209
215
  secret. The action writes it to host-side `.runtime.env` with mode 0600 and
210
216
  supplies it to Compose after the generated `.env`; never print its content.
package/README.md CHANGED
@@ -166,10 +166,34 @@ mobile-only repo is unaffected (the backend path is not installed).
166
166
 
167
167
  Both supported layouts keep application source under `backend/` or `server/`:
168
168
 
169
- - the compose file can live beside that source as `backend/docker-compose.yml`; or
170
- - it can live at the repository root as `docker-compose.yml` and build `./backend`
169
+ - the Compose file can live beside that source as `backend/compose.yaml`; or
170
+ - it can live at the repository root as `compose.yaml` and build `./backend`
171
171
  (equivalently for `server/`).
172
172
 
173
+ The standard names `compose.yaml`, `compose.yml`, `docker-compose.yaml` and
174
+ `docker-compose.yml` are supported without renaming source. Installation, both workflows and the
175
+ backend action use the same selector. Multiple matching files or source directories refuse with
176
+ `backend_layout_ambiguous` before installer writes or SSH. Select the intended existing paths using
177
+ `backend.backend-dir` and `backend.compose-file` in `.gowalk-cicd.yml`; custom source subdirectories
178
+ are supported through that configuration. Source at the repository root (`backend-dir: .`) remains
179
+ unsupported. The host receives and explicitly uses the selected filename, even if an older alternate
180
+ file exists there. Other root-level files are not implicitly copied.
181
+
182
+ Before preparing the complete app candidate, run the released installer with `--dry-run`, then run
183
+ it without that option. Verify `.github/workflows/deploy-backend.yml` exists and run the ready local
184
+ selector from the app checkout:
185
+
186
+ ```sh
187
+ node .github/actions/backend-app/layout.cjs
188
+ # For an explicitly selected layout, pass the same nonsecret configuration values:
189
+ BACKEND_DIR_INPUT=service COMPOSE_FILE_INPUT=service/compose.yml \
190
+ BACKEND_LAYOUT_REQUIRED=true node .github/actions/backend-app/layout.cjs
191
+ ```
192
+
193
+ These commands only inspect file locations and return a JSON layout receipt; they do not read Compose
194
+ contents, contact a provider or deploy. Preserve app-owned configuration and generated workflow changes
195
+ in the same validated candidate. A layout receipt proves selection, not production readiness.
196
+
173
197
  The backend workflow runs on a GitHub-hosted runner and, on a push touching
174
198
  `backend/**`, deploys to the gowalk host (`138.197.36.107`, publicly reachable):
175
199
  it rsyncs the backend dir to `/opt/gowalk-backends/<app>/`, runs
@@ -182,7 +206,7 @@ certificate is installed for the requested domain on redeploy, repairing older H
182
206
 
183
207
  Requirements on the consumer repo:
184
208
 
185
- - A supported **`docker-compose.yml`** that publishes the API on a **loopback host
209
+ - A supported **Compose file** that publishes the API on a **loopback host
186
210
  port** (`127.0.0.1:<port>:<container-port>`) so nginx can proxy it. Include a
187
211
  Postgres service (or use a per-app database) and a `/health` endpoint.
188
212
  - A **`Dockerfile`** the compose file builds; pin the base image, run non-root,
@@ -1 +1 @@
1
- 1.0.156
1
+ 1.0.157
@@ -1 +1 @@
1
- 1.0.156
1
+ 1.0.157
@@ -1 +1 @@
1
- 1.0.156
1
+ 1.0.157
@@ -49,8 +49,8 @@ inputs:
49
49
  default: "backend"
50
50
  compose-file:
51
51
  description: >
52
- Compose file path. Empty defaults to <backend-dir>/docker-compose.yml;
53
- docker-compose.yml supports a root compose file that builds ./backend or ./server.
52
+ Compose file path: compose.yaml, compose.yml, docker-compose.yaml or docker-compose.yml
53
+ at the root or inside backend-dir. Empty selects the one unambiguous supported file.
54
54
  required: false
55
55
  default: ""
56
56
  health-path:
@@ -104,8 +104,9 @@ runs:
104
104
  id: layout
105
105
  shell: bash
106
106
  env:
107
- BACKEND_DIR: ${{ inputs.backend-dir }}
107
+ BACKEND_DIR_INPUT: ${{ inputs.backend-dir }}
108
108
  COMPOSE_FILE_INPUT: ${{ inputs.compose-file }}
109
+ BACKEND_LAYOUT_REQUIRED: "true"
109
110
  BACKEND_PUBLIC_HEALTH_URL: ${{ inputs.public-health-url }}
110
111
  BACKEND_PUBLIC_BASE_URL: ${{ inputs.public-base-url }}
111
112
  BACKEND_EXPECTED_BUILD_SHA: ${{ inputs.expected-build-sha }}
@@ -116,26 +117,10 @@ runs:
116
117
  source "${{ github.action_path }}/deploy_evidence.sh"
117
118
  BACKEND_DEPLOY_PHASE=validate_inputs
118
119
  python3 "${{ github.action_path }}/edge_inputs.py" check
119
- COMPOSE_FILE="${COMPOSE_FILE_INPUT:-$BACKEND_DIR/docker-compose.yml}"
120
- test -d "$BACKEND_DIR" \
121
- || { echo "::error::no backend directory $BACKEND_DIR"; exit 1; }
122
- test -f "$COMPOSE_FILE" \
123
- || { echo "::error::no compose file $COMPOSE_FILE"; exit 1; }
124
- case "$BACKEND_DIR" in
125
- /*|../*|*/../*|*/..|.) echo "::error::backend-dir must be a safe relative path"; exit 1;;
126
- esac
127
- case "$COMPOSE_FILE" in
128
- /*|../*|*/../*|*/..) echo "::error::compose-file must be a safe relative path"; exit 1;;
129
- esac
130
- if [ "$COMPOSE_FILE" != "$BACKEND_DIR/docker-compose.yml" ] \
131
- && [ "$COMPOSE_FILE" != "docker-compose.yml" ]; then
132
- echo "::error::compose-file must be docker-compose.yml or $BACKEND_DIR/docker-compose.yml"
133
- exit 1
134
- fi
135
120
  case "${{ inputs.app-name }}" in
136
121
  *[!a-z0-9-]*|"") echo "::error::app-name must be [a-z0-9-]"; exit 1;;
137
122
  esac
138
- echo "compose-file=$COMPOSE_FILE" >> "$GITHUB_OUTPUT"
123
+ node "${{ github.action_path }}/layout.cjs"
139
124
 
140
125
  - name: Configure SSH
141
126
  shell: bash
@@ -155,7 +140,7 @@ runs:
155
140
  if: inputs.operation == 'deploy'
156
141
  shell: bash
157
142
  env:
158
- BACKEND_DIR: ${{ inputs.backend-dir }}
143
+ BACKEND_DIR: ${{ steps.layout.outputs.path }}
159
144
  COMPOSE_FILE: ${{ steps.layout.outputs.compose-file }}
160
145
  run: |
161
146
  set -euo pipefail
@@ -166,7 +151,7 @@ runs:
166
151
  $SSH "${{ inputs.ssh-user }}@${{ inputs.host }}" "mkdir -p '$DEST'"
167
152
  # --delete keeps the mirror true; exclude .env so server-side secrets
168
153
  # (generated DB password, etc.) survive across deploys.
169
- if [ "$COMPOSE_FILE" = "$BACKEND_DIR/docker-compose.yml" ]; then
154
+ if [ "${COMPOSE_FILE%/*}" = "$BACKEND_DIR" ]; then
170
155
  rsync -az --delete --exclude '.env' --exclude '.git' \
171
156
  --exclude '.backend-edge.json' --exclude '.backend-ingress.conf' --exclude '.backend-public.env' \
172
157
  -e "$SSH" "$BACKEND_DIR/" \
@@ -178,7 +163,7 @@ runs:
178
163
  -e "$SSH" "$BACKEND_DIR/" \
179
164
  "${{ inputs.ssh-user }}@${{ inputs.host }}:$DEST/$BACKEND_DIR/"
180
165
  rsync -az -e "$SSH" "$COMPOSE_FILE" \
181
- "${{ inputs.ssh-user }}@${{ inputs.host }}:$DEST/docker-compose.yml"
166
+ "${{ inputs.ssh-user }}@${{ inputs.host }}:$DEST/${COMPOSE_FILE##*/}"
182
167
  fi
183
168
  # Ship remote helpers after the application mirror. They are package
184
169
  # owned and kept out of the consumer's source tree.
@@ -206,6 +191,7 @@ runs:
206
191
  shell: bash
207
192
  env:
208
193
  RUNTIME_ENV: ${{ inputs.runtime-env }}
194
+ COMPOSE_FILE: ${{ steps.layout.outputs.compose-file }}
209
195
  BACKEND_PUBLIC_HEALTH_URL: ${{ inputs.public-health-url }}
210
196
  BACKEND_PUBLIC_BASE_URL: ${{ inputs.public-base-url }}
211
197
  BACKEND_EXPECTED_BUILD_SHA: ${{ inputs.expected-build-sha }}
@@ -230,7 +216,7 @@ runs:
230
216
  $SSH "${{ inputs.ssh-user }}@${{ inputs.host }}" \
231
217
  "bash '$DEST/remote_deploy.sh' \
232
218
  '${{ inputs.app-name }}' '${{ inputs.api-domain }}' \
233
- '${{ inputs.health-path }}' '${{ inputs.cert-email }}'"
219
+ '${{ inputs.health-path }}' '${{ inputs.cert-email }}' '${COMPOSE_FILE##*/}'"
234
220
 
235
221
  - name: Read fixed backend identity diagnostics
236
222
  id: identity_diagnostics
@@ -0,0 +1,72 @@
1
+ // One local-only layout decision for installation, workflow selection and action validation.
2
+ const { appendFileSync, existsSync, lstatSync } = require('node:fs');
3
+ const { join, resolve } = require('node:path');
4
+
5
+ const NAMES = ['compose.yaml', 'compose.yml', 'docker-compose.yaml', 'docker-compose.yml'];
6
+
7
+ function refusal(code) {
8
+ const error = new Error(code);
9
+ error.result = { schema: 'gowalk-cicd/backend-layout.v1', code,
10
+ next: 'Set backend.backend-dir and backend.compose-file in .gowalk-cicd.yml, then rerun the installer.' };
11
+ throw error;
12
+ }
13
+
14
+ function safePath(value) {
15
+ if (typeof value !== 'string' || !/^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*$/.test(value)
16
+ || value.split('/').some(part => part === '.' || part === '..')) refusal('backend_layout_path_invalid');
17
+ return value;
18
+ }
19
+
20
+ function present(root, path, directory = false) {
21
+ let current = root;
22
+ const parts = path.split('/');
23
+ for (const [index, part] of parts.entries()) {
24
+ current = join(current, part);
25
+ if (!existsSync(current)) return false;
26
+ const stat = lstatSync(current);
27
+ if (stat.isSymbolicLink()) refusal('backend_layout_symlink_refused');
28
+ if (index < parts.length - 1 && !stat.isDirectory()) return false;
29
+ }
30
+ const stat = lstatSync(current);
31
+ return directory ? stat.isDirectory() : stat.isFile();
32
+ }
33
+
34
+ function selectLayout(root, { backendDir = '', composeFile = '', required = false } = {}) {
35
+ root = resolve(root);
36
+ if (backendDir) safePath(backendDir);
37
+ if (composeFile) safePath(composeFile);
38
+ const directories = backendDir ? [backendDir] : ['backend', 'server'];
39
+ const layouts = [];
40
+ for (const directory of directories) {
41
+ if (!present(root, directory, true)) continue;
42
+ const files = NAMES.map(name => `${directory}/${name}`);
43
+ if (present(root, `${directory}/Dockerfile`)) files.push(...NAMES);
44
+ for (const file of files) {
45
+ if ((!composeFile || composeFile === file) && present(root, file)) {
46
+ layouts.push({ backendDir: directory, composeFile: file });
47
+ }
48
+ }
49
+ }
50
+ if (layouts.length > 1) refusal('backend_layout_ambiguous');
51
+ if (!layouts.length && (required || backendDir || composeFile)) refusal('backend_layout_missing');
52
+ return layouts[0] || null;
53
+ }
54
+
55
+ function main() {
56
+ try {
57
+ const layout = selectLayout(process.cwd(), { backendDir: process.env.BACKEND_DIR_INPUT || '',
58
+ composeFile: process.env.COMPOSE_FILE_INPUT || '', required: process.env.BACKEND_LAYOUT_REQUIRED === 'true' });
59
+ if (process.env.GITHUB_OUTPUT) {
60
+ const source = layout?.backendDir || '', compose = layout?.composeFile || '';
61
+ appendFileSync(process.env.GITHUB_OUTPUT, `path=${source}\ncompose=${compose}\ncompose-file=${compose}\n`);
62
+ }
63
+ console.log(JSON.stringify({ schema: 'gowalk-cicd/backend-layout.v1', layout }));
64
+ } catch (error) {
65
+ console.error(JSON.stringify(error.result || { schema: 'gowalk-cicd/backend-layout.v1',
66
+ code: 'backend_layout_unavailable' }));
67
+ process.exitCode = 1;
68
+ }
69
+ }
70
+
71
+ module.exports = { NAMES, selectLayout };
72
+ if (require.main === module) main();
@@ -12,6 +12,12 @@ DIR="/opt/gowalk-backends/$APP"
12
12
  source "$(dirname "${BASH_SOURCE[0]}")/deploy_evidence.sh"
13
13
  BACKEND_DEPLOY_PHASE=remote_setup
14
14
  cd "$DIR"
15
+ export COMPOSE_FILE="${5:-docker-compose.yml}"
16
+ case "$COMPOSE_FILE" in
17
+ compose.yaml|compose.yml|docker-compose.yaml|docker-compose.yml) ;;
18
+ *) echo "::error::unsupported backend compose filename"; exit 1;;
19
+ esac
20
+ test -f "$COMPOSE_FILE" || { echo "::error::selected backend compose file is missing"; exit 1; }
15
21
  source "$DIR/select_certbot_account.sh"
16
22
  source "$DIR/ensure_certificate.sh"
17
23
 
@@ -35,6 +35,26 @@ until the app explicitly replaces their references in this file. Keep passwords,
35
35
  signing material, tokens and private keys in GitHub Secrets. Reference their names;
36
36
  never put their values in this configuration.
37
37
 
38
+ Backend discovery accepts `compose.yaml`, `compose.yml`, `docker-compose.yaml` and
39
+ `docker-compose.yml` inside `backend/` or `server/`, or at the repository root with a Dockerfile in
40
+ exactly one of those source directories. Multiple matches require an explicit selection; no file is
41
+ renamed and no arbitrary precedence chooses the deployment. Existing named settings disambiguate it:
42
+
43
+ ```yaml
44
+ backend:
45
+ backend-dir: service
46
+ compose-file: service/compose.yml
47
+ ```
48
+
49
+ Use an existing source subdirectory and either its standard Compose file or a root standard Compose
50
+ file. Root Compose layouts also require that source directory's Dockerfile. `backend-dir: .`, parent
51
+ paths and symlinks are refused. Both generated backend entrypoints receive the selected settings.
52
+ For a custom source directory, retain its app-owned backend workflow path filter alongside the standard
53
+ `backend/**` and `server/**` defaults; layout selection does not change custom deployment triggers.
54
+ Run `npx --yes gowalk-cicd@<version> --dry-run`, then the same installer without `--dry-run`; a
55
+ `backend_layout_ambiguous` or `backend_layout_missing` receipt identifies selection repair before any
56
+ files are written. Keep runtime and workflow customizations in this configuration when rerunning it.
57
+
38
58
  Backend edge settings are optional and preserve legacy deployments when absent. When enabled,
39
59
  the infrastructure `api-domain` must be a fresh alias distinct from the retained legacy hostname.
40
60
  Provision its Cloudflare route/token first. Public health proves the exact `build_sha`; the app's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.156",
3
+ "version": "1.0.157",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,11 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import layout from '../backend-action/layout.cjs';
4
+ import { readYaml } from './config/yaml.mjs';
5
+
6
+ export function backendLayout(root) {
7
+ const file = '.gowalk-cicd.yml', path = join(root, file);
8
+ const settings = existsSync(path) ? readYaml(readFileSync(path, 'utf8'), file).value.backend || {} : {};
9
+ const local = key => typeof settings[key] === 'string' && !settings[key].includes('${{') ? settings[key] : '';
10
+ return layout.selectLayout(root, { backendDir: local('backend-dir'), composeFile: local('compose-file') });
11
+ }
@@ -36,6 +36,16 @@ export function applySettings(config, file, workflow) {
36
36
  const value = at(config, name);
37
37
  if (value !== undefined) assign(workflow, { path, value });
38
38
  }
39
+ const job = file === 'deploy.yml' ? 'backend-preview' : file === 'deploy-backend.yml' ? 'deploy' : null;
40
+ if (job) {
41
+ for (const [key, output, variable] of [['backend-dir', 'path', 'BACKEND_DIR_INPUT'],
42
+ ['compose-file', 'compose', 'COMPOSE_FILE_INPUT']]) {
43
+ const value = config.backend?.[key];
44
+ if (value !== undefined && value !== `\${{ steps.dir.outputs.${output} }}`) {
45
+ assign(workflow, { path: ['jobs', job, 'steps', 'items', 'id:dir', 'env', variable], value });
46
+ }
47
+ }
48
+ }
39
49
  return workflow;
40
50
  }
41
51
 
package/src/install.mjs CHANGED
@@ -21,6 +21,7 @@ import { fileURLToPath } from 'node:url';
21
21
  import { dirname } from 'node:path';
22
22
  import { planWorkflows, writeWorkflows } from './config/workflows.mjs';
23
23
  import { recoverWorkflows } from './config/transaction.mjs';
24
+ import { backendLayout } from './backend-layout.mjs';
24
25
 
25
26
  const __filename = fileURLToPath(import.meta.url);
26
27
  const __dirname = dirname(__filename);
@@ -40,26 +41,6 @@ const VERSION_MARKER_REL = join('.github', 'actions', 'swift-app', '.daemux-vers
40
41
  const ANDROID_VERSION_MARKER_REL = join('.github', 'actions', 'android-app', '.daemux-version');
41
42
  const BACKEND_VERSION_MARKER_REL = join('.github', 'actions', 'backend-app', '.daemux-version');
42
43
 
43
- // A repo has a backend when backend/ or server/ either carries the compose file
44
- // or is referenced by a root compose file. The latter is Flutter's common
45
- // full-stack layout: docker-compose.yml stays at the repo root so it can build
46
- // ./backend while the deploy action still syncs only backend-owned files.
47
- function backendLayout(repoRoot) {
48
- for (const d of ['backend', 'server']) {
49
- if (existsSync(join(repoRoot, d, 'docker-compose.yml'))) {
50
- return { backendDir: d, composeFile: join(d, 'docker-compose.yml') };
51
- }
52
- }
53
- if (existsSync(join(repoRoot, 'docker-compose.yml'))) {
54
- for (const d of ['backend', 'server']) {
55
- if (existsSync(join(repoRoot, d, 'Dockerfile'))) {
56
- return { backendDir: d, composeFile: 'docker-compose.yml' };
57
- }
58
- }
59
- }
60
- return null;
61
- }
62
-
63
44
  // Lines we treat as ignoring credentials. If any appears in the consumer's
64
45
  // .gitignore, CI will not see one or both platform keys. We warn instead of
65
46
  // auto-editing, because
@@ -10,6 +10,9 @@ on:
10
10
  - "backend/**"
11
11
  - "server/**"
12
12
  - "docker-compose.yml"
13
+ - "docker-compose.yaml"
14
+ - "compose.yml"
15
+ - "compose.yaml"
13
16
  - ".github/workflows/deploy-backend.yml"
14
17
  - ".github/actions/backend-app/**"
15
18
  - "!.github/actions/backend-app/.daemux-version"
@@ -47,23 +50,9 @@ jobs:
47
50
  # Pick the source dir and either its nested compose file or a root one.
48
51
  - id: dir
49
52
  if: inputs.operation == '' || inputs.operation == 'deploy'
50
- run: |
51
- if [ -f backend/docker-compose.yml ]; then
52
- echo "path=backend" >> "$GITHUB_OUTPUT"
53
- echo "compose=backend/docker-compose.yml" >> "$GITHUB_OUTPUT"
54
- elif [ -f server/docker-compose.yml ]; then
55
- echo "path=server" >> "$GITHUB_OUTPUT"
56
- echo "compose=server/docker-compose.yml" >> "$GITHUB_OUTPUT"
57
- elif [ -f docker-compose.yml ] && [ -f backend/Dockerfile ]; then
58
- echo "path=backend" >> "$GITHUB_OUTPUT"
59
- echo "compose=docker-compose.yml" >> "$GITHUB_OUTPUT"
60
- elif [ -f docker-compose.yml ] && [ -f server/Dockerfile ]; then
61
- echo "path=server" >> "$GITHUB_OUTPUT"
62
- echo "compose=docker-compose.yml" >> "$GITHUB_OUTPUT"
63
- else
64
- echo "::error::no supported backend compose layout"
65
- exit 1
66
- fi
53
+ env:
54
+ BACKEND_LAYOUT_REQUIRED: "true"
55
+ run: node .github/actions/backend-app/layout.cjs
67
56
 
68
57
  - name: Deploy backend
69
58
  uses: ./.github/actions/backend-app
@@ -687,18 +687,8 @@ jobs:
687
687
  - id: dir
688
688
  shell: bash
689
689
  run: |
690
- if [ -f backend/docker-compose.yml ]; then
691
- echo "path=backend" >> "$GITHUB_OUTPUT"
692
- echo "compose=backend/docker-compose.yml" >> "$GITHUB_OUTPUT"
693
- elif [ -f server/docker-compose.yml ]; then
694
- echo "path=server" >> "$GITHUB_OUTPUT"
695
- echo "compose=server/docker-compose.yml" >> "$GITHUB_OUTPUT"
696
- elif [ -f docker-compose.yml ] && [ -f backend/Dockerfile ]; then
697
- echo "path=backend" >> "$GITHUB_OUTPUT"
698
- echo "compose=docker-compose.yml" >> "$GITHUB_OUTPUT"
699
- elif [ -f docker-compose.yml ] && [ -f server/Dockerfile ]; then
700
- echo "path=server" >> "$GITHUB_OUTPUT"
701
- echo "compose=docker-compose.yml" >> "$GITHUB_OUTPUT"
690
+ if [ -f .github/actions/backend-app/layout.cjs ]; then
691
+ node .github/actions/backend-app/layout.cjs
702
692
  else
703
693
  echo "path=" >> "$GITHUB_OUTPUT"
704
694
  echo "compose=" >> "$GITHUB_OUTPUT"
Binary file