@bakers-build/addon-forge 0.9.1-next.8 → 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.
@@ -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
- * Checks on `ref` (a SHA or branch), `required` as `base`'s rules say. Only
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;
@@ -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
- /** Text over HTTP for backends: throws status and body text on failure. */
49
- export async function requestText(doFetch, url, init) {
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
- }
@@ -1,4 +1,4 @@
1
- // The forge addon: six conductor tools a sandboxed run delivers through,
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. Each row: status, required, the forge's raw conclusion (cancelled, skipped, timed_out, ...), run_id, job_id (for forge_job_log), run_started_at. overall is success only when every required check (flagged, or named required) is green; else failure, pending or no_required_checks.",
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 overall is success. Human approval is the workflow's merge gate, not this tool's.",
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
  };
@@ -6,13 +6,8 @@ export interface ForgeToolDeps {
6
6
  /** The forge entry's `with` (merge rules). */
7
7
  settings: unknown;
8
8
  }
9
- export declare const LOG_DEFAULT_BYTES = 16384;
10
- export declare const LOG_MAX_BYTES = 262144;
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
- /** Required by the forge, or the aggregate check a workflow names `required`. */
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(isRequired);
37
- if (required.length === 0)
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
- if (overall(checks) === 'no_required_checks')
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.conclusion ?? c.status})`).join(', ')}`);
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.9.1-next.8",
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.9.1-next.8",
26
- "@bakers-build/core": "^0.9.1-next.8"
25
+ "@bakers-build/conductor": "^0.10.0",
26
+ "@bakers-build/core": "^0.10.0"
27
27
  },
28
28
  "bakersBuild": {
29
- "commit": "41eb5631e4ddf107b0b7a5ce2ffe2ed2a6052b5e",
30
- "channel": "next"
29
+ "commit": "5857b00008bc0926fd52fdc8d9366d6d52444e35",
30
+ "channel": "latest"
31
31
  }
32
32
  }