@neurcode/action 0.2.1 → 0.2.2
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 +9 -3
- package/action.yml +36 -4
- package/dist/index.js +618 -9
- package/dist/index.js.map +1 -1
- package/dist/runtime-compat.js +125 -0
- package/dist/runtime-compat.js.map +1 -0
- package/dist/verify-mode.js +15 -0
- package/dist/verify-mode.js.map +1 -1
- package/package.json +9 -8
- package/src/index.ts +400 -9
- package/src/runtime-compat.ts +201 -0
- package/src/verify-mode.ts +32 -0
- package/tests/reliability-contract.test.ts +136 -0
- package/LICENSE +0 -201
package/src/index.ts
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import * as core from '@actions/core';
|
|
2
2
|
import * as exec from '@actions/exec';
|
|
3
3
|
import * as github from '@actions/github';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
parseCliVerifyJsonPayload,
|
|
6
|
+
RUNTIME_COMPATIBILITY_CONTRACT_VERSION,
|
|
7
|
+
} from '@neurcode-ai/contracts';
|
|
5
8
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
9
|
+
import * as http from 'http';
|
|
10
|
+
import * as https from 'https';
|
|
6
11
|
import { join, resolve } from 'path';
|
|
7
12
|
import {
|
|
8
13
|
buildVerifyArgs,
|
|
9
14
|
getVerifyFallbackDecision,
|
|
10
15
|
isMissingPlanVerificationFailure,
|
|
16
|
+
resolveEnterpriseEnforcement,
|
|
11
17
|
} from './verify-mode';
|
|
18
|
+
import {
|
|
19
|
+
parseApiHealthCompatibilityPayload,
|
|
20
|
+
parseCliCompatibilityPayload,
|
|
21
|
+
validateActionHandshake,
|
|
22
|
+
} from './runtime-compat';
|
|
12
23
|
|
|
13
24
|
interface Violation {
|
|
14
25
|
file: string;
|
|
@@ -112,6 +123,19 @@ interface CliInvocation {
|
|
|
112
123
|
description: string;
|
|
113
124
|
}
|
|
114
125
|
|
|
126
|
+
interface VerifyCapabilities {
|
|
127
|
+
supportsCompiledPolicy: boolean;
|
|
128
|
+
supportsChangeContract: boolean;
|
|
129
|
+
supportsEnforceChangeContract: boolean;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
interface HttpJsonResponse {
|
|
133
|
+
statusCode: number;
|
|
134
|
+
body: string;
|
|
135
|
+
payload: unknown | null;
|
|
136
|
+
url: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
115
139
|
type CliInstallSource = 'npm' | 'workspace';
|
|
116
140
|
|
|
117
141
|
const ANSI_PATTERN = /\u001b\[[0-9;]*m/g;
|
|
@@ -163,6 +187,16 @@ function parseBoolean(input: string | undefined, fallback: boolean): boolean {
|
|
|
163
187
|
return fallback;
|
|
164
188
|
}
|
|
165
189
|
|
|
190
|
+
function parseBooleanOrUndefined(input: string | undefined): boolean | undefined {
|
|
191
|
+
if (input === undefined || input === null || input.trim() === '') {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
const normalized = input.trim().toLowerCase();
|
|
195
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
|
|
196
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
166
200
|
function parsePositiveInt(input: string | undefined, fallback: number, min: number, max: number): number {
|
|
167
201
|
const parsed = Number(input);
|
|
168
202
|
if (!Number.isFinite(parsed)) return fallback;
|
|
@@ -303,6 +337,70 @@ function readJsonFile(path: string): Record<string, unknown> | null {
|
|
|
303
337
|
}
|
|
304
338
|
}
|
|
305
339
|
|
|
340
|
+
function assertStrictDeterministicArtifacts(input: {
|
|
341
|
+
cwd: string;
|
|
342
|
+
strictMode: boolean;
|
|
343
|
+
compiledPolicyPath?: string;
|
|
344
|
+
changeContractPath?: string;
|
|
345
|
+
supportsCompiledPolicy: boolean;
|
|
346
|
+
supportsChangeContract: boolean;
|
|
347
|
+
}): void {
|
|
348
|
+
if (!input.strictMode) return;
|
|
349
|
+
|
|
350
|
+
const errors: string[] = [];
|
|
351
|
+
|
|
352
|
+
if (!input.supportsCompiledPolicy) {
|
|
353
|
+
errors.push('Current CLI does not support --compiled-policy, but strict mode requires a compiled policy artifact.');
|
|
354
|
+
}
|
|
355
|
+
if (!input.supportsChangeContract) {
|
|
356
|
+
errors.push('Current CLI does not support --change-contract, but strict mode requires a change contract artifact.');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const compiledPath = input.compiledPolicyPath?.trim();
|
|
360
|
+
if (!compiledPath) {
|
|
361
|
+
errors.push('Missing compiled policy artifact path in strict mode (compiled_policy_path).');
|
|
362
|
+
} else {
|
|
363
|
+
const absoluteCompiledPath = resolve(input.cwd, compiledPath);
|
|
364
|
+
const compiled = readJsonFile(absoluteCompiledPath);
|
|
365
|
+
if (!compiled) {
|
|
366
|
+
errors.push(`Compiled policy artifact not found or invalid JSON: ${compiledPath}`);
|
|
367
|
+
} else {
|
|
368
|
+
const hasFingerprint = typeof compiled.fingerprint === 'string' && compiled.fingerprint.trim().length > 0;
|
|
369
|
+
const hasRules = Array.isArray(compiled.rules) || Array.isArray(compiled.statements);
|
|
370
|
+
if (!hasFingerprint && !hasRules) {
|
|
371
|
+
errors.push(
|
|
372
|
+
`Compiled policy artifact appears invalid (missing fingerprint/rules): ${compiledPath}`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const contractPath = input.changeContractPath?.trim();
|
|
379
|
+
if (!contractPath) {
|
|
380
|
+
errors.push('Missing change contract artifact path in strict mode (change_contract_path).');
|
|
381
|
+
} else {
|
|
382
|
+
const absoluteContractPath = resolve(input.cwd, contractPath);
|
|
383
|
+
const contract = readJsonFile(absoluteContractPath);
|
|
384
|
+
if (!contract) {
|
|
385
|
+
errors.push(`Change contract artifact not found or invalid JSON: ${contractPath}`);
|
|
386
|
+
} else {
|
|
387
|
+
const hasContractId = typeof contract.contractId === 'string' && contract.contractId.trim().length > 0;
|
|
388
|
+
const hasPlanId = typeof contract.planId === 'string' && contract.planId.trim().length > 0;
|
|
389
|
+
if (!hasContractId || !hasPlanId) {
|
|
390
|
+
errors.push(
|
|
391
|
+
`Change contract artifact appears invalid (missing contractId/planId): ${contractPath}`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (errors.length > 0) {
|
|
398
|
+
throw new Error(
|
|
399
|
+
`Strict enterprise mode requires deterministic compiled-policy + change-contract artifacts:\n- ${errors.join('\n- ')}`
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
306
404
|
function detectProjectOrgId(cwd: string): string | undefined {
|
|
307
405
|
const statePath = join(cwd, '.neurcode', 'config.json');
|
|
308
406
|
const state = readJsonFile(statePath);
|
|
@@ -381,6 +479,187 @@ function extractLastJsonObject(output: string): unknown | null {
|
|
|
381
479
|
return null;
|
|
382
480
|
}
|
|
383
481
|
|
|
482
|
+
function resolveActionPackageVersion(): string {
|
|
483
|
+
const fromEnv = process.env.NEURCODE_ACTION_VERSION || process.env.npm_package_version;
|
|
484
|
+
if (fromEnv && fromEnv.trim()) {
|
|
485
|
+
return fromEnv.trim();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const candidates = [
|
|
489
|
+
resolve(__dirname, '../package.json'),
|
|
490
|
+
resolve(process.cwd(), 'package.json'),
|
|
491
|
+
];
|
|
492
|
+
for (const candidate of candidates) {
|
|
493
|
+
try {
|
|
494
|
+
if (!existsSync(candidate)) continue;
|
|
495
|
+
const parsed = JSON.parse(readFileSync(candidate, 'utf-8'));
|
|
496
|
+
const version = parsed?.version;
|
|
497
|
+
if (typeof version === 'string' && version.trim()) {
|
|
498
|
+
return version.trim();
|
|
499
|
+
}
|
|
500
|
+
} catch {
|
|
501
|
+
// Ignore and continue to next candidate.
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return '0.0.0';
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function normalizeApiBaseUrl(value: string): string {
|
|
509
|
+
return value.trim().replace(/\/+$/, '');
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function requestJson(url: string, timeoutMs: number): Promise<HttpJsonResponse> {
|
|
513
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
514
|
+
let target: URL;
|
|
515
|
+
try {
|
|
516
|
+
target = new URL(url);
|
|
517
|
+
} catch (error) {
|
|
518
|
+
rejectPromise(new Error(`Invalid API URL: ${url} (${error instanceof Error ? error.message : String(error)})`));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const transport = target.protocol === 'http:' ? http : https;
|
|
522
|
+
const request = transport.request(
|
|
523
|
+
{
|
|
524
|
+
protocol: target.protocol,
|
|
525
|
+
hostname: target.hostname,
|
|
526
|
+
port: target.port || undefined,
|
|
527
|
+
path: `${target.pathname}${target.search}`,
|
|
528
|
+
method: 'GET',
|
|
529
|
+
headers: {
|
|
530
|
+
Accept: 'application/json',
|
|
531
|
+
'User-Agent': 'neurcode-action/runtime-compat',
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
(response) => {
|
|
535
|
+
let body = '';
|
|
536
|
+
response.setEncoding('utf8');
|
|
537
|
+
response.on('data', (chunk) => {
|
|
538
|
+
body += chunk;
|
|
539
|
+
});
|
|
540
|
+
response.on('end', () => {
|
|
541
|
+
let payload: unknown | null = null;
|
|
542
|
+
try {
|
|
543
|
+
payload = body ? JSON.parse(body) : null;
|
|
544
|
+
} catch {
|
|
545
|
+
payload = null;
|
|
546
|
+
}
|
|
547
|
+
resolvePromise({
|
|
548
|
+
statusCode: response.statusCode || 0,
|
|
549
|
+
body,
|
|
550
|
+
payload,
|
|
551
|
+
url,
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
request.on('error', (error) => {
|
|
558
|
+
rejectPromise(new Error(`Request failed for ${url}: ${error.message}`));
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
request.setTimeout(timeoutMs, () => {
|
|
562
|
+
request.destroy(new Error(`Request timed out after ${timeoutMs}ms`));
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
request.end();
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function resolveApiHealthPayload(apiBaseUrl: string, timeoutMs: number): Promise<unknown> {
|
|
570
|
+
const baseUrl = normalizeApiBaseUrl(apiBaseUrl);
|
|
571
|
+
const candidates = [`${baseUrl}/api/v1/health`, `${baseUrl}/health`];
|
|
572
|
+
const failures: string[] = [];
|
|
573
|
+
|
|
574
|
+
for (const url of candidates) {
|
|
575
|
+
try {
|
|
576
|
+
const response = await requestJson(url, timeoutMs);
|
|
577
|
+
const payload = response.payload;
|
|
578
|
+
const statusValue =
|
|
579
|
+
payload && typeof payload === 'object' && !Array.isArray(payload)
|
|
580
|
+
? (payload as Record<string, unknown>).status
|
|
581
|
+
: null;
|
|
582
|
+
if (response.statusCode >= 200 && response.statusCode < 300 && statusValue === 'ok') {
|
|
583
|
+
return payload;
|
|
584
|
+
}
|
|
585
|
+
failures.push(`${url} returned ${response.statusCode}`);
|
|
586
|
+
} catch (error) {
|
|
587
|
+
failures.push(error instanceof Error ? error.message : String(error));
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
throw new Error(
|
|
592
|
+
`API compatibility probe failed (${failures.join(' | ')}).`
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async function runCompatibilityHandshake(input: {
|
|
597
|
+
cli: CliInvocation;
|
|
598
|
+
cwd: string;
|
|
599
|
+
timeoutMinutes: number;
|
|
600
|
+
apiUrl?: string;
|
|
601
|
+
requireApiCompatibility: boolean;
|
|
602
|
+
}): Promise<{
|
|
603
|
+
actionVersion: string;
|
|
604
|
+
cliVersion: string;
|
|
605
|
+
apiVersion?: string;
|
|
606
|
+
}> {
|
|
607
|
+
const actionVersion = resolveActionPackageVersion();
|
|
608
|
+
|
|
609
|
+
const compatCommand = withCliCommandTimeout(input.cli, ['compat', '--json'], input.timeoutMinutes);
|
|
610
|
+
const compatResult = await runCommand(compatCommand.cmd, compatCommand.args, { cwd: input.cwd });
|
|
611
|
+
if (compatResult.exitCode !== 0) {
|
|
612
|
+
throw new Error(
|
|
613
|
+
`Compatibility handshake failed: unable to run "neurcode compat --json". ` +
|
|
614
|
+
`Output: ${stripAnsi(compatResult.output).trim() || 'no output'}`
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const cliCompatPayload = extractLastJsonObject(compatResult.output);
|
|
619
|
+
if (!cliCompatPayload) {
|
|
620
|
+
throw new Error('Compatibility handshake failed: CLI compat output did not contain JSON.');
|
|
621
|
+
}
|
|
622
|
+
const cliCompatibility = parseCliCompatibilityPayload(cliCompatPayload);
|
|
623
|
+
|
|
624
|
+
let apiCompatibility = null;
|
|
625
|
+
let apiVersion: string | undefined;
|
|
626
|
+
if (input.apiUrl) {
|
|
627
|
+
const apiHealthPayload = await resolveApiHealthPayload(input.apiUrl, 10000);
|
|
628
|
+
apiCompatibility = parseApiHealthCompatibilityPayload(apiHealthPayload);
|
|
629
|
+
if (!apiCompatibility && input.requireApiCompatibility) {
|
|
630
|
+
throw new Error(
|
|
631
|
+
'Compatibility handshake failed: API health payload does not include compatibility metadata.'
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
if (apiCompatibility?.componentVersion) {
|
|
635
|
+
apiVersion = apiCompatibility.componentVersion;
|
|
636
|
+
} else if (
|
|
637
|
+
apiHealthPayload
|
|
638
|
+
&& typeof apiHealthPayload === 'object'
|
|
639
|
+
&& !Array.isArray(apiHealthPayload)
|
|
640
|
+
&& typeof (apiHealthPayload as Record<string, unknown>).version === 'string'
|
|
641
|
+
) {
|
|
642
|
+
apiVersion = (apiHealthPayload as Record<string, unknown>).version as string;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const errors = validateActionHandshake({
|
|
647
|
+
actionVersion,
|
|
648
|
+
cliCompatibility,
|
|
649
|
+
apiCompatibility,
|
|
650
|
+
requireApiCompatibility: input.requireApiCompatibility && Boolean(input.apiUrl),
|
|
651
|
+
});
|
|
652
|
+
if (errors.length > 0) {
|
|
653
|
+
throw new Error(`Compatibility handshake failed:\n- ${errors.join('\n- ')}`);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return {
|
|
657
|
+
actionVersion,
|
|
658
|
+
cliVersion: cliCompatibility.componentVersion,
|
|
659
|
+
apiVersion,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
384
663
|
function parseVerifyResult(output: string): NeurcodeVerifyResult | null {
|
|
385
664
|
const parsed = extractLastJsonObject(output);
|
|
386
665
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
|
|
@@ -893,6 +1172,26 @@ function withCliCommandTimeout(
|
|
|
893
1172
|
return withCommandTimeout(cli.cmd, [...cli.argsPrefix, ...args], timeoutMinutes);
|
|
894
1173
|
}
|
|
895
1174
|
|
|
1175
|
+
async function resolveVerifyCapabilities(cli: CliInvocation, cwd: string): Promise<VerifyCapabilities> {
|
|
1176
|
+
const helpCommand = withCliCommandTimeout(cli, ['verify', '--help'], 2);
|
|
1177
|
+
const helpResult = await runCommand(helpCommand.cmd, helpCommand.args, { cwd });
|
|
1178
|
+
if (helpResult.exitCode !== 0) {
|
|
1179
|
+
core.warning('Unable to detect verify flag capabilities; defaulting to full verify flag set.');
|
|
1180
|
+
return {
|
|
1181
|
+
supportsCompiledPolicy: true,
|
|
1182
|
+
supportsChangeContract: true,
|
|
1183
|
+
supportsEnforceChangeContract: true,
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const helpText = stripAnsi(helpResult.output).toLowerCase();
|
|
1188
|
+
return {
|
|
1189
|
+
supportsCompiledPolicy: helpText.includes('--compiled-policy'),
|
|
1190
|
+
supportsChangeContract: helpText.includes('--change-contract'),
|
|
1191
|
+
supportsEnforceChangeContract: helpText.includes('--enforce-change-contract'),
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
|
|
896
1195
|
async function resolveCliInvocation(cwd: string): Promise<CliInvocation | null> {
|
|
897
1196
|
const direct = await runCommand('neurcode', ['--version'], { cwd });
|
|
898
1197
|
if (direct.exitCode === 0) {
|
|
@@ -1166,10 +1465,26 @@ async function run(): Promise<void> {
|
|
|
1166
1465
|
const cliInstallSource = parseCliInstallSource(core.getInput('neurcode_cli_source'));
|
|
1167
1466
|
const cliWorkspacePath = core.getInput('neurcode_cli_workspace_path') || 'packages/cli';
|
|
1168
1467
|
const verifyTimeoutMinutes = parsePositiveInt(core.getInput('verify_timeout_minutes'), 8, 1, 120);
|
|
1468
|
+
const enforceCompatibilityHandshake = parseBoolean(
|
|
1469
|
+
core.getInput('enforce_compatibility_handshake'),
|
|
1470
|
+
true
|
|
1471
|
+
);
|
|
1472
|
+
const requireApiCompatibilityHandshake = parseBoolean(
|
|
1473
|
+
core.getInput('require_api_compatibility_handshake'),
|
|
1474
|
+
true
|
|
1475
|
+
);
|
|
1476
|
+
const compatibilityProbeTimeoutMinutes = parsePositiveInt(
|
|
1477
|
+
core.getInput('compatibility_probe_timeout_minutes'),
|
|
1478
|
+
2,
|
|
1479
|
+
1,
|
|
1480
|
+
15
|
|
1481
|
+
);
|
|
1482
|
+
const configuredApiUrl = (process.env.NEURCODE_API_URL || '').trim();
|
|
1169
1483
|
const verifyPolicyOnly = parseBoolean(core.getInput('verify_policy_only'), false);
|
|
1484
|
+
const enterpriseMode = parseBoolean(core.getInput('enterprise_mode'), true);
|
|
1170
1485
|
const compiledPolicyPath = (core.getInput('compiled_policy_path') || 'neurcode.policy.compiled.json').trim();
|
|
1171
1486
|
const changeContractPath = (core.getInput('change_contract_path') || '.neurcode/change-contract.json').trim();
|
|
1172
|
-
const
|
|
1487
|
+
const enforceChangeContractOverride = parseBooleanOrUndefined(core.getInput('enforce_change_contract'));
|
|
1173
1488
|
const changedFilesOnly = parseBoolean(core.getInput('changed_files_only'), false);
|
|
1174
1489
|
|
|
1175
1490
|
const autoRemediate = parseBoolean(core.getInput('auto_remediate'), false);
|
|
@@ -1189,7 +1504,15 @@ async function run(): Promise<void> {
|
|
|
1189
1504
|
);
|
|
1190
1505
|
const remediationCommit = parseBoolean(core.getInput('remediation_commit'), false);
|
|
1191
1506
|
const remediationPush = parseBoolean(core.getInput('remediation_push'), false);
|
|
1192
|
-
const
|
|
1507
|
+
const enforceStrictVerificationOverride = parseBooleanOrUndefined(core.getInput('enforce_strict_verification'));
|
|
1508
|
+
const enterpriseEnforcement = resolveEnterpriseEnforcement({
|
|
1509
|
+
enterpriseMode,
|
|
1510
|
+
verifyPolicyOnly,
|
|
1511
|
+
enforceChangeContract: enforceChangeContractOverride,
|
|
1512
|
+
enforceStrictVerification: enforceStrictVerificationOverride,
|
|
1513
|
+
});
|
|
1514
|
+
let enforceChangeContract = enterpriseEnforcement.enforceChangeContract;
|
|
1515
|
+
const enforceStrictVerification = enterpriseEnforcement.enforceStrictVerification;
|
|
1193
1516
|
const verifyAfterRemediation = parseBoolean(core.getInput('verify_after_remediation'), true);
|
|
1194
1517
|
const verifyAfterRemediationTimeoutMinutes = parsePositiveInt(
|
|
1195
1518
|
core.getInput('verify_after_remediation_timeout_minutes'),
|
|
@@ -1235,6 +1558,55 @@ async function run(): Promise<void> {
|
|
|
1235
1558
|
workspacePath: cliWorkspacePath,
|
|
1236
1559
|
});
|
|
1237
1560
|
|
|
1561
|
+
if (enforceCompatibilityHandshake) {
|
|
1562
|
+
const handshake = await runCompatibilityHandshake({
|
|
1563
|
+
cli: cliInvocation,
|
|
1564
|
+
cwd,
|
|
1565
|
+
timeoutMinutes: compatibilityProbeTimeoutMinutes,
|
|
1566
|
+
apiUrl: configuredApiUrl || undefined,
|
|
1567
|
+
requireApiCompatibility: requireApiCompatibilityHandshake,
|
|
1568
|
+
});
|
|
1569
|
+
core.info(
|
|
1570
|
+
`Compatibility handshake passed (action=${handshake.actionVersion}, cli=${handshake.cliVersion}${handshake.apiVersion ? `, api=${handshake.apiVersion}` : ''}).`
|
|
1571
|
+
);
|
|
1572
|
+
core.setOutput('compatibility_handshake', 'passed');
|
|
1573
|
+
core.setOutput('compatibility_contract_version', RUNTIME_COMPATIBILITY_CONTRACT_VERSION);
|
|
1574
|
+
core.setOutput('compatibility_action_version', handshake.actionVersion);
|
|
1575
|
+
core.setOutput('compatibility_cli_version', handshake.cliVersion);
|
|
1576
|
+
if (handshake.apiVersion) {
|
|
1577
|
+
core.setOutput('compatibility_api_version', handshake.apiVersion);
|
|
1578
|
+
}
|
|
1579
|
+
} else {
|
|
1580
|
+
core.warning('Compatibility handshake is disabled via enforce_compatibility_handshake=false.');
|
|
1581
|
+
core.setOutput('compatibility_handshake', 'skipped');
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
const verifyCapabilities = await resolveVerifyCapabilities(cliInvocation, cwd);
|
|
1585
|
+
const effectiveCompiledPolicyPath = verifyCapabilities.supportsCompiledPolicy
|
|
1586
|
+
? (compiledPolicyPath || undefined)
|
|
1587
|
+
: undefined;
|
|
1588
|
+
const effectiveChangeContractPath = verifyCapabilities.supportsChangeContract
|
|
1589
|
+
? (changeContractPath || undefined)
|
|
1590
|
+
: undefined;
|
|
1591
|
+
if (!verifyCapabilities.supportsCompiledPolicy && compiledPolicyPath) {
|
|
1592
|
+
core.warning('CLI does not support --compiled-policy; compiled policy artifact input will be ignored.');
|
|
1593
|
+
}
|
|
1594
|
+
if (!verifyCapabilities.supportsChangeContract && changeContractPath) {
|
|
1595
|
+
core.warning('CLI does not support --change-contract; contract path input will be ignored.');
|
|
1596
|
+
}
|
|
1597
|
+
if (enforceChangeContract && !verifyCapabilities.supportsEnforceChangeContract) {
|
|
1598
|
+
core.warning('CLI does not support --enforce-change-contract; contract hard-fail mode will be disabled.');
|
|
1599
|
+
enforceChangeContract = false;
|
|
1600
|
+
}
|
|
1601
|
+
assertStrictDeterministicArtifacts({
|
|
1602
|
+
cwd,
|
|
1603
|
+
strictMode: enforceStrictVerification,
|
|
1604
|
+
compiledPolicyPath: effectiveCompiledPolicyPath,
|
|
1605
|
+
changeContractPath: effectiveChangeContractPath,
|
|
1606
|
+
supportsCompiledPolicy: verifyCapabilities.supportsCompiledPolicy,
|
|
1607
|
+
supportsChangeContract: verifyCapabilities.supportsChangeContract,
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1238
1610
|
const pr = github.context.payload.pull_request;
|
|
1239
1611
|
const defaultBaseRef = pr ? `origin/${pr.base.ref}` : 'HEAD~1';
|
|
1240
1612
|
const baseRef = baseRefInput.trim() || defaultBaseRef;
|
|
@@ -1242,6 +1614,13 @@ async function run(): Promise<void> {
|
|
|
1242
1614
|
? `Running verify in PR context against ${baseRef}`
|
|
1243
1615
|
: `Running verify in push context against ${baseRef}`);
|
|
1244
1616
|
core.info(`Verification mode: ${verifyPolicyOnly ? 'policy-only' : 'plan-aware'}`);
|
|
1617
|
+
core.info(`Enterprise mode: ${enterpriseMode ? 'enabled' : 'disabled'}`);
|
|
1618
|
+
core.info(
|
|
1619
|
+
`Verify capabilities => compiled_policy=${verifyCapabilities.supportsCompiledPolicy}, change_contract=${verifyCapabilities.supportsChangeContract}, enforce_change_contract=${verifyCapabilities.supportsEnforceChangeContract}`
|
|
1620
|
+
);
|
|
1621
|
+
core.info(
|
|
1622
|
+
`Enterprise enforcement => enforce_change_contract=${enforceChangeContract}, enforce_strict_verification=${enforceStrictVerification}`
|
|
1623
|
+
);
|
|
1245
1624
|
|
|
1246
1625
|
let changedFiles = new Set<string>();
|
|
1247
1626
|
if (changedFilesOnly) {
|
|
@@ -1255,6 +1634,7 @@ async function run(): Promise<void> {
|
|
|
1255
1634
|
}
|
|
1256
1635
|
|
|
1257
1636
|
let effectiveVerifyPolicyOnly = verifyPolicyOnly;
|
|
1637
|
+
let effectiveEnforceChangeContract = enforceChangeContract;
|
|
1258
1638
|
const hasExplicitPlanId = Boolean(planId && planId.trim());
|
|
1259
1639
|
let policyOnlyFallbackUsed = false;
|
|
1260
1640
|
let fallbackFailureHint: string | null = null;
|
|
@@ -1264,9 +1644,10 @@ async function run(): Promise<void> {
|
|
|
1264
1644
|
projectId: projectId || undefined,
|
|
1265
1645
|
policyOnly: effectiveVerifyPolicyOnly,
|
|
1266
1646
|
record,
|
|
1267
|
-
compiledPolicyPath:
|
|
1268
|
-
changeContractPath:
|
|
1269
|
-
enforceChangeContract,
|
|
1647
|
+
compiledPolicyPath: effectiveCompiledPolicyPath,
|
|
1648
|
+
changeContractPath: effectiveChangeContractPath,
|
|
1649
|
+
enforceChangeContract: effectiveEnforceChangeContract,
|
|
1650
|
+
strictArtifacts: enforceStrictVerification,
|
|
1270
1651
|
});
|
|
1271
1652
|
|
|
1272
1653
|
let verifyCommand = withCliCommandTimeout(cliInvocation, verifyArgs, verifyTimeoutMinutes);
|
|
@@ -1294,15 +1675,22 @@ async function run(): Promise<void> {
|
|
|
1294
1675
|
);
|
|
1295
1676
|
policyOnlyFallbackUsed = true;
|
|
1296
1677
|
effectiveVerifyPolicyOnly = true;
|
|
1678
|
+
effectiveEnforceChangeContract =
|
|
1679
|
+
(typeof enforceChangeContractOverride === 'boolean' ? enforceChangeContractOverride : false)
|
|
1680
|
+
&& verifyCapabilities.supportsEnforceChangeContract;
|
|
1681
|
+
if (typeof enforceChangeContractOverride !== 'boolean' && enforceChangeContract) {
|
|
1682
|
+
core.info('Policy-only fallback detected; auto-disabling change-contract hard-fail for fallback run.');
|
|
1683
|
+
}
|
|
1297
1684
|
verifyArgs = buildVerifyArgs({
|
|
1298
1685
|
baseRef,
|
|
1299
1686
|
projectId: projectId || undefined,
|
|
1300
1687
|
planId: undefined,
|
|
1301
1688
|
policyOnly: true,
|
|
1302
1689
|
record,
|
|
1303
|
-
compiledPolicyPath:
|
|
1304
|
-
changeContractPath:
|
|
1305
|
-
enforceChangeContract,
|
|
1690
|
+
compiledPolicyPath: effectiveCompiledPolicyPath,
|
|
1691
|
+
changeContractPath: effectiveChangeContractPath,
|
|
1692
|
+
enforceChangeContract: effectiveEnforceChangeContract,
|
|
1693
|
+
strictArtifacts: enforceStrictVerification,
|
|
1306
1694
|
});
|
|
1307
1695
|
verifyCommand = withCliCommandTimeout(cliInvocation, verifyArgs, verifyTimeoutMinutes);
|
|
1308
1696
|
verifyRun = await runCommand(verifyCommand.cmd, verifyCommand.args, {
|
|
@@ -1628,6 +2016,9 @@ async function run(): Promise<void> {
|
|
|
1628
2016
|
: 'plan_aware';
|
|
1629
2017
|
core.setOutput('verify_mode', verifyModeOutput);
|
|
1630
2018
|
core.setOutput('policy_only_fallback_used', policyOnlyFallbackUsed ? 'true' : 'false');
|
|
2019
|
+
core.setOutput('enterprise_mode_active', enterpriseMode ? 'true' : 'false');
|
|
2020
|
+
core.setOutput('enterprise_enforced_change_contract', effectiveEnforceChangeContract ? 'true' : 'false');
|
|
2021
|
+
core.setOutput('enterprise_enforced_strict_verification', enforceStrictVerification ? 'true' : 'false');
|
|
1631
2022
|
|
|
1632
2023
|
if (remediation) {
|
|
1633
2024
|
core.setOutput('remediation_status', remediation.status);
|