gowalk-cicd 1.0.197 → 1.0.199
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 +4 -0
- package/backend-action/action.yml +6 -2
- package/backend-action/identity_diagnostics.py +17 -9
- package/backend-action/runtime_diagnostics.py +35 -4
- package/bin/cli.mjs +6 -1
- package/docs/BACKEND_SSH_GATE.md +13 -1
- package/docs/CONFIGURATION.md +17 -0
- package/package.json +1 -1
- package/src/config/transaction.mjs +7 -3
- package/src/config/workflows.mjs +13 -4
- package/src/install.mjs +20 -11
package/README.md
CHANGED
|
@@ -23,6 +23,10 @@ From the root of your app repo:
|
|
|
23
23
|
npx --yes gowalk-cicd
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
To update only backend delivery while preserving mobile tooling and workflows, use
|
|
27
|
+
`npx --yes gowalk-cicd --backend-only`. The lock records the backend release separately;
|
|
28
|
+
see [scoped installation](docs/CONFIGURATION.md).
|
|
29
|
+
|
|
26
30
|
Writes three core things into your repo:
|
|
27
31
|
|
|
28
32
|
- `.github/actions/swift-app/` — the vendored composite action (action.yml + scripts)
|
|
@@ -306,9 +306,11 @@ runs:
|
|
|
306
306
|
run: |
|
|
307
307
|
RESULT_DIR="$(mktemp -d "$RUNNER_TEMP/backend-runtime-diagnostics.XXXXXX")"
|
|
308
308
|
echo "result=$RESULT_DIR/result.json" >> "$GITHUB_OUTPUT"
|
|
309
|
+
echo "transport=$RESULT_DIR/transport.json" >> "$GITHUB_OUTPUT"
|
|
309
310
|
python3 "${{ github.action_path }}/runtime_diagnostics.py" \
|
|
310
311
|
--app "$DIAGNOSTIC_APP" --service "$DIAGNOSTIC_SERVICE" --expected-build-sha "$DIAGNOSTIC_BUILD" \
|
|
311
|
-
--host "$DIAGNOSTIC_HOST" --user "$DIAGNOSTIC_USER" --output "$RESULT_DIR/result.json"
|
|
312
|
+
--host "$DIAGNOSTIC_HOST" --user "$DIAGNOSTIC_USER" --output "$RESULT_DIR/result.json" \
|
|
313
|
+
--transport-output "$RESULT_DIR/transport.json"
|
|
312
314
|
|
|
313
315
|
- name: Retain scoped backend runtime result
|
|
314
316
|
if: always() && inputs.operation == 'runtime-diagnostics'
|
|
@@ -317,7 +319,9 @@ runs:
|
|
|
317
319
|
NODE_OPTIONS: --require "${{ github.action_path }}/scripts/artifact_env.cjs"
|
|
318
320
|
with:
|
|
319
321
|
name: backend-runtime-diagnostics-${{ github.job }}-${{ github.run_attempt }}
|
|
320
|
-
path:
|
|
322
|
+
path: |
|
|
323
|
+
${{ steps.runtime_diagnostics.outputs.result }}
|
|
324
|
+
${{ steps.runtime_diagnostics.outputs.transport }}
|
|
321
325
|
if-no-files-found: error
|
|
322
326
|
|
|
323
327
|
- name: Read scoped backend network prerequisites
|
|
@@ -40,7 +40,7 @@ class ReaderFailure(Exception):
|
|
|
40
40
|
super().__init__(self.code)
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
def transfer(selector, process, payload, offset, data):
|
|
43
|
+
def transfer(selector, process, payload, offset, data, errors):
|
|
44
44
|
for key, _ in selector.select(0.1):
|
|
45
45
|
if key.fileobj is process.stdin:
|
|
46
46
|
offset += os.write(process.stdin.fileno(), payload[offset:offset + 4096])
|
|
@@ -48,23 +48,27 @@ def transfer(selector, process, payload, offset, data):
|
|
|
48
48
|
selector.unregister(process.stdin)
|
|
49
49
|
process.stdin.close()
|
|
50
50
|
else:
|
|
51
|
-
chunk = os.read(
|
|
51
|
+
chunk = os.read(key.fileobj.fileno(), 65536)
|
|
52
52
|
if not chunk:
|
|
53
|
-
selector.unregister(
|
|
54
|
-
data.extend(chunk)
|
|
55
|
-
if len(data) > LIMIT:
|
|
53
|
+
selector.unregister(key.fileobj)
|
|
54
|
+
(errors if key.fileobj is process.stderr else data).extend(chunk)
|
|
55
|
+
if len(data) + len(errors) > LIMIT:
|
|
56
56
|
raise ReaderFailure('output_limit')
|
|
57
57
|
return offset
|
|
58
58
|
|
|
59
59
|
|
|
60
|
-
def bounded(argv, *, payload=None, seconds=30, with_status=False):
|
|
60
|
+
def bounded(argv, *, payload=None, seconds=30, with_status=False, with_stderr=False):
|
|
61
61
|
"""Bound output, runtime and owned process cleanup without printing stderr."""
|
|
62
62
|
process = subprocess.Popen(argv, stdin=subprocess.PIPE if payload else subprocess.DEVNULL,
|
|
63
|
-
stdout=subprocess.PIPE,
|
|
64
|
-
|
|
63
|
+
stdout=subprocess.PIPE,
|
|
64
|
+
stderr=subprocess.PIPE if with_stderr else subprocess.DEVNULL,
|
|
65
|
+
start_new_session=True)
|
|
66
|
+
deadline, data, errors = time.monotonic() + seconds, bytearray(), bytearray()
|
|
65
67
|
try:
|
|
66
68
|
with selectors.DefaultSelector() as selector:
|
|
67
69
|
selector.register(process.stdout, selectors.EVENT_READ)
|
|
70
|
+
if with_stderr:
|
|
71
|
+
selector.register(process.stderr, selectors.EVENT_READ)
|
|
68
72
|
if payload:
|
|
69
73
|
os.set_blocking(process.stdin.fileno(), False)
|
|
70
74
|
selector.register(process.stdin, selectors.EVENT_WRITE)
|
|
@@ -72,12 +76,14 @@ def bounded(argv, *, payload=None, seconds=30, with_status=False):
|
|
|
72
76
|
while selector.get_map():
|
|
73
77
|
if time.monotonic() >= deadline:
|
|
74
78
|
raise ReaderFailure('command_timeout')
|
|
75
|
-
offset = transfer(selector, process, payload, offset, data)
|
|
79
|
+
offset = transfer(selector, process, payload, offset, data, errors)
|
|
76
80
|
try:
|
|
77
81
|
status = process.wait(timeout=max(0.01, deadline - time.monotonic()))
|
|
78
82
|
except subprocess.TimeoutExpired:
|
|
79
83
|
raise ReaderFailure('command_timeout') from None
|
|
80
84
|
if with_status:
|
|
85
|
+
if with_stderr:
|
|
86
|
+
return status, bytes(data), bytes(errors)
|
|
81
87
|
return status, bytes(data)
|
|
82
88
|
if status:
|
|
83
89
|
raise ReaderFailure('command_failed')
|
|
@@ -90,6 +96,8 @@ def bounded(argv, *, payload=None, seconds=30, with_status=False):
|
|
|
90
96
|
pass
|
|
91
97
|
process.wait(timeout=5)
|
|
92
98
|
process.stdout.close()
|
|
99
|
+
if process.stderr:
|
|
100
|
+
process.stderr.close()
|
|
93
101
|
if process.stdin and not process.stdin.closed:
|
|
94
102
|
process.stdin.close()
|
|
95
103
|
|
|
@@ -3,27 +3,50 @@ import argparse
|
|
|
3
3
|
import json
|
|
4
4
|
import re
|
|
5
5
|
import shlex
|
|
6
|
+
import sys
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
from identity_diagnostics import ReaderFailure, bounded, retain_result
|
|
9
10
|
from runtime_projection import RuntimeFailure, envelope, scope, validate
|
|
10
11
|
|
|
11
12
|
|
|
12
|
-
def
|
|
13
|
+
def transport_signals(status, raw):
|
|
14
|
+
"""Observe fixed SSH failure labels; private stderr never enters a receipt."""
|
|
15
|
+
lines = raw.decode('utf-8', errors='replace').splitlines()
|
|
16
|
+
patterns = {
|
|
17
|
+
1: [('gate_refused', r'backend_ssh_refused')],
|
|
18
|
+
127: [('remote_command_unavailable',
|
|
19
|
+
r'(?:/bin/)?(?:bash|sh): (?:(?:line )?\d+: )?gowalk-backend: (?:command )?not found')],
|
|
20
|
+
255: [
|
|
21
|
+
('authentication_refused', r'[^\r\n]+: Permission denied \([a-z,-]+\)\.'),
|
|
22
|
+
('host_key_refused', r'Host key verification failed\.'),
|
|
23
|
+
('connection_refused', r'ssh: connect to host [^\r\n ]+ port \d+: Connection refused'),
|
|
24
|
+
('connection_timeout', r'ssh: connect to host [^\r\n ]+ port \d+: (?:Connection|Operation) timed out'),
|
|
25
|
+
('name_resolution_failed', r'ssh: Could not resolve hostname [^\r\n ]+: [^\r\n]+'),
|
|
26
|
+
],
|
|
27
|
+
}
|
|
28
|
+
return [name for name, pattern in patterns.get(status, [])
|
|
29
|
+
if any(re.fullmatch(pattern, line) for line in lines)]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def local(args, transport=None):
|
|
13
33
|
scope(args.app, args.service, args.expected_build_sha)
|
|
14
34
|
if (not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9.-]{0,252}', args.host or '')
|
|
15
35
|
or not re.fullmatch(r'[a-z_][a-z0-9_-]{0,31}', args.user)):
|
|
16
36
|
raise RuntimeFailure('invalid_scope')
|
|
17
37
|
command = shlex.join(['gowalk-backend', 'runtime-diagnostics', args.app, args.service, args.expected_build_sha])
|
|
18
38
|
try:
|
|
19
|
-
status, raw = bounded(['ssh', '-T', '-i', str(Path.home() / '.ssh/backend_deploy'),
|
|
39
|
+
status, raw, errors = bounded(['ssh', '-T', '-i', str(Path.home() / '.ssh/backend_deploy'),
|
|
20
40
|
'-o', 'IdentitiesOnly=yes', '-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=yes',
|
|
21
41
|
'-o', 'ConnectTimeout=10', '-o', 'ServerAliveInterval=5', '-o', 'ServerAliveCountMax=2',
|
|
22
|
-
'-o', 'ClearAllForwardings=yes', args.user + '@' + args.host, command],
|
|
42
|
+
'-o', 'ClearAllForwardings=yes', args.user + '@' + args.host, command],
|
|
43
|
+
seconds=75, with_status=True, with_stderr=True)
|
|
23
44
|
except ReaderFailure as error:
|
|
24
45
|
raise RuntimeFailure(error.code) from None
|
|
25
46
|
except OSError:
|
|
26
47
|
raise RuntimeFailure('transport_failure') from None
|
|
48
|
+
if transport is not None:
|
|
49
|
+
transport.update(exit_status=status, signals=transport_signals(status, errors))
|
|
27
50
|
if status not in (0, 1):
|
|
28
51
|
raise RuntimeFailure('transport_failure')
|
|
29
52
|
if status == 1 and not raw.strip():
|
|
@@ -45,14 +68,22 @@ def main():
|
|
|
45
68
|
parser.add_argument('--host', required=True)
|
|
46
69
|
parser.add_argument('--user', default='root')
|
|
47
70
|
parser.add_argument('--output', required=True)
|
|
71
|
+
parser.add_argument('--transport-output', help='Separate closed SSH observation receipt')
|
|
48
72
|
args = parser.parse_args()
|
|
73
|
+
transport = {'schema': 'gowalk-cicd/backend-runtime-transport.v1', 'exit_status': None, 'signals': []}
|
|
49
74
|
try:
|
|
50
|
-
result = local(args)
|
|
75
|
+
result = local(args, transport)
|
|
51
76
|
except Exception as error:
|
|
52
77
|
result = envelope(args.app, args.service, args.expected_build_sha,
|
|
53
78
|
error.code if isinstance(error, RuntimeFailure) else 'unclassified', reader_version=None)
|
|
54
79
|
try:
|
|
55
80
|
retain_result(args.output, result)
|
|
81
|
+
if args.transport_output:
|
|
82
|
+
retain_result(args.transport_output, transport)
|
|
83
|
+
status = 'unknown' if transport['exit_status'] is None else str(transport['exit_status'])
|
|
84
|
+
print('::notice title=backend_runtime_transport::schema=' + transport['schema']
|
|
85
|
+
+ ' exit_status=' + status + ' signals=' + (','.join(transport['signals']) or 'none'),
|
|
86
|
+
file=sys.stderr)
|
|
56
87
|
except ReaderFailure:
|
|
57
88
|
result = envelope(args.app, args.service, args.expected_build_sha, 'artifact_write_failed', reader_version=None)
|
|
58
89
|
print(json.dumps(result, separators=(',', ':')))
|
package/bin/cli.mjs
CHANGED
|
@@ -31,6 +31,7 @@ if (args[0] === 'release') {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
let dryRun = false;
|
|
34
|
+
let backendOnly = false;
|
|
34
35
|
|
|
35
36
|
for (const arg of args) {
|
|
36
37
|
switch (arg) {
|
|
@@ -59,6 +60,7 @@ Options:
|
|
|
59
60
|
release [ios|android|both] Prepare a mobile release intent on the task branch
|
|
60
61
|
validate run|check|publish|verify Run local focused checks or verify their authenticated source receipt
|
|
61
62
|
--dry-run Print what would happen without writing files
|
|
63
|
+
--backend-only Update backend tooling without changing mobile tooling or workflows
|
|
62
64
|
-v, --version Show version number
|
|
63
65
|
-h, --help Show this help message
|
|
64
66
|
|
|
@@ -75,6 +77,9 @@ After install:
|
|
|
75
77
|
case '--dry-run':
|
|
76
78
|
dryRun = true;
|
|
77
79
|
break;
|
|
80
|
+
case '--backend-only':
|
|
81
|
+
backendOnly = true;
|
|
82
|
+
break;
|
|
78
83
|
default:
|
|
79
84
|
console.error(`Unknown option: ${arg}`);
|
|
80
85
|
console.error(`Run with --help for usage.`);
|
|
@@ -84,7 +89,7 @@ After install:
|
|
|
84
89
|
|
|
85
90
|
const { runInstall } = await import('../src/install.mjs');
|
|
86
91
|
try {
|
|
87
|
-
await runInstall({ dryRun });
|
|
92
|
+
await runInstall({ dryRun, backendOnly });
|
|
88
93
|
} catch (error) {
|
|
89
94
|
console.error(JSON.stringify(error.result || {
|
|
90
95
|
schema: 'gowalk-cicd/migration-repair.v1', code: 'installer_unavailable',
|
package/docs/BACKEND_SSH_GATE.md
CHANGED
|
@@ -68,7 +68,7 @@ consumer code or an uploaded script. An older/refusing gate has no reader receip
|
|
|
68
68
|
`runtime_reader_unavailable`; locally synthesized failures carry `reader_version: null`, never host-adoption proof.
|
|
69
69
|
A collector refusal carries its actual `reader_version` and fixed failure code.
|
|
70
70
|
|
|
71
|
-
The `backend-runtime-diagnostics-<job>-<attempt>` artifact contains
|
|
71
|
+
The `backend-runtime-diagnostics-<job>-<attempt>` artifact contains `result.json`, schema
|
|
72
72
|
`gowalk-cicd/backend-runtime-diagnostics.v1`. Success binds the expected/observed build to the running API's
|
|
73
73
|
`BUILD_SHA`, enumerates at most 32 exact-project containers (including stopped ones), and rechecks their exact
|
|
74
74
|
project/service/working-directory labels. It returns container and image IDs, effective memory/reservation/CPU/PID
|
|
@@ -77,6 +77,18 @@ Docker's display precision. CPU samples are Docker's bounded observations, not a
|
|
|
77
77
|
collector never launches workload, reads logs, executes in containers or inspects a shared database's cgroup.
|
|
78
78
|
Both container identity/configuration and the private profile are rechecked before returning success.
|
|
79
79
|
|
|
80
|
+
The same artifact also retains `transport.json`, schema `gowalk-cicd/backend-runtime-transport.v1`.
|
|
81
|
+
Its fixed fields also appear in the brace-free `backend_runtime_transport` notice. It reports the SSH child's
|
|
82
|
+
`exit_status` (null when no complete exit was observed) and fixed `signals`: `gate_refused`,
|
|
83
|
+
`remote_command_unavailable`, `authentication_refused`, `host_key_refused`, `connection_refused`,
|
|
84
|
+
`connection_timeout` or `name_resolution_failed`. Unknown text yields no signal; stderr and remote
|
|
85
|
+
command output never enter this receipt. The original `result.json` schema and failure codes remain
|
|
86
|
+
unchanged. These observations explain a refused read, never attest host reader installation or authorize
|
|
87
|
+
a retry or transport bypass. Exit 127 without the exact command-not-found observation does not prove a
|
|
88
|
+
missing reader; a gate refusal does not identify its policy or ownership cause. Normal backend deployment
|
|
89
|
+
does not install the trusted reader, and an updated workflow client alone cannot repair an unavailable
|
|
90
|
+
host command. Install the published host runtime through the procedure below.
|
|
91
|
+
|
|
80
92
|
`database.mode: host_profile` with `binding_matches_profile: true` proves this app's root-owned profile supplied
|
|
81
93
|
the API's current private `DATABASE_URL`. `removed_services` describes that profile's applied removals; a remaining
|
|
82
94
|
removed service refuses. `app_service` means the runtime connection names another reported service.
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -5,6 +5,23 @@ extracts supported settings from existing workflows into `.gowalk-cicd.yml` and
|
|
|
5
5
|
renders the managed workflows in the same change. Later upgrades read that file.
|
|
6
6
|
The installer does this locally; app CI gains no configuration job or preflight.
|
|
7
7
|
|
|
8
|
+
For backend-only work in a repository whose mobile tooling must stay unchanged, run
|
|
9
|
+
`npx --yes gowalk-cicd@<version> --backend-only` (also accepts `--dry-run`). It updates only
|
|
10
|
+
`.github/actions/backend-app/`, `deploy-backend.yml` and their configuration/lock state.
|
|
11
|
+
Mobile actions, workflows, validation workflows and release intents retain their exact bytes.
|
|
12
|
+
Existing backend settings and workflow overrides are preserved by the normal migration rules.
|
|
13
|
+
The separate backend deploy still follows its existing protected merge and push trigger.
|
|
14
|
+
|
|
15
|
+
For this scoped installation, `.gowalk-cicd.lock.json` records `backend_package`; `package`
|
|
16
|
+
continues to name the last full installation. Verify `backend_package` (or `package` after a
|
|
17
|
+
full install), the backend action's `.daemux-version`, the published package integrity and the
|
|
18
|
+
installed backend bytes. A scoped backend upgrade makes no claim that mobile tooling is current.
|
|
19
|
+
The next full installation updates all tooling and removes the scoped version override.
|
|
20
|
+
Repositories with existing managed mobile workflows require their existing `.gowalk-cicd.yml`
|
|
21
|
+
configuration before scoped installation; it never partially migrates an unconfigured mobile
|
|
22
|
+
installation. An interrupted full installation also refuses a backend-only retry, preserving
|
|
23
|
+
its journal instead of applying mobile writes outside the requested scope.
|
|
24
|
+
|
|
8
25
|
```yaml
|
|
9
26
|
schema: 1
|
|
10
27
|
mobile:
|
package/package.json
CHANGED
|
@@ -44,7 +44,7 @@ function apply(root, writes, dryRun) {
|
|
|
44
44
|
if (!dryRun) unlinkSync(join(root, JOURNAL));
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export function recoverWorkflows(root, dryRun) {
|
|
47
|
+
export function recoverWorkflows(root, dryRun, { backendOnly = false } = {}) {
|
|
48
48
|
if (!existsSync(join(root, JOURNAL))) return false;
|
|
49
49
|
let journal;
|
|
50
50
|
try { journal = JSON.parse(readFileSync(join(root, JOURNAL), 'utf8')); }
|
|
@@ -52,14 +52,18 @@ export function recoverWorkflows(root, dryRun) {
|
|
|
52
52
|
if (journal.schema !== 1 || !Array.isArray(journal.writes)) {
|
|
53
53
|
throw new MigrationRepair('invalid_pending_migration', JOURNAL);
|
|
54
54
|
}
|
|
55
|
+
if (backendOnly && journal.scope !== 'backend') {
|
|
56
|
+
throw new MigrationRepair('pending_migration_scope_mismatch', JOURNAL);
|
|
57
|
+
}
|
|
55
58
|
apply(root, journal.writes, dryRun);
|
|
56
59
|
return true;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
export function writeWorkflows(root, writes, dryRun) {
|
|
62
|
+
export function writeWorkflows(root, writes, dryRun, { backendOnly = false } = {}) {
|
|
60
63
|
if (!writes.length) return;
|
|
61
64
|
const planned = writes.map(write => ({ ...write, before: current(join(root, write.file)) }));
|
|
62
65
|
for (const write of planned) validate(root, write);
|
|
63
|
-
if (!dryRun) atomicWrite(join(root, JOURNAL), JSON.stringify({ schema: 1,
|
|
66
|
+
if (!dryRun) atomicWrite(join(root, JOURNAL), JSON.stringify({ schema: 1,
|
|
67
|
+
...(backendOnly ? { scope: 'backend' } : {}), writes: planned }));
|
|
64
68
|
apply(root, planned, dryRun);
|
|
65
69
|
}
|
package/src/config/workflows.mjs
CHANGED
|
@@ -80,9 +80,14 @@ function upgradeWorkflow(root, file, templateDir, state) {
|
|
|
80
80
|
return { file: `.github/workflows/${file}`, content: output };
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
export function planWorkflows(root, templateDir, files, version) {
|
|
83
|
+
export function planWorkflows(root, templateDir, files, version, { backendOnly = false } = {}) {
|
|
84
84
|
const state = configState(root);
|
|
85
|
-
|
|
85
|
+
if (backendOnly && state.migrating && MANAGED.some(file => file !== 'deploy-backend.yml'
|
|
86
|
+
&& load(root, `.github/workflows/${file}`) !== null)) {
|
|
87
|
+
throw new MigrationRepair('backend_scope_requires_existing_config', CONFIG);
|
|
88
|
+
}
|
|
89
|
+
const selected = backendOnly ? ['deploy-backend.yml']
|
|
90
|
+
: [...new Set([...files, ...Object.keys(state.config.workflows)])];
|
|
86
91
|
for (const file of selected) {
|
|
87
92
|
if (!MANAGED.includes(file)) throw new MigrationRepair('unknown_managed_workflow', CONFIG);
|
|
88
93
|
}
|
|
@@ -113,8 +118,12 @@ export function planWorkflows(root, templateDir, files, version) {
|
|
|
113
118
|
for (const [file, entry] of Object.entries(workflows)) {
|
|
114
119
|
for (const change of entry.overrides) assertReferences(change.value, file, change.path);
|
|
115
120
|
}
|
|
116
|
-
|
|
117
|
-
|
|
121
|
+
if (backendOnly) state.lock.backend_package = version;
|
|
122
|
+
else {
|
|
123
|
+
writes.push(...planValidation(root, [...selected, WARM, RECOVERY]));
|
|
124
|
+
state.lock.package = version;
|
|
125
|
+
delete state.lock.backend_package;
|
|
126
|
+
}
|
|
118
127
|
const config = state.text && equal(readYaml(state.text, CONFIG).value, state.config)
|
|
119
128
|
? state.text
|
|
120
129
|
: '# App-owned settings. Edit this file, then run gowalk-cicd to render workflows.\n' + encode(state.config);
|
package/src/install.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import { dirname } from 'node:path';
|
|
|
23
23
|
import { planWorkflows, writeWorkflows } from './config/workflows.mjs';
|
|
24
24
|
import { recoverWorkflows } from './config/transaction.mjs';
|
|
25
25
|
import { backendLayout } from './backend-layout.mjs';
|
|
26
|
+
import { MigrationRepair } from './config/yaml.mjs';
|
|
26
27
|
|
|
27
28
|
const __filename = fileURLToPath(import.meta.url);
|
|
28
29
|
const __dirname = dirname(__filename);
|
|
@@ -201,32 +202,38 @@ function printSummary() {
|
|
|
201
202
|
console.log(' npx --yes gowalk-cicd');
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
export async function runInstall({ dryRun = false } = {}) {
|
|
205
|
+
export async function runInstall({ dryRun = false, backendOnly = false } = {}) {
|
|
205
206
|
const repoRoot = findRepoRoot(process.cwd());
|
|
206
207
|
console.log(`Installing gowalk-cicd into ${repoRoot}${dryRun ? ' (dry-run)' : ''}`);
|
|
207
208
|
console.log('');
|
|
208
|
-
if (recoverWorkflows(repoRoot, dryRun) && dryRun) return;
|
|
209
|
+
if (recoverWorkflows(repoRoot, dryRun, { backendOnly }) && dryRun) return;
|
|
209
210
|
|
|
210
211
|
const layout = backendLayout(repoRoot);
|
|
212
|
+
if (backendOnly && !layout && !existsSync(join(repoRoot, BACKEND_ACTION_DEST_REL))
|
|
213
|
+
&& !existsSync(join(repoRoot, '.github/workflows/deploy-backend.yml'))) {
|
|
214
|
+
throw new MigrationRepair('backend_layout_missing', '.github/workflows/deploy-backend.yml');
|
|
215
|
+
}
|
|
211
216
|
// deploy-web.yml is no longer added; an existing one is upgraded, or removed
|
|
212
217
|
// when it is still the unmodified generated file (planWorkflows).
|
|
213
|
-
const files = ['deploy.yml', 'mobile-candidate.yml'];
|
|
214
|
-
if (layout || existsSync(join(repoRoot, BACKEND_ACTION_DEST_REL))) files.push('deploy-backend.yml');
|
|
218
|
+
const files = backendOnly ? ['deploy-backend.yml'] : ['deploy.yml', 'mobile-candidate.yml'];
|
|
219
|
+
if (!backendOnly && (layout || existsSync(join(repoRoot, BACKEND_ACTION_DEST_REL)))) files.push('deploy-backend.yml');
|
|
215
220
|
for (const file of ['deploy-web.yml', 'deploy-backend.yml']) {
|
|
216
|
-
if (existsSync(join(repoRoot, '.github/workflows', file)) && !files.includes(file)) files.push(file);
|
|
221
|
+
if (!backendOnly && existsSync(join(repoRoot, '.github/workflows', file)) && !files.includes(file)) files.push(file);
|
|
217
222
|
}
|
|
218
223
|
const version = String(JSON.parse(readFileSync(PACKAGE_JSON, 'utf8')).version);
|
|
219
|
-
const writes = planWorkflows(repoRoot, join(PACKAGE_DIR, 'templates'), files, version);
|
|
224
|
+
const writes = planWorkflows(repoRoot, join(PACKAGE_DIR, 'templates'), files, version, { backendOnly });
|
|
220
225
|
const workflows = workflowText(repoRoot, writes);
|
|
221
226
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
227
|
+
if (!backendOnly) {
|
|
228
|
+
copyAction(ACTION_SRC, ACTION_DEST_REL, repoRoot, dryRun, workflows);
|
|
229
|
+
copyAction(ANDROID_ACTION_SRC, ANDROID_ACTION_DEST_REL, repoRoot, dryRun, workflows);
|
|
230
|
+
writeVersionMarkers(repoRoot, dryRun);
|
|
231
|
+
}
|
|
225
232
|
if (files.includes('deploy-backend.yml')) {
|
|
226
233
|
copyAction(BACKEND_ACTION_SRC, BACKEND_ACTION_DEST_REL, repoRoot, dryRun, workflows);
|
|
227
234
|
if (!dryRun) writeFileSync(join(repoRoot, BACKEND_VERSION_MARKER_REL), version);
|
|
228
235
|
}
|
|
229
|
-
writeWorkflows(repoRoot, writes, dryRun);
|
|
236
|
+
writeWorkflows(repoRoot, writes, dryRun, { backendOnly });
|
|
230
237
|
|
|
231
238
|
const gi = checkGitignore(repoRoot);
|
|
232
239
|
printGitignoreWarning(gi);
|
|
@@ -237,5 +244,7 @@ export async function runInstall({ dryRun = false } = {}) {
|
|
|
237
244
|
return;
|
|
238
245
|
}
|
|
239
246
|
|
|
240
|
-
|
|
247
|
+
if (backendOnly) {
|
|
248
|
+
console.log('\nDone. Backend tooling updated; mobile files and release intents are unchanged.');
|
|
249
|
+
} else printSummary();
|
|
241
250
|
}
|