@bakers-build/addon-forge-github 0.10.0 → 0.10.1-next.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/dist/src/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // shortly before it expires; or, with `auth: 'gh'`, the conductor's own `gh`
4
4
  // login. The repository comes from config, never from a clone's `origin`.
5
5
  import { createSign } from 'node:crypto';
6
- import { cliToken, requestJson, } from '@bakers-build/addon-forge/backend';
6
+ import { cliToken, requestJson, requestText, tail, } from '@bakers-build/addon-forge/backend';
7
7
  const API = 'https://api.github.com';
8
8
  const REFRESH_MS = 60_000;
9
9
  function settingsOf(raw) {
@@ -64,6 +64,7 @@ function stateStatus(state) {
64
64
  return 'success';
65
65
  return state === 'pending' ? 'pending' : 'failure';
66
66
  }
67
+ const NO_RUN = { run_id: null, job_id: null, run_started_at: null };
67
68
  const basic = (token) => Buffer.from(`x-access-token:${token}`).toString('base64');
68
69
  const factory = async (raw, context) => {
69
70
  const s = settingsOf(raw);
@@ -101,7 +102,7 @@ const factory = async (raw, context) => {
101
102
  };
102
103
  return out.token;
103
104
  };
104
- const api = async (path, init = {}) => requestJson(doFetch, `${repo}${path}`, {
105
+ const api = async (path, init = {}) => (init.text ? requestText : requestJson)(doFetch, `${repo}${path}`, {
105
106
  method: init.method ?? 'GET',
106
107
  headers: {
107
108
  authorization: `Bearer ${await accessToken()}`,
@@ -128,18 +129,18 @@ const factory = async (raw, context) => {
128
129
  method: 'POST',
129
130
  body: { head, base, title, body },
130
131
  }))),
131
- updatePr: async (pr, { title, body }) => prOf((await api(`/pulls/${pr.number}`, {
132
+ updatePr: async (pr, { title, body, base }) => prOf((await api(`/pulls/${pr.number}`, {
132
133
  method: 'PATCH',
133
- body: { title, body },
134
+ body: { title, body, base },
134
135
  }))),
135
136
  openPrsWithBase: async (base) => (await api(`/pulls?state=open&base=${encodeURIComponent(base)}&per_page=100`))
136
137
  .filter((p) => p.base.ref === base)
137
138
  .map(prOf),
138
- checks: async (ref, base) => {
139
+ checks: async (ref, base, opts = {}) => {
139
140
  const at = encodeURIComponent(ref);
140
141
  const b = encodeURIComponent(base);
141
142
  const [runs, status, rules, branch] = await Promise.all([
142
- api(`/commits/${at}/check-runs?per_page=100`),
143
+ api(`/commits/${at}/check-runs?filter=all&per_page=100`),
143
144
  api(`/commits/${at}/status`),
144
145
  api(`/rules/branches/${b}`, { allow404: true }),
145
146
  api(`/branches/${b}`, { allow404: true }),
@@ -151,21 +152,67 @@ const factory = async (raw, context) => {
151
152
  .map((c) => c.context),
152
153
  ...(branch?.protection?.required_status_checks?.contexts ?? []),
153
154
  ]);
155
+ // shortcut: one page only; a partial list could hide a newer attempt, so refuse it; paginate when this trips.
156
+ if ((runs.total_count ?? 0) > runs.check_runs.length)
157
+ throw new Error(`${ref} has ${runs.total_count} check runs, more than the ${runs.check_runs.length} one page holds; the result would be partial`);
158
+ // `ref` may be a branch: the runs lookup needs the commit the check runs report.
159
+ const sha = runs.check_runs[0]?.head_sha;
160
+ const workflowRuns = sha
161
+ ? (await api(`/actions/runs?head_sha=${encodeURIComponent(sha)}&per_page=100`)).workflow_runs
162
+ : [];
163
+ const bySuite = new Map(workflowRuns.map((w) => [w.check_suite_id, w]));
164
+ const newest = new Map();
165
+ for (const w of workflowRuns) {
166
+ const kept = newest.get(w.workflow_id);
167
+ if (!kept ||
168
+ w.run_started_at > kept.run_started_at ||
169
+ (w.run_started_at === kept.run_started_at && w.id > kept.id))
170
+ newest.set(w.workflow_id, w);
171
+ }
172
+ let jobs = runs.check_runs.map((r) => ({
173
+ r,
174
+ w: r.check_suite ? bySuite.get(r.check_suite.id) : undefined,
175
+ }));
176
+ if (!opts.allRuns) {
177
+ // An Actions check run without a listed run belongs to an earlier attempt.
178
+ jobs = jobs.filter(({ r, w }) => w
179
+ ? newest.get(w.workflow_id) === w
180
+ : r.app?.slug !== 'github-actions');
181
+ // A re-run attempt is a newer check run of the same name in the same run.
182
+ const latest = new Map();
183
+ const key = (j) => `${j.w?.id ?? ''}\0${j.r.name}`;
184
+ for (const j of jobs)
185
+ latest.set(key(j), Math.max(latest.get(key(j)) ?? 0, j.r.id));
186
+ jobs = jobs.filter((j) => latest.get(key(j)) === j.r.id);
187
+ }
154
188
  const rows = [
155
- ...runs.check_runs.map((r) => ({
189
+ ...jobs.map(({ r, w }) => ({
156
190
  name: r.name,
157
191
  status: runStatus(r),
158
192
  required: required.has(r.name),
193
+ conclusion: r.conclusion,
194
+ run_id: w?.id ?? null,
195
+ // An Actions job's id is its check run's id.
196
+ job_id: w ? r.id : null,
197
+ run_started_at: w?.run_started_at ?? null,
159
198
  })),
160
199
  ...status.statuses.map((st) => ({
161
200
  name: st.context,
162
201
  status: stateStatus(st.state),
163
202
  required: required.has(st.context),
203
+ conclusion: st.state,
204
+ ...NO_RUN,
164
205
  })),
165
206
  ];
166
207
  for (const name of required)
167
208
  if (!rows.some((r) => r.name === name))
168
- rows.push({ name, status: 'missing', required: true });
209
+ rows.push({
210
+ name,
211
+ status: 'missing',
212
+ required: true,
213
+ conclusion: null,
214
+ ...NO_RUN,
215
+ });
169
216
  return rows;
170
217
  },
171
218
  mergeBlocker: async (pr) => {
@@ -190,6 +237,16 @@ const factory = async (raw, context) => {
190
237
  });
191
238
  },
192
239
  isMerged: async (pr) => (await pull(pr.number)).merged === true,
240
+ // The API redirects to a signed download URL; fetch follows it.
241
+ jobLog: async (jobId, maxBytes) => {
242
+ const log = (await api(`/actions/jobs/${jobId}/logs`, {
243
+ allow404: true,
244
+ text: true,
245
+ }));
246
+ if (log === undefined)
247
+ throw new Error(`job ${jobId} is not a job of ${s.repository}`);
248
+ return tail(log, maxBytes);
249
+ },
193
250
  };
194
251
  return backend;
195
252
  };
@@ -5,9 +5,7 @@
5
5
  "description": "The forge entry's with.backend.with: a GitHub App, or the conductor's gh login with auth 'gh'.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
- "required": [
9
- "repository"
10
- ],
8
+ "required": ["repository"],
11
9
  "properties": {
12
10
  "repository": {
13
11
  "type": "string",
@@ -19,18 +17,12 @@
19
17
  "description": "Use the conductor's own gh login instead of a GitHub App."
20
18
  },
21
19
  "app_id": {
22
- "type": [
23
- "integer",
24
- "string"
25
- ],
20
+ "type": ["integer", "string"],
26
21
  "minLength": 1,
27
22
  "description": "The GitHub App's ID; required unless `auth` is set."
28
23
  },
29
24
  "installation_id": {
30
- "type": [
31
- "integer",
32
- "string"
33
- ],
25
+ "type": ["integer", "string"],
34
26
  "minLength": 1,
35
27
  "description": "The App's installation on the repository; required unless `auth` is set."
36
28
  },
@@ -46,14 +38,9 @@
46
38
  }
47
39
  },
48
40
  "if": {
49
- "required": [
50
- "auth"
51
- ]
41
+ "required": ["auth"]
52
42
  },
53
43
  "else": {
54
- "required": [
55
- "app_id",
56
- "installation_id"
57
- ]
44
+ "required": ["app_id", "installation_id"]
58
45
  }
59
46
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bakers-build/addon-forge-github",
3
- "version": "0.10.0",
3
+ "version": "0.10.1-next.2",
4
4
  "description": "bakers.build forge backend for GitHub: a GitHub App installation token scoped to one repository. Implements @bakers-build/addon-forge's ForgeBackend contract.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -21,11 +21,11 @@
21
21
  "directory": "bakers-cli/addons/forge-github"
22
22
  },
23
23
  "peerDependencies": {
24
- "@bakers-build/addon-forge": "^0.10.0",
25
- "@bakers-build/core": "^0.10.0"
24
+ "@bakers-build/addon-forge": "^0.10.1-next.2",
25
+ "@bakers-build/core": "^0.10.1-next.2"
26
26
  },
27
27
  "bakersBuild": {
28
- "commit": "5857b00008bc0926fd52fdc8d9366d6d52444e35",
29
- "channel": "latest"
28
+ "commit": "1bc392c2e9aaebacbb1904ad2b55563da8784233",
29
+ "channel": "next"
30
30
  }
31
31
  }