gowalk-cicd 1.0.155 → 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 +6 -0
- package/README.md +27 -3
- 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 +10 -24
- package/backend-action/layout.cjs +72 -0
- package/backend-action/network_prepare.py +2 -1
- package/backend-action/network_test_fixture.py +2 -1
- package/backend-action/remote_deploy.sh +6 -0
- package/backend-action/test_network_prepare.py +21 -0
- package/docs/BACKEND_NETWORKS.md +15 -4
- package/docs/CONFIGURATION.md +20 -0
- package/package.json +1 -1
- package/src/backend-layout.mjs +11 -0
- package/src/config/settings.mjs +10 -0
- package/src/install.mjs +1 -20
- package/templates/deploy-backend.yml +6 -17
- package/templates/deploy.yml +2 -12
- package/templates/history.json.gz +0 -0
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
|
|
170
|
-
- it can live at the repository root as `
|
|
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
|
|
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,
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.157
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.157
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.157
|
|
@@ -49,8 +49,8 @@ inputs:
|
|
|
49
49
|
default: "backend"
|
|
50
50
|
compose-file:
|
|
51
51
|
description: >
|
|
52
|
-
Compose file path.
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: ${{
|
|
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
|
|
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
|
|
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();
|
|
@@ -122,7 +122,8 @@ def prepare(config, app, *, reader=collect, owner_reader=ownership_inventory, co
|
|
|
122
122
|
inventory = reader(command, require_pools=False)
|
|
123
123
|
if inventory.get('pool_failure'):
|
|
124
124
|
raise NetworkFailure(inventory['pool_failure'])
|
|
125
|
-
|
|
125
|
+
# Compose 2 and 5 reuse a project network that carries no config-hash label (docs/BACKEND_NETWORKS.md).
|
|
126
|
+
if not re.fullmatch(r'v?(?:2|5)\.\d+\.\d+', inventory['compose_version']):
|
|
126
127
|
raise NetworkFailure('compose_version_unsupported')
|
|
127
128
|
rows.append(create_one(plan, inventory, reader, command))
|
|
128
129
|
inventory = reader(command, require_pools=False)
|
|
@@ -28,9 +28,10 @@ class Host:
|
|
|
28
28
|
self.calls = []
|
|
29
29
|
self.pool_failure = ''
|
|
30
30
|
self.lose_reply = False
|
|
31
|
+
self.compose_version = '2.39.4'
|
|
31
32
|
|
|
32
33
|
def inventory(self, command, require_pools=True):
|
|
33
|
-
return {'docker_version': '28.0.4', 'compose_version':
|
|
34
|
+
return {'docker_version': '28.0.4', 'compose_version': self.compose_version, 'observed_at': '2026-09-15T00:00:00Z',
|
|
34
35
|
'python_version': '3.8.20', 'pool_source': 'fixture', 'pool_failure': self.pool_failure,
|
|
35
36
|
'pools': [{'base': '172.28.0.0/22', 'size': 16}], 'networks': copy.deepcopy(self.networks),
|
|
36
37
|
'host_excluded': [], 'host_records': [],
|
|
@@ -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
|
|
|
@@ -85,6 +85,27 @@ class PreparationTests(unittest.TestCase):
|
|
|
85
85
|
self.assertEqual(set(values), {'172.28.1.0/24', '172.28.2.0/24'})
|
|
86
86
|
self.assertEqual(len(host.calls), 2)
|
|
87
87
|
|
|
88
|
+
def test_reviewed_compose_majors_allocate_and_others_refuse_before_create(self):
|
|
89
|
+
for version in ('2.39.4', 'v2.40.3', '5.0.0', '5.3.1', 'v5.5.1'):
|
|
90
|
+
host = Host()
|
|
91
|
+
host.compose_version = version
|
|
92
|
+
result = prepare(config('first'), 'first', **host.arguments())
|
|
93
|
+
self.assertTrue(result['ok'])
|
|
94
|
+
self.assertEqual(result['compose_version'], version)
|
|
95
|
+
self.assertEqual(host.networks[-1]['Labels']['com.docker.compose.version'], version)
|
|
96
|
+
self.assertNotIn('com.docker.compose.config-hash', host.networks[-1]['Labels'])
|
|
97
|
+
self.assertEqual(len(host.calls), 1)
|
|
98
|
+
for version in ('1.29.2', '3.0.0', '4.1.0', '6.0.0', '5.3', '25.0.0'):
|
|
99
|
+
host = Host()
|
|
100
|
+
host.compose_version = version
|
|
101
|
+
with self.assertRaisesRegex(NetworkFailure, 'compose_version_unsupported'):
|
|
102
|
+
prepare(config('first'), 'first', **host.arguments())
|
|
103
|
+
self.assertEqual(host.calls, [])
|
|
104
|
+
host.networks.append(network('existing', '172.29.0.0/16'))
|
|
105
|
+
preserved = prepare(config('existing'), 'existing', **host.arguments())
|
|
106
|
+
self.assertEqual(preserved['networks'][0]['state'], 'preserved')
|
|
107
|
+
self.assertEqual(host.calls, [])
|
|
108
|
+
|
|
88
109
|
def test_full_effective_pool_and_unknown_provenance_refuse_expansion(self):
|
|
89
110
|
for unknown in (False, True):
|
|
90
111
|
host = Host()
|
package/docs/BACKEND_NETWORKS.md
CHANGED
|
@@ -28,10 +28,21 @@ fingerprint. A later change to its network contract refuses before Compose rathe
|
|
|
28
28
|
ignoring authored IPAM or replacing a network that may carry active workloads. An app requiring a
|
|
29
29
|
different network must implement that deliberate migration in its own deployment.
|
|
30
30
|
|
|
31
|
-
The helper intentionally does not manufacture `com.docker.compose.config-hash`. Compose 2
|
|
32
|
-
network with the correct project/network labels and no hash. A wrong hash can trigger
|
|
33
|
-
Its own fingerprint protects replay, including configuration changes.
|
|
34
|
-
|
|
31
|
+
The helper intentionally does not manufacture `com.docker.compose.config-hash`. Compose 2 and Compose 5
|
|
32
|
+
accept a network with the correct project/network labels and no hash. A wrong hash can trigger
|
|
33
|
+
replacement. Its own fingerprint protects replay, including configuration changes. New-network
|
|
34
|
+
preparation admits Compose 2.x and 5.x; any other major version refuses with
|
|
35
|
+
`compose_version_unsupported` until it has been reviewed. Existing selected networks are preserved
|
|
36
|
+
under every Compose version because preservation never allocates.
|
|
37
|
+
|
|
38
|
+
Compose 5 review (2026-09-17, docker/compose tags v5.0.0, v5.3.1 and v5.5.1 against v2.40.3):
|
|
39
|
+
`resolveOrCreateNetwork` in `pkg/compose/create.go` at v5.0.0 and v5.3.1 returns the inspected
|
|
40
|
+
network when its `com.docker.compose.network` label matches and its config-hash label is empty or
|
|
41
|
+
equal (`hash == "" || hash == expected`), the same branch as v2.40.3; the remaining differences are
|
|
42
|
+
Docker client types, IPAM parsing and event emission. v5.5.1 moved reuse into `reconcileNetworks`
|
|
43
|
+
in `pkg/compose/reconcile.go`, whose observed state is scoped by the project label and which leaves a
|
|
44
|
+
network with no recorded hash untouched (`observed.ConfigHash == ""`). docker/compose carries no v3 or
|
|
45
|
+
v4 tags, and Compose 1 (`docker-compose`) stays unreviewed.
|
|
35
46
|
|
|
36
47
|
## Address-space proof
|
|
37
48
|
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -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
|
@@ -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
|
+
}
|
package/src/config/settings.mjs
CHANGED
|
@@ -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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
package/templates/deploy.yml
CHANGED
|
@@ -687,18 +687,8 @@ jobs:
|
|
|
687
687
|
- id: dir
|
|
688
688
|
shell: bash
|
|
689
689
|
run: |
|
|
690
|
-
if [ -f backend/
|
|
691
|
-
|
|
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
|