vouchington-tooling 0.2.0 → 0.3.0
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 +32 -0
- package/dist/agent-blackboard/index.d.mts +41 -0
- package/dist/agent-blackboard/index.mjs +94 -0
- package/dist/agent-blackboard/session-id.d.mts +1 -0
- package/dist/agent-blackboard/session-id.mjs +5 -0
- package/dist/agent-blackboard/snapshot-cleanup-directory.d.mts +11 -0
- package/dist/agent-blackboard/snapshot-cleanup-directory.mjs +66 -0
- package/dist/agent-blackboard/snapshot-cleanup-key.d.mts +14 -0
- package/dist/agent-blackboard/snapshot-cleanup-key.mjs +129 -0
- package/dist/agent-blackboard/snapshot-cleanup-receipt.d.mts +6 -0
- package/dist/agent-blackboard/snapshot-cleanup-receipt.mjs +117 -0
- package/dist/agent-blackboard/snapshot-cleanup-resume.d.mts +16 -0
- package/dist/agent-blackboard/snapshot-cleanup-resume.mjs +98 -0
- package/dist/agent-blackboard/snapshot-partition-cleanup.d.mts +13 -0
- package/dist/agent-blackboard/snapshot-partition-cleanup.mjs +136 -0
- package/dist/agent-blackboard/snapshot-partition-format.d.mts +32 -0
- package/dist/agent-blackboard/snapshot-partition-format.mjs +151 -0
- package/dist/agent-blackboard/snapshot-partition-io.d.mts +4 -0
- package/dist/agent-blackboard/snapshot-partition-io.mjs +37 -0
- package/dist/agent-blackboard/snapshot-partition-read.d.mts +9 -0
- package/dist/agent-blackboard/snapshot-partition-read.mjs +70 -0
- package/dist/agent-blackboard/snapshot-partition-validate.d.mts +4 -0
- package/dist/agent-blackboard/snapshot-partition-validate.mjs +40 -0
- package/dist/agent-blackboard/snapshot-partition-write.d.mts +2 -0
- package/dist/agent-blackboard/snapshot-partition-write.mjs +91 -0
- package/dist/agent-blackboard/snapshot-partitions.d.mts +10 -0
- package/dist/agent-blackboard/snapshot-partitions.mjs +102 -0
- package/dist/agent-blackboard/snapshot-types.d.mts +67 -0
- package/dist/agent-blackboard/snapshot-types.mjs +1 -0
- package/dist/agent-blackboard/snapshot.d.mts +3 -0
- package/dist/agent-blackboard/snapshot.mjs +2 -0
- package/dist/cli/commands/agent-blackboard.d.mts +3 -0
- package/dist/cli/commands/agent-blackboard.mjs +114 -0
- package/dist/cli/commands/retrospective-facts.d.mts +1 -0
- package/dist/cli/commands/retrospective-facts.mjs +33 -0
- package/dist/cli/index.mjs +11 -0
- package/dist/cli/parse.d.mts +7 -1
- package/dist/cli/parse.mjs +6 -0
- package/dist/cli/usage.d.mts +1 -1
- package/dist/cli/usage.mjs +12 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +2 -0
- package/dist/retrospective-facts/exec.d.mts +3 -0
- package/dist/retrospective-facts/exec.mjs +19 -0
- package/dist/retrospective-facts/foreign.d.mts +2 -0
- package/dist/retrospective-facts/foreign.mjs +36 -0
- package/dist/retrospective-facts/format.d.mts +10 -0
- package/dist/retrospective-facts/format.mjs +64 -0
- package/dist/retrospective-facts/index.d.mts +3 -0
- package/dist/retrospective-facts/index.mjs +26 -0
- package/dist/retrospective-facts/local.d.mts +2 -0
- package/dist/retrospective-facts/local.mjs +144 -0
- package/dist/retrospective-facts/shared.d.mts +17 -0
- package/dist/retrospective-facts/shared.mjs +1 -0
- package/package.json +21 -2
- package/scripts/gha/harness-admission-lane.sh +26 -0
- package/scripts/gha/harness-assert-gates.sh +24 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { shell } from './exec.mjs';
|
|
2
|
+
import { foreignFacts } from './foreign.mjs';
|
|
3
|
+
import { localFacts } from './local.mjs';
|
|
4
|
+
export async function runRetrospectiveFacts(options) {
|
|
5
|
+
validate(options);
|
|
6
|
+
const execute = options.execute ?? shell;
|
|
7
|
+
return options.repo ? foreignFacts(options, execute) : localFacts(options, execute);
|
|
8
|
+
}
|
|
9
|
+
function validate(options) {
|
|
10
|
+
if (options.pr !== undefined && !/^\d+$/.test(options.pr))
|
|
11
|
+
throw new Error('--pr requires a number');
|
|
12
|
+
if (options.branch !== undefined && (!options.branch || options.branch.startsWith('-')))
|
|
13
|
+
throw new Error('--branch requires a name');
|
|
14
|
+
if (options.repo !== undefined && !/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(options.repo))
|
|
15
|
+
throw new Error('--repo requires an owner/name value');
|
|
16
|
+
if (options.pr && options.noPr)
|
|
17
|
+
throw new Error('--pr and --no-pr are mutually exclusive');
|
|
18
|
+
if (options.repo && options.branch)
|
|
19
|
+
throw new Error('--branch cannot be combined with --repo');
|
|
20
|
+
if (options.repo && options.noPr)
|
|
21
|
+
throw new Error('--no-pr cannot be combined with --repo');
|
|
22
|
+
if (options.repo && !options.pr)
|
|
23
|
+
throw new Error('--repo requires --pr (a foreign repo has no current PR for this local branch)');
|
|
24
|
+
if (!options.pr && !options.branch && !options.noPr)
|
|
25
|
+
throw new Error('pass --pr <number>, --branch <name>, or --no-pr');
|
|
26
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { rawBlock } from './exec.mjs';
|
|
2
|
+
import { apiFiles, count, dirs, format, objectField, readJson, stringField, topDirs, } from './format.mjs';
|
|
3
|
+
import { PR_JSON_FIELDS, } from './shared.mjs';
|
|
4
|
+
export async function localFacts(options, execute) {
|
|
5
|
+
const calls = [];
|
|
6
|
+
const run = async (command, args) => {
|
|
7
|
+
const result = await execute(command, args);
|
|
8
|
+
calls.push({ command, args, result });
|
|
9
|
+
return result;
|
|
10
|
+
};
|
|
11
|
+
const mainSpec = 'main:refs/remotes/origin/main';
|
|
12
|
+
const branchSpec = options.branch
|
|
13
|
+
? `${options.branch}:refs/remotes/origin/${options.branch}`
|
|
14
|
+
: undefined;
|
|
15
|
+
const fetch = await run('git', ['fetch', 'origin', mainSpec]);
|
|
16
|
+
const branchFetch = branchSpec ? await run('git', ['fetch', 'origin', branchSpec]) : undefined;
|
|
17
|
+
const originMain = fetch.ok ||
|
|
18
|
+
(await run('git', ['rev-parse', '--verify', '--quiet', 'refs/remotes/origin/main'])).ok;
|
|
19
|
+
const branchResult = await run('git', ['branch', '--show-current']);
|
|
20
|
+
const localBranch = text(branchResult) || 'unavailable';
|
|
21
|
+
const selector = options.noPr ? undefined : (options.pr ?? options.branch);
|
|
22
|
+
const ghArgs = selector ? ['pr', 'view', selector, '--json', PR_JSON_FIELDS] : undefined;
|
|
23
|
+
const gh = ghArgs ? await run('gh', ghArgs) : undefined;
|
|
24
|
+
const data = gh?.ok ? readJson(gh.stdout) : undefined;
|
|
25
|
+
const state = options.noPr
|
|
26
|
+
? 'none'
|
|
27
|
+
: stringField(data, 'state', gh?.ok ? 'unavailable' : gh ? 'gh failed' : 'unavailable');
|
|
28
|
+
const head = stringField(data, 'headRefName');
|
|
29
|
+
const branch = options.branch ??
|
|
30
|
+
(options.pr && head !== 'unavailable' ? head : options.noPr ? localBranch : 'unavailable');
|
|
31
|
+
const rangeName = options.branch ?? (options.noPr ? localBranch : undefined);
|
|
32
|
+
const localRange = options.noPr && options.branch === undefined && localBranch === 'unavailable'
|
|
33
|
+
? 'HEAD'
|
|
34
|
+
: rangeName;
|
|
35
|
+
const resolved = localRange
|
|
36
|
+
? await resolveNamedRef(localRange, run, localRange === options.branch && branchFetch?.ok === true)
|
|
37
|
+
: unresolvedRange();
|
|
38
|
+
const range = resolved.range;
|
|
39
|
+
const commitsResult = !data && range && originMain
|
|
40
|
+
? await run('git', ['rev-list', '--count', `origin/main..${range}`])
|
|
41
|
+
: undefined;
|
|
42
|
+
const diffResult = !data && range && originMain
|
|
43
|
+
? await run('git', ['diff', '--name-only', `origin/main...${range}`])
|
|
44
|
+
: undefined;
|
|
45
|
+
const scoped = Boolean(options.branch &&
|
|
46
|
+
(localBranch !== options.branch || (head !== 'unavailable' && head !== options.branch))) || Boolean(options.pr && !options.branch && (head === 'unavailable' || head !== localBranch));
|
|
47
|
+
const scope = options.branch ?? `#${options.pr}`;
|
|
48
|
+
const status = scoped
|
|
49
|
+
? undefined
|
|
50
|
+
: await run('git', ['status', '--porcelain', '--untracked-files=normal']);
|
|
51
|
+
const reflog = scoped || branch === 'unavailable'
|
|
52
|
+
? undefined
|
|
53
|
+
: await run('git', ['reflog', 'show', `origin/${branch}`]);
|
|
54
|
+
const merge = objectField(data, 'mergeCommit', 'oid');
|
|
55
|
+
const merged = await mergeFact(data, state, merge, resolved.range, originMain, options, run);
|
|
56
|
+
const filesText = diffResult?.ok ? text(diffResult) : undefined;
|
|
57
|
+
const raw = options.raw
|
|
58
|
+
? `\n=== Raw Command Output ===\n${calls.map((call) => rawBlock(call.command, call.args, call.result)).join('')}`
|
|
59
|
+
: '';
|
|
60
|
+
return format({
|
|
61
|
+
fetch: `git fetch origin ${mainSpec}${branchSpec ? `; git fetch origin ${branchSpec}` : ''}`,
|
|
62
|
+
fetchStatus: fetch.ok ? 'ok' : 'failed',
|
|
63
|
+
fetchNote: `${fetch.ok
|
|
64
|
+
? 'origin/main refreshed'
|
|
65
|
+
: originMain
|
|
66
|
+
? 'using existing local origin/main ref after failed fetch'
|
|
67
|
+
: 'origin/main unavailable after failed fetch'}${resolved.refreshed ? `; origin/${localRange} refreshed` : ''}`,
|
|
68
|
+
branch,
|
|
69
|
+
pr: options.noPr ? 'none' : stringField(data, 'number'),
|
|
70
|
+
state,
|
|
71
|
+
mergedAt: stringField(data, 'mergedAt'),
|
|
72
|
+
mergeCommit: merge,
|
|
73
|
+
merged,
|
|
74
|
+
commits: data ? 'unavailable' : commitsResult?.ok ? text(commitsResult) : 'unavailable',
|
|
75
|
+
prCommits: data ? count(data, 'commits') : undefined,
|
|
76
|
+
files: data
|
|
77
|
+
? apiFiles(data)
|
|
78
|
+
: filesText === undefined
|
|
79
|
+
? 'unavailable'
|
|
80
|
+
: String(lines(filesText).length),
|
|
81
|
+
dirs: data ? dirs(data) : filesText === undefined ? 'unavailable' : topDirs(filesText),
|
|
82
|
+
changeSource: data ? 'api' : 'local',
|
|
83
|
+
remote: reflog === undefined
|
|
84
|
+
? undefined
|
|
85
|
+
: reflog.ok
|
|
86
|
+
? String(lines(text(reflog)).length)
|
|
87
|
+
: 'unavailable',
|
|
88
|
+
pushes: reflog === undefined
|
|
89
|
+
? undefined
|
|
90
|
+
: reflog.ok
|
|
91
|
+
? String(lines(text(reflog)).filter((line) => line.includes('update by push')).length)
|
|
92
|
+
: 'unavailable',
|
|
93
|
+
...(scoped ? { scoped: scope } : {}),
|
|
94
|
+
working: status === undefined
|
|
95
|
+
? undefined
|
|
96
|
+
: status.ok
|
|
97
|
+
? String(lines(text(status)).length)
|
|
98
|
+
: 'unavailable',
|
|
99
|
+
}, raw);
|
|
100
|
+
}
|
|
101
|
+
function text(result) {
|
|
102
|
+
return result.stdout.trim();
|
|
103
|
+
}
|
|
104
|
+
function lines(value) {
|
|
105
|
+
return value.split('\n').filter(Boolean);
|
|
106
|
+
}
|
|
107
|
+
async function resolveNamedRef(name, run, fetched = false) {
|
|
108
|
+
if (name === 'HEAD')
|
|
109
|
+
return { range: name, refreshed: false };
|
|
110
|
+
if ((await run('git', ['rev-parse', '--verify', '--quiet', `refs/heads/${name}`])).ok)
|
|
111
|
+
return { range: name, refreshed: false };
|
|
112
|
+
const fetch = fetched
|
|
113
|
+
? { ok: true, stdout: '', stderr: '' }
|
|
114
|
+
: await run('git', ['fetch', 'origin', `${name}:refs/remotes/origin/${name}`]);
|
|
115
|
+
if (!fetch.ok)
|
|
116
|
+
return { range: undefined, refreshed: false };
|
|
117
|
+
if ((await run('git', ['rev-parse', '--verify', '--quiet', `refs/remotes/origin/${name}`])).ok)
|
|
118
|
+
return { range: `origin/${name}`, refreshed: true };
|
|
119
|
+
return { range: undefined, refreshed: false };
|
|
120
|
+
}
|
|
121
|
+
function unresolvedRange() {
|
|
122
|
+
return { range: undefined, refreshed: false };
|
|
123
|
+
}
|
|
124
|
+
async function mergeFact(data, state, merge, range, originMain, options, run) {
|
|
125
|
+
if (merge && originMain) {
|
|
126
|
+
const inOrigin = await run('git', ['merge-base', '--is-ancestor', merge, 'origin/main']);
|
|
127
|
+
if (!inOrigin.ok)
|
|
128
|
+
return inOrigin.exitCode === 1 ? `no (origin/main lacks ${merge})` : 'unavailable';
|
|
129
|
+
const inLocalMain = await run('git', ['merge-base', '--is-ancestor', merge, 'main']);
|
|
130
|
+
if (!inLocalMain.ok && inLocalMain.exitCode === 1)
|
|
131
|
+
options.onWarning?.(`Warning: local main lacks PR merge commit ${merge}, but origin/main contains it.`);
|
|
132
|
+
return `yes (origin/main contains ${merge})`;
|
|
133
|
+
}
|
|
134
|
+
if (data && (state === 'OPEN' || state === 'CLOSED'))
|
|
135
|
+
return 'unmerged at time of retro';
|
|
136
|
+
if (!data && range && originMain) {
|
|
137
|
+
const rangeInOrigin = await run('git', ['merge-base', '--is-ancestor', range, 'origin/main']);
|
|
138
|
+
if (rangeInOrigin.ok)
|
|
139
|
+
return `yes (origin/main contains ${range})`;
|
|
140
|
+
if (rangeInOrigin.exitCode === 1)
|
|
141
|
+
return `no (origin/main lacks ${range})`;
|
|
142
|
+
}
|
|
143
|
+
return 'unavailable';
|
|
144
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const PR_JSON_FIELDS = "number,state,mergedAt,mergeCommit,changedFiles,files,commits,headRefName,baseRefName";
|
|
2
|
+
export type CommandResult = {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
exitCode?: number | null;
|
|
7
|
+
};
|
|
8
|
+
export type CommandExecutor = (command: string, args: string[]) => Promise<CommandResult>;
|
|
9
|
+
export type RetrospectiveFactsOptions = {
|
|
10
|
+
pr?: string;
|
|
11
|
+
branch?: string;
|
|
12
|
+
noPr?: boolean;
|
|
13
|
+
repo?: string;
|
|
14
|
+
raw?: boolean;
|
|
15
|
+
execute?: CommandExecutor;
|
|
16
|
+
onWarning?: (message: string) => void;
|
|
17
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PR_JSON_FIELDS = 'number,state,mergedAt,mergeCommit,changedFiles,files,commits,headRefName,baseRefName';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vouchington-tooling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Vouchington CLI and extractable tooling libraries.",
|
|
5
5
|
"homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -46,6 +46,16 @@
|
|
|
46
46
|
"import": "./dist/retrospective-transcript/index.mjs",
|
|
47
47
|
"default": "./dist/retrospective-transcript/index.mjs"
|
|
48
48
|
},
|
|
49
|
+
"./retrospective-facts": {
|
|
50
|
+
"types": "./dist/retrospective-facts/index.d.mts",
|
|
51
|
+
"import": "./dist/retrospective-facts/index.mjs",
|
|
52
|
+
"default": "./dist/retrospective-facts/index.mjs"
|
|
53
|
+
},
|
|
54
|
+
"./agent-blackboard": {
|
|
55
|
+
"types": "./dist/agent-blackboard/index.d.mts",
|
|
56
|
+
"import": "./dist/agent-blackboard/index.mjs",
|
|
57
|
+
"default": "./dist/agent-blackboard/index.mjs"
|
|
58
|
+
},
|
|
49
59
|
"./session-friction": {
|
|
50
60
|
"types": "./dist/session-friction/index.d.mts",
|
|
51
61
|
"import": "./dist/session-friction/index.mjs",
|
|
@@ -239,7 +249,16 @@
|
|
|
239
249
|
"yaml": "2.9.0"
|
|
240
250
|
},
|
|
241
251
|
"devDependencies": {
|
|
242
|
-
"@types/picomatch": "^4.0.3"
|
|
252
|
+
"@types/picomatch": "^4.0.3",
|
|
253
|
+
"agent-blackboard": "^0.3.1"
|
|
254
|
+
},
|
|
255
|
+
"peerDependencies": {
|
|
256
|
+
"agent-blackboard": "^0.3.1"
|
|
257
|
+
},
|
|
258
|
+
"peerDependenciesMeta": {
|
|
259
|
+
"agent-blackboard": {
|
|
260
|
+
"optional": true
|
|
261
|
+
}
|
|
243
262
|
},
|
|
244
263
|
"optionalDependencies": {
|
|
245
264
|
"@libpg-query/parser": "^18.0.0"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
if [ "${#}" -ne 1 ]; then
|
|
5
|
+
echo "usage: harness-admission-lane.sh <lanes>" >&2
|
|
6
|
+
exit 2
|
|
7
|
+
fi
|
|
8
|
+
|
|
9
|
+
if [ -z "${GITHUB_OUTPUT:-}" ]; then
|
|
10
|
+
echo 'GITHUB_OUTPUT must be set' >&2
|
|
11
|
+
exit 2
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
LANES="$1"
|
|
15
|
+
if ! [[ "$LANES" =~ ^[1-9][0-9]*$ ]]; then
|
|
16
|
+
echo "::error::admission lane count must be a positive integer, got: '$LANES'"
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
RUN_ID="${GITHUB_RUN_ID:-}"
|
|
21
|
+
if ! [[ "$RUN_ID" =~ ^[0-9]+$ ]]; then
|
|
22
|
+
echo "::error::GITHUB_RUN_ID must be a non-negative integer, got: '${RUN_ID}'"
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
echo "lane=$(( RUN_ID % LANES ))" >> "$GITHUB_OUTPUT"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
if [ "$#" -eq 0 ]; then
|
|
5
|
+
echo "usage: harness-assert-gates.sh <gate>..." >&2
|
|
6
|
+
exit 2
|
|
7
|
+
fi
|
|
8
|
+
|
|
9
|
+
for gate in "$@"; do
|
|
10
|
+
if ! [[ "$gate" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
|
|
11
|
+
echo "::error::invalid gate name: '$gate'"
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
done
|
|
15
|
+
|
|
16
|
+
for gate in "$@"; do
|
|
17
|
+
value="${!gate:-}"
|
|
18
|
+
if [ "$value" = "true" ]; then
|
|
19
|
+
echo "::error::$gate must be disabled but is enabled"
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
done
|
|
23
|
+
|
|
24
|
+
exit 0
|