diff --git a/.github/workflows/ql3-ci.yml b/.github/workflows/ql3-ci.yml index 90e3ab95..edbe58aa 100644 --- a/.github/workflows/ql3-ci.yml +++ b/.github/workflows/ql3-ci.yml @@ -715,6 +715,7 @@ jobs: HEADLESS_IMAGE: qinglong3-local-application:ci-${{ matrix.image_arch }} CONSOLE_IMAGE: qinglong3-local-console:ci-${{ matrix.image_arch }} OPERATOR_IMAGE: qinglong3-local-operator:ci-${{ matrix.image_arch }} + QL3_LOCAL_DEPLOYMENT_DIAGNOSTICS: '1' TRIAL_VARIANT: ${{ inputs.local_alpha_variant }} run: | set -euo pipefail diff --git a/packages/ql3-local-owner-cli/src/deployment/localDeploymentCli.ts b/packages/ql3-local-owner-cli/src/deployment/localDeploymentCli.ts index 21942bac..0d0a5291 100644 --- a/packages/ql3-local-owner-cli/src/deployment/localDeploymentCli.ts +++ b/packages/ql3-local-owner-cli/src/deployment/localDeploymentCli.ts @@ -10,6 +10,7 @@ import { consumeLocalServiceManagerCutoverOutcomeCommandFile, consumeLocalServiceManagerLegacyRollbackCommandFile, inspectLocalDeploymentStatusCommandFile, + LocalDeploymentConfigurationError, preflightLocalDeploymentComposeCommandFile, prepareLocalServiceManagerIntentCommandFile, prepareLocalServiceManagerLegacyRollbackCommandFile, @@ -282,8 +283,15 @@ async function main(argv: readonly string[]): Promise { } catch (error) { const candidate = error as { readonly code?: unknown; + readonly message?: unknown; readonly name?: unknown; }; + const diagnosticMessage = + process.env.QL3_LOCAL_DEPLOYMENT_DIAGNOSTICS === '1' && + error instanceof LocalDeploymentConfigurationError && + typeof candidate.message === 'string' + ? { message: candidate.message } + : {}; process.stderr.write( `${JSON.stringify({ code: @@ -291,6 +299,7 @@ async function main(argv: readonly string[]): Promise { ? candidate.code : 'QL3_LOCAL_DEPLOYMENT_FAILED', name: typeof candidate.name === 'string' ? candidate.name : 'Error', + ...diagnosticMessage, })}\n`, ); process.exitCode = 1; diff --git a/packages/ql3-local-owner-cli/test/localDeployment.test.cjs b/packages/ql3-local-owner-cli/test/localDeployment.test.cjs index 3ed5ac36..b944564a 100644 --- a/packages/ql3-local-owner-cli/test/localDeployment.test.cjs +++ b/packages/ql3-local-owner-cli/test/localDeployment.test.cjs @@ -1000,6 +1000,42 @@ test('status CLI is private-command-only and emits no deployment authority', asy assert.equal(rejected.stderr.includes(state.deploymentRoot), false); }); +test('deployment CLI exposes configuration detail only under explicit diagnostics', (t) => { + const state = fixture(t, 'compose'); + const commandPath = path.join(state.managementRoot, 'invalid-command.json'); + fs.writeFileSync(commandPath, '{}\n', { mode: 0o600 }); + const cli = path.resolve( + __dirname, + '../dist/deployment/localDeploymentCli.js', + ); + const normal = spawnSync( + process.execPath, + [cli, 'prepare', '--command-file', commandPath], + { encoding: 'utf8' }, + ); + assert.equal(normal.status, 1); + assert.deepEqual(JSON.parse(normal.stderr), { + code: 'QL3_LOCAL_DEPLOYMENT_CONFIGURATION_INVALID', + name: 'LocalDeploymentConfigurationError', + }); + + const diagnostic = spawnSync( + process.execPath, + [cli, 'prepare', '--command-file', commandPath], + { + encoding: 'utf8', + env: { ...process.env, QL3_LOCAL_DEPLOYMENT_DIAGNOSTICS: '1' }, + }, + ); + assert.equal(diagnostic.status, 1); + assert.deepEqual(JSON.parse(diagnostic.stderr), { + code: 'QL3_LOCAL_DEPLOYMENT_CONFIGURATION_INVALID', + name: 'LocalDeploymentConfigurationError', + message: + 'Local deployment configuration is invalid: command shape is invalid', + }); +}); + test('durable status keeps live supervisor and database authority out of its module', () => { const source = fs.readFileSync( path.resolve(__dirname, '../src/deployment/localDeploymentStatus.ts'), diff --git a/scripts/templates/ql3-local-alpha-reconciliation-rehearsal.sh b/scripts/templates/ql3-local-alpha-reconciliation-rehearsal.sh index ec266bf3..3d0567d0 100644 --- a/scripts/templates/ql3-local-alpha-reconciliation-rehearsal.sh +++ b/scripts/templates/ql3-local-alpha-reconciliation-rehearsal.sh @@ -210,6 +210,8 @@ run_deploy() { --mount "type=bind,src=$decision_parent,dst=$decision_parent,readonly" [ -z "$secondary_decision_parent" ] || set -- "$@" \ --mount "type=bind,src=$secondary_decision_parent,dst=$secondary_decision_parent,readonly" + [ "${QL3_LOCAL_DEPLOYMENT_DIAGNOSTICS:-0}" != 1 ] || set -- "$@" \ + --env QL3_LOCAL_DEPLOYMENT_DIAGNOSTICS=1 set -- "$@" "$OPERATOR_IMAGE" deploy "$subcommand" \ --command-file "$command_root/$command_file" result_stage="$result_root/.$result_file.$$"