mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-15 04:47:23 +01:00
feat(pregate): price_command config key for the non-executing call plan a paid gate is priced from
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
b4481089ef
commit
2093513069
3 files changed
+34
-3
No files matched your search
@@ -62,6 +62,7 @@ the byte-pinned `/ship` renders in `test/fixtures/golden/`).
|
||||
| `selection` | `workflow.yml/job` → a command printing the lane's file list, for matrices a planner computes (gstack: `scripts/test-free-shards.ts --list`) |
|
||||
| `warn` | check ids downgraded to warn; `secrets` and required lanes never |
|
||||
| `strays.allow` | intentional root files or new top-level directories |
|
||||
| `price_command` | one command that prints the paid gate's call plan (`GSTACK_EVAL_CALLS v1` lines) without calling a provider; `gstack-eval-plan gate` prices from it ([docs/eval-calls.md](eval-calls.md)) |
|
||||
|
||||
Only the `origin/<base>` copy is honoured, so a downgrade is reviewed before it
|
||||
takes effect. `gstack-pregate validate` / `init` cover the working-tree file.
|
||||
|
||||
+11
-3
@@ -13,6 +13,8 @@
|
||||
* warn check ids downgraded to warn (never `secrets`; `lanes` keeps
|
||||
* its required-lane and requires-remote verdicts)
|
||||
* strays.allow root files or new top-level directories that are intentional
|
||||
* price_command the non-executing call-plan command `gstack-eval-plan gate`
|
||||
* prices a paid gate from (GSTACK_EVAL_CALLS v1 lines)
|
||||
*/
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
@@ -31,13 +33,14 @@ export interface PregateConfig {
|
||||
selection: Record<string, string>;
|
||||
warn: string[];
|
||||
strays: { allow: string[] };
|
||||
price_command: string | null;
|
||||
}
|
||||
export const DEFAULT_CONFIG: PregateConfig = { runner: null, dependencies: {}, selection: {}, warn: [], strays: { allow: [] } };
|
||||
export const DEFAULT_CONFIG: PregateConfig = { runner: null, dependencies: {}, selection: {}, warn: [], strays: { allow: [] }, price_command: null };
|
||||
export interface ConfigError { code: ResultCodeName; path: string; message: string }
|
||||
|
||||
const MAX_LIST = 256;
|
||||
const MAX_COMMAND = 512;
|
||||
const KEYS = ['runner', 'dependencies', 'selection', 'warn', 'strays'];
|
||||
const KEYS = ['runner', 'dependencies', 'selection', 'warn', 'strays', 'price_command'];
|
||||
|
||||
function globError(g: string): string | null {
|
||||
const why = containedRelative(g);
|
||||
@@ -61,6 +64,10 @@ export function validateConfig(raw: unknown, knownChecks: readonly string[] = []
|
||||
if (why) errors.push({ code: 'PREGATE_CONFIG_INVALID', path: 'runner', message: why });
|
||||
else if (!(obj.runner as string).includes('{files}')) errors.push({ code: 'PREGATE_CONFIG_INVALID', path: 'runner', message: 'must contain the {files} placeholder' });
|
||||
}
|
||||
if ('price_command' in obj && obj.price_command !== null) {
|
||||
const why = commandError(obj.price_command);
|
||||
if (why) errors.push({ code: 'PREGATE_CONFIG_INVALID', path: 'price_command', message: why });
|
||||
}
|
||||
if ('dependencies' in obj) {
|
||||
const deps = obj.dependencies;
|
||||
if (!deps || typeof deps !== 'object' || Array.isArray(deps)) errors.push({ code: 'PREGATE_CONFIG_INVALID', path: 'dependencies', message: 'must be an object of glob → test file list' });
|
||||
@@ -112,7 +119,7 @@ export function parseConfig(text: string, knownChecks: readonly string[] = []):
|
||||
const errors = validateConfig(raw, knownChecks);
|
||||
if (errors.length) return { config: DEFAULT_CONFIG, errors };
|
||||
const o = raw as Partial<PregateConfig>;
|
||||
return { config: { $schema: o.$schema, runner: o.runner ?? null, dependencies: o.dependencies ?? {}, selection: o.selection ?? {}, warn: o.warn ?? [], strays: { allow: o.strays?.allow ?? [] } }, errors: [] };
|
||||
return { config: { $schema: o.$schema, runner: o.runner ?? null, dependencies: o.dependencies ?? {}, selection: o.selection ?? {}, warn: o.warn ?? [], strays: { allow: o.strays?.allow ?? [] }, price_command: o.price_command ?? null }, errors: [] };
|
||||
}
|
||||
|
||||
export type ConfigLoad = { config: PregateConfig; errors: ConfigError[]; present: boolean; label: string; kind: 'base' | 'head' | 'none' } | { unavailable: true; base: string; detail: string };
|
||||
@@ -160,5 +167,6 @@ export function explainConfig(): string {
|
||||
'selection: <workflow.yml>/<job> → the runner-owned command that lists that lane’s test files when its matrix is planner-computed',
|
||||
`warn: check ids downgraded to warn; never ${NON_DOWNGRADABLE.join(', ')}; lanes keeps its required-lane and requires-remote verdicts`,
|
||||
'strays.allow: root files or new top-level directories that are intentional',
|
||||
'price_command: one command that prints GSTACK_EVAL_CALLS v1 lines without calling a provider; `gstack-eval-plan gate` prices a paid gate from it (docs/eval-calls.md)',
|
||||
].join('\n') + '\n';
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Field wave CD1/CD2 config keys in `.gstack/pregate.json`: the repo's guards
|
||||
* (commands a lane must pass before it reports done), the serial-test
|
||||
* patterns the lanes check runs in their own process, and the non-executing
|
||||
* call-plan command a paid gate is priced from. Validation stays strict:
|
||||
* every key is typed, globs are contained, commands are one line.
|
||||
*/
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { parseConfig, validateConfig } from '../lib/pregate/config';
|
||||
import { CHECK_IDS } from '../lib/pregate/index';
|
||||
|
||||
const codes = (c: unknown) => validateConfig(c, CHECK_IDS).map(e => `${e.code}:${e.path}`);
|
||||
|
||||
describe('pregate config: price_command', () => {
|
||||
test('a one-line price_command is valid and parsed; a multi-line or empty one is PREGATE_CONFIG_INVALID', () => {
|
||||
expect(codes({ price_command: 'bun run eval:plan-calls' })).toEqual([]);
|
||||
expect(parseConfig(JSON.stringify({ price_command: 'bun run eval:plan-calls' })).config.price_command).toBe('bun run eval:plan-calls');
|
||||
expect(parseConfig('{}').config.price_command).toBeNull();
|
||||
expect(codes({ price_command: 'a\nb' })).toEqual(['PREGATE_CONFIG_INVALID:price_command']);
|
||||
expect(codes({ price_command: '' })).toEqual(['PREGATE_CONFIG_INVALID:price_command']);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user