@arjunkhera/atlas 0.4.11 → 0.4.12

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atlas",
3
- "version": "0.4.11",
3
+ "version": "0.4.12",
4
4
  "description": "Atlas: the delivery lifecycle, its crews and the Atlas tools, for any repository.",
5
5
  "author": {
6
6
  "name": "Arjun Khera"
package/door/lib/gate.mjs CHANGED
@@ -218,36 +218,48 @@ export const gateRunTitle = (pr, sha) => `gate #${pr} at ${sha}`;
218
218
  // Why one workflow run cannot carry a required check of this pull request,
219
219
  // or null when it can. The run must come from a base file that defines the
220
220
  // check, from a pull request event, on this head, for this pull request into
221
- // its base branch.
222
- export function refusal(run, { name, pr, sha, baseRef, homes }) {
221
+ // its base branch. The gate runs on pull_request_target, and its title must
222
+ // name this pull request and this head. The probe of 26 October (done line
223
+ // 15, atlas#211) showed that its run and its check run sit on the head commit,
224
+ // like the others.
225
+ export function refusal(run, { name, pr, sha, baseRef, homes, sides = () => [] }) {
223
226
  if (!run) return 'no workflow run';
224
227
  if (!homes.get(name)?.has(run.path)) return `from ${run.path ?? 'no file'}, not a base file that defines the check`;
225
- if (run.event !== 'pull_request') return `from the event ${run.event}`;
228
+ const event = name === GATE_CHECK ? 'pull_request_target' : 'pull_request';
229
+ if (run.event !== event) return `from the event ${run.event}`;
226
230
  if (run.head_sha !== sha) return 'of another head commit';
231
+ if (name === GATE_CHECK && run.display_title !== gateRunTitle(pr, sha)) return 'of another pull request or head';
227
232
  const pulls = run.pull_requests ?? [];
228
233
  if (!pulls.some((each) => Number(each.number) === Number(pr) && each.base?.ref === baseRef)) return `not of #${pr} into ${baseRef}`;
229
234
  // GitHub lists every open pull request with this head, so a run made for a
230
235
  // pull request into a side branch can also name this one. Such a run used
231
236
  // the side branch's file, so it does not count.
232
237
  if (pulls.some((each) => each.base?.ref !== baseRef)) return `also of a pull request into ${pulls.find((each) => each.base?.ref !== baseRef).base?.ref ?? 'another branch'}`;
233
- return null;
234
- }
235
-
236
- // The same for a run of the gate. It runs on pull_request_target, so it sits
237
- // on a commit of the base branch, not on the head. Its title names the pull
238
- // request and the head; the run must come from the base branch and its file.
239
- export function gateRefusal(run, { pr, sha, baseRef, onBase }) {
240
- if (run.path !== GATE_WORKFLOW) return `from ${run.path ?? 'no file'}, not ${GATE_WORKFLOW}`;
241
- if (run.event !== 'pull_request_target') return `from the event ${run.event}`;
242
- if (run.display_title !== gateRunTitle(pr, sha)) return 'of another pull request or head';
243
- if (run.head_branch !== baseRef) return `from the branch ${run.head_branch}, not ${baseRef}`;
244
- if (!onBase(run.head_sha)) return `from a commit that is not on ${baseRef}`;
238
+ // GitHub builds that list when it is read, from the open pull requests of
239
+ // the head branch. Once a side pull request closes, its gate run names only
240
+ // this one, and its title can be set to match. So the gate also refuses a
241
+ // head branch that ever had a pull request into another branch.
242
+ if (name === GATE_CHECK) {
243
+ const side = sides();
244
+ if (side === null) return 'no head branch to check for side pull requests';
245
+ if (side.length) return `the head branch also had #${side[0].number} into ${side[0].base?.ref ?? 'another branch'}`;
246
+ }
245
247
  return null;
246
248
  }
247
249
 
248
250
  // The check runs that may count for this pull request, and the ones refused.
249
- function pinnedRuns(gh, repo, { pr, sha, baseRef, checks }) {
251
+ function pinnedRuns(gh, repo, { pr, sha, baseRef, head, checks }) {
250
252
  const homes = homesAt(gh, repo, baseRef, checks);
253
+ // Every pull request, open or closed, from the head branch into another
254
+ // branch. Read once, and only when a gate run needs it.
255
+ let sideList;
256
+ const sides = () => {
257
+ if (sideList !== undefined) return sideList;
258
+ const owner = head?.repo?.owner?.login ?? head?.user?.login;
259
+ if (!head?.ref || !owner) return (sideList = null);
260
+ const all = gh('GET', `repos/${repo}/pulls?head=${encodeURIComponent(`${owner}:${head.ref}`)}&state=all&per_page=100`) ?? [];
261
+ return (sideList = all.filter((each) => each.base?.ref !== baseRef));
262
+ };
251
263
  const suites = new Map();
252
264
  const runOf = (id) => {
253
265
  if (!suites.has(id)) suites.set(id, id ? (gh('GET', `repos/${repo}/actions/runs?check_suite_id=${id}`)?.workflow_runs ?? [])[0] ?? null : null);
@@ -256,21 +268,10 @@ function pinnedRuns(gh, repo, { pr, sha, baseRef, checks }) {
256
268
  const kept = [];
257
269
  const refused = [];
258
270
  for (const check of checkRuns(gh, repo, sha)) {
259
- if (!checks.includes(check.name) || check.name === GATE_CHECK || check.app?.id !== ACTIONS_APP_ID) continue;
260
- const why = refusal(runOf(check.check_suite?.id), { name: check.name, pr, sha, baseRef, homes });
271
+ if (!checks.includes(check.name) || check.app?.id !== ACTIONS_APP_ID) continue;
272
+ const why = refusal(runOf(check.check_suite?.id), { name: check.name, pr, sha, baseRef, homes, sides });
261
273
  if (why) refused.push({ name: check.name, why }); else kept.push(check);
262
274
  }
263
- if (checks.includes(GATE_CHECK)) {
264
- const onBase = (commit) => ['ahead', 'identical'].includes(gh('GET', `repos/${repo}/compare/${commit}...${encodeURIComponent(baseRef)}`)?.status);
265
- const runs = gh('GET', `repos/${repo}/actions/workflows/${GATE_WORKFLOW.split('/').pop()}/runs?event=pull_request_target&per_page=100`)?.workflow_runs ?? [];
266
- for (const run of runs.filter((each) => each.display_title === gateRunTitle(pr, sha))) {
267
- const why = gateRefusal(run, { pr, sha, baseRef, onBase });
268
- if (why) { refused.push({ name: GATE_CHECK, why }); continue; }
269
- for (const job of gh('GET', `repos/${repo}/actions/runs/${run.id}/jobs`)?.jobs ?? []) {
270
- if (job.name === GATE_CHECK) kept.push({ ...job, app: { id: ACTIONS_APP_ID } });
271
- }
272
- }
273
- }
274
275
  return { kept, refused };
275
276
  }
276
277
 
@@ -283,7 +284,7 @@ export function readyCommand({ repo, pr, answer }, line, gh = ghApi) {
283
284
  if (pull?.state !== 'open') { line(`atlas gate ready: pull request #${pr} is ${pull?.merged ? 'merged' : pull?.state ?? 'not found'}. NOT READY.`); return 1; }
284
285
  const sha = pull.head.sha;
285
286
  const checks = checksAt(gh, repo, pull.base.ref);
286
- const { kept, refused } = pinnedRuns(gh, repo, { pr, sha, baseRef: pull.base.ref, checks });
287
+ const { kept, refused } = pinnedRuns(gh, repo, { pr, sha, baseRef: pull.base.ref, head: pull.head, checks });
287
288
  const rows = checkStates(checks, kept);
288
289
  line(`atlas gate ready: #${pr} at ${sha.slice(0, 7)}, ${checks.length} required check(s) from atlas.yaml on ${pull.base.ref}.`);
289
290
  for (const row of rows) line(checkLine(row));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arjunkhera/atlas",
3
- "version": "0.4.11",
3
+ "version": "0.4.12",
4
4
  "description": "Atlas: the delivery lifecycle, its crews and the Atlas tools, as a Claude Code plugin for any repository.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",