@bakers-build/addon-forge 0.9.1-next.9 → 0.10.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/dist/src/backend.d.ts +2 -31
- package/dist/src/backend.js +2 -29
- package/dist/src/preset.js +1 -1
- package/dist/src/tool-definitions.js +2 -21
- package/dist/src/tools.d.ts +2 -7
- package/dist/src/tools.js +6 -35
- package/package.json +5 -5
package/dist/src/backend.d.ts
CHANGED
|
@@ -15,13 +15,6 @@ export interface ForgeCheck {
|
|
|
15
15
|
readonly status: CheckStatus;
|
|
16
16
|
/** Required by the base branch's rules (GitHub) or not allowed to fail (GitLab). */
|
|
17
17
|
readonly required: boolean;
|
|
18
|
-
/** The forge's own verdict (`cancelled`, `skipped`, `timed_out`, ...); null while running. */
|
|
19
|
-
readonly conclusion: string | null;
|
|
20
|
-
/** GitHub Actions run / GitLab pipeline; null for a commit status. */
|
|
21
|
-
readonly run_id: number | null;
|
|
22
|
-
/** The id `forge_job_log` reads; null when the row is no job. */
|
|
23
|
-
readonly job_id: number | null;
|
|
24
|
-
readonly run_started_at: string | null;
|
|
25
18
|
}
|
|
26
19
|
export interface ForgeMerge {
|
|
27
20
|
readonly method: 'squash' | 'merge';
|
|
@@ -47,26 +40,13 @@ export interface ForgeBackend {
|
|
|
47
40
|
body: string;
|
|
48
41
|
}): Promise<ForgePr>;
|
|
49
42
|
openPrsWithBase(base: string): Promise<ForgePr[]>;
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
* the newest run per workflow unless `allRuns`, where the forge has several.
|
|
53
|
-
*/
|
|
54
|
-
checks(ref: string, base: string, opts?: {
|
|
55
|
-
allRuns?: boolean;
|
|
56
|
-
}): Promise<ForgeCheck[]>;
|
|
43
|
+
/** Checks on `ref` (a SHA or branch), `required` as `base`'s rules say. */
|
|
44
|
+
checks(ref: string, base: string): Promise<ForgeCheck[]>;
|
|
57
45
|
/** A conflict or a not yet computed mergeability is a reason. */
|
|
58
46
|
mergeBlocker(pr: ForgePr): Promise<string | undefined>;
|
|
59
47
|
/** Merges with every option sent explicitly; deletes the source when asked. */
|
|
60
48
|
merge(pr: ForgePr, options: ForgeMerge): Promise<void>;
|
|
61
49
|
isMerged(pr: ForgePr): Promise<boolean>;
|
|
62
|
-
/** The [tail] of a job's log; refuses a job outside the repository. */
|
|
63
|
-
jobLog(jobId: number, maxBytes: number): Promise<LogTail>;
|
|
64
|
-
}
|
|
65
|
-
export interface LogTail {
|
|
66
|
-
readonly text: string;
|
|
67
|
-
/** The whole log's size. */
|
|
68
|
-
readonly bytes: number;
|
|
69
|
-
readonly truncated: boolean;
|
|
70
50
|
}
|
|
71
51
|
export type ForgeBackendFactory = (settings: unknown, context: {
|
|
72
52
|
readonly home: string;
|
|
@@ -80,16 +60,7 @@ export declare function assertForgeBackend(value: unknown, spec: string): assert
|
|
|
80
60
|
* Every value read is pushed to `seen`, for the backend's `secrets()`.
|
|
81
61
|
*/
|
|
82
62
|
export declare function cliToken(cmd: string, args: string[], seen: string[]): () => Promise<string>;
|
|
83
|
-
/** Text over HTTP for backends: throws status and body text on failure. */
|
|
84
|
-
export declare function requestText(doFetch: typeof fetch, url: string, init: RequestInit & {
|
|
85
|
-
allow404?: boolean;
|
|
86
|
-
}): Promise<string | undefined>;
|
|
87
63
|
/** JSON over HTTP for backends: throws status and body text on failure. */
|
|
88
64
|
export declare function requestJson(doFetch: typeof fetch, url: string, init: RequestInit & {
|
|
89
65
|
allow404?: boolean;
|
|
90
66
|
}): Promise<unknown>;
|
|
91
|
-
/**
|
|
92
|
-
* The last `maxBytes` bytes of `log`, from the first whole line in them; when
|
|
93
|
-
* the last line alone is longer, from the first whole character.
|
|
94
|
-
*/
|
|
95
|
-
export declare function tail(log: string, maxBytes: number): LogTail;
|
package/dist/src/backend.js
CHANGED
|
@@ -14,7 +14,6 @@ const METHODS = [
|
|
|
14
14
|
'mergeBlocker',
|
|
15
15
|
'merge',
|
|
16
16
|
'isMerged',
|
|
17
|
-
'jobLog',
|
|
18
17
|
];
|
|
19
18
|
/** Reject a factory result that is not a `ForgeBackend`. */
|
|
20
19
|
export function assertForgeBackend(value, spec) {
|
|
@@ -45,39 +44,13 @@ export function cliToken(cmd, args, seen) {
|
|
|
45
44
|
return value;
|
|
46
45
|
};
|
|
47
46
|
}
|
|
48
|
-
/**
|
|
49
|
-
export async function
|
|
47
|
+
/** JSON over HTTP for backends: throws status and body text on failure. */
|
|
48
|
+
export async function requestJson(doFetch, url, init) {
|
|
50
49
|
const res = await doFetch(url, init);
|
|
51
50
|
if (res.status === 404 && init.allow404)
|
|
52
51
|
return undefined;
|
|
53
52
|
const text = await res.text();
|
|
54
53
|
if (!res.ok)
|
|
55
54
|
throw new Error(`${init.method ?? 'GET'} ${new URL(url).pathname} answered ${res.status}: ${text.slice(0, 500)}`);
|
|
56
|
-
return text;
|
|
57
|
-
}
|
|
58
|
-
/** JSON over HTTP for backends: throws status and body text on failure. */
|
|
59
|
-
export async function requestJson(doFetch, url, init) {
|
|
60
|
-
const text = await requestText(doFetch, url, init);
|
|
61
55
|
return text ? JSON.parse(text) : undefined;
|
|
62
56
|
}
|
|
63
|
-
/**
|
|
64
|
-
* The last `maxBytes` bytes of `log`, from the first whole line in them; when
|
|
65
|
-
* the last line alone is longer, from the first whole character.
|
|
66
|
-
*/
|
|
67
|
-
export function tail(log, maxBytes) {
|
|
68
|
-
const buf = Buffer.from(log);
|
|
69
|
-
if (buf.length <= maxBytes)
|
|
70
|
-
return { text: log, bytes: buf.length, truncated: false };
|
|
71
|
-
let start = buf.length - maxBytes;
|
|
72
|
-
const newline = buf.indexOf(0x0a, start - 1);
|
|
73
|
-
if (newline !== -1 && newline < buf.length - 1)
|
|
74
|
-
start = newline + 1;
|
|
75
|
-
else
|
|
76
|
-
while (start < buf.length && (buf.readUInt8(start) & 0xc0) === 0x80)
|
|
77
|
-
start++;
|
|
78
|
-
return {
|
|
79
|
-
text: buf.subarray(start).toString(),
|
|
80
|
-
bytes: buf.length,
|
|
81
|
-
truncated: true,
|
|
82
|
-
};
|
|
83
|
-
}
|
package/dist/src/preset.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The forge addon:
|
|
1
|
+
// The forge addon: five conductor tools a sandboxed run delivers through,
|
|
2
2
|
// while the token stays with the conductor. Without a forge entry no tool is
|
|
3
3
|
// served and runs deliver as before.
|
|
4
4
|
import { FORGE_TOOL_DEFINITIONS } from './tool-definitions.js';
|
|
@@ -42,36 +42,17 @@ export const FORGE_TOOL_DEFINITIONS = {
|
|
|
42
42
|
},
|
|
43
43
|
},
|
|
44
44
|
forge_checks: {
|
|
45
|
-
description: "Status of each check or pipeline job on the head of an owned branch's PR/MR, or on any `ref` (branch or SHA); read only, one shot.
|
|
45
|
+
description: "Status of each check or pipeline job, and whether it is required, on the head of an owned branch's PR/MR, or on any `ref` (branch or SHA); read only, one shot.",
|
|
46
46
|
input: {
|
|
47
47
|
type: 'object',
|
|
48
48
|
properties: {
|
|
49
49
|
branch,
|
|
50
50
|
ref: { type: 'string', description: 'any branch or SHA, read only' },
|
|
51
|
-
all_runs: {
|
|
52
|
-
type: 'boolean',
|
|
53
|
-
description: 'every run on the commit; default: only the newest run per workflow',
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
forge_job_log: {
|
|
59
|
-
description: "The end of one job's log, by a job_id from forge_checks, read with the conductor's token; read only. Starts at a whole line; truncated says whether earlier lines were cut.",
|
|
60
|
-
input: {
|
|
61
|
-
type: 'object',
|
|
62
|
-
properties: {
|
|
63
|
-
job_id: { type: 'integer', minimum: 1 },
|
|
64
|
-
max_bytes: {
|
|
65
|
-
type: 'integer',
|
|
66
|
-
minimum: 1,
|
|
67
|
-
description: 'bytes from the end; default 16384, at most 262144',
|
|
68
|
-
},
|
|
69
51
|
},
|
|
70
|
-
required: ['job_id'],
|
|
71
52
|
},
|
|
72
53
|
},
|
|
73
54
|
forge_merge: {
|
|
74
|
-
description: "Merge the open PR/MR of an owned branch into its base, once it is conflict free and
|
|
55
|
+
description: "Merge the open PR/MR of an owned branch into its base, once it is conflict free and every required check is green. Human approval is the workflow's merge gate, not this tool's.",
|
|
75
56
|
input: { type: 'object', properties: { branch } },
|
|
76
57
|
},
|
|
77
58
|
};
|
package/dist/src/tools.d.ts
CHANGED
|
@@ -6,13 +6,8 @@ export interface ForgeToolDeps {
|
|
|
6
6
|
/** The forge entry's `with` (merge rules). */
|
|
7
7
|
settings: unknown;
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
export declare
|
|
11
|
-
export type Overall = 'success' | 'failure' | 'pending' | 'no_required_checks';
|
|
12
|
-
/** Required by the forge, or the aggregate check a workflow names `required`. */
|
|
13
|
-
export declare const isRequired: (c: ForgeCheck) => boolean;
|
|
14
|
-
/** Failure first, then pending, over the required checks; none required is never green. */
|
|
15
|
-
export declare function overall(checks: readonly ForgeCheck[]): Overall;
|
|
9
|
+
/** Failure first, then pending, over the required checks. */
|
|
10
|
+
export declare function overall(checks: readonly ForgeCheck[]): ForgeCheck['status'];
|
|
16
11
|
export declare function forgeTools(deps: ForgeToolDeps): Record<string, ToolHandler>;
|
|
17
12
|
/** The live tools for the conductor's effective forge entry `with`. */
|
|
18
13
|
export declare function toolsFor(options: unknown): Record<string, ToolHandler>;
|
package/dist/src/tools.js
CHANGED
|
@@ -23,20 +23,13 @@ function ownedBranch(req, run) {
|
|
|
23
23
|
throw new Error(ownershipRefusal(branch, run.branchName));
|
|
24
24
|
return branch;
|
|
25
25
|
}
|
|
26
|
-
export const LOG_DEFAULT_BYTES = 16_384;
|
|
27
|
-
export const LOG_MAX_BYTES = 262_144;
|
|
28
26
|
const json = (value) => ({
|
|
29
27
|
text: JSON.stringify(value, null, 2),
|
|
30
28
|
});
|
|
31
|
-
/**
|
|
32
|
-
export const isRequired = (c) => c.required || c.name === 'required';
|
|
33
|
-
const isRed = (c) => c.status === 'failure' || c.conclusion === 'skipped';
|
|
34
|
-
/** Failure first, then pending, over the required checks; none required is never green. */
|
|
29
|
+
/** Failure first, then pending, over the required checks. */
|
|
35
30
|
export function overall(checks) {
|
|
36
|
-
const required = checks.filter(
|
|
37
|
-
if (required.
|
|
38
|
-
return 'no_required_checks';
|
|
39
|
-
if (required.some(isRed))
|
|
31
|
+
const required = checks.filter((c) => c.required);
|
|
32
|
+
if (required.some((c) => c.status === 'failure'))
|
|
40
33
|
return 'failure';
|
|
41
34
|
if (required.some((c) => c.status !== 'success'))
|
|
42
35
|
return 'pending';
|
|
@@ -129,9 +122,7 @@ export function forgeTools(deps) {
|
|
|
129
122
|
ref = pr?.headSha ?? branch;
|
|
130
123
|
base = pr?.base ?? derivedBase(branch, run);
|
|
131
124
|
}
|
|
132
|
-
const checks = await b.checks(ref, base
|
|
133
|
-
allRuns: req.args.all_runs === true,
|
|
134
|
-
});
|
|
125
|
+
const checks = await b.checks(ref, base);
|
|
135
126
|
return json({ ref, base, overall: overall(checks), checks });
|
|
136
127
|
},
|
|
137
128
|
forge_merge: async (req) => {
|
|
@@ -150,11 +141,9 @@ export function forgeTools(deps) {
|
|
|
150
141
|
if (blocker)
|
|
151
142
|
throw new Error(`PR/MR ${pr.url} cannot merge: ${blocker}`);
|
|
152
143
|
const checks = await b.checks(pr.headSha, pr.base);
|
|
153
|
-
|
|
154
|
-
throw new Error(`PR/MR ${pr.url} cannot merge: no required checks reported`);
|
|
155
|
-
const notGreen = checks.filter((c) => isRequired(c) && (isRed(c) || c.status !== 'success'));
|
|
144
|
+
const notGreen = checks.filter((c) => c.required && c.status !== 'success');
|
|
156
145
|
if (notGreen.length > 0)
|
|
157
|
-
throw new Error(`PR/MR ${pr.url} has required checks that are not green: ${notGreen.map((c) => `${c.name} (${c.
|
|
146
|
+
throw new Error(`PR/MR ${pr.url} has required checks that are not green: ${notGreen.map((c) => `${c.name} (${c.status})`).join(', ')}`);
|
|
158
147
|
const rule = mergeRule(deps.settings, base);
|
|
159
148
|
const dependents = (await b.openPrsWithBase(branch)).filter((p) => p.number !== pr.number);
|
|
160
149
|
const deleteSource = rule.delete_source && dependents.length === 0;
|
|
@@ -177,24 +166,6 @@ export function forgeTools(deps) {
|
|
|
177
166
|
: {}),
|
|
178
167
|
});
|
|
179
168
|
},
|
|
180
|
-
forge_job_log: async (req) => {
|
|
181
|
-
runOf(req);
|
|
182
|
-
const jobId = req.args.job_id;
|
|
183
|
-
if (typeof jobId !== 'number' || !Number.isInteger(jobId) || jobId <= 0)
|
|
184
|
-
throw new Error('job_id must be a job_id from forge_checks');
|
|
185
|
-
const asked = req.args.max_bytes;
|
|
186
|
-
const maxBytes = typeof asked === 'number' && asked >= 1
|
|
187
|
-
? Math.min(Math.floor(asked), LOG_MAX_BYTES)
|
|
188
|
-
: LOG_DEFAULT_BYTES;
|
|
189
|
-
const log = await (await backend()).jobLog(jobId, maxBytes);
|
|
190
|
-
return json({
|
|
191
|
-
job_id: jobId,
|
|
192
|
-
bytes: log.bytes,
|
|
193
|
-
truncated: log.truncated,
|
|
194
|
-
max_bytes: maxBytes,
|
|
195
|
-
log: log.text,
|
|
196
|
-
});
|
|
197
|
-
},
|
|
198
169
|
};
|
|
199
170
|
return Object.fromEntries(Object.entries(tools).map(([n, h]) => [n, guarded(h)]));
|
|
200
171
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bakers-build/addon-forge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "bakers.build forge tools: a sandboxed run fetches, pushes its own branches, opens its PR/MR, reads checks and merges through the conductor, which holds the token. Backends implement the ForgeBackend contract (./backend).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
"directory": "bakers-cli/addons/forge"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@bakers-build/conductor": "^0.
|
|
26
|
-
"@bakers-build/core": "^0.
|
|
25
|
+
"@bakers-build/conductor": "^0.10.0",
|
|
26
|
+
"@bakers-build/core": "^0.10.0"
|
|
27
27
|
},
|
|
28
28
|
"bakersBuild": {
|
|
29
|
-
"commit": "
|
|
30
|
-
"channel": "
|
|
29
|
+
"commit": "5857b00008bc0926fd52fdc8d9366d6d52444e35",
|
|
30
|
+
"channel": "latest"
|
|
31
31
|
}
|
|
32
32
|
}
|