From 9e49e0fdac889f7d08ffbaa05c08ad409bf0aed3 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Mon, 26 Oct 2026 03:00:07 +0900 Subject: [PATCH] e2e: update CI tests to use approval model Signed-off-by: Seongmin Lee --- e2e/harness/shared.ts | 32 ++++++++++++++++++--------- e2e/specs/fork-pulls.spec.ts | 38 ++++++++++++++------------------ e2e/specs/pull-pipelines.spec.ts | 10 ++++----- e2e/specs/refusals.spec.ts | 10 ++++----- 4 files changed, 47 insertions(+), 43 deletions(-) diff --git a/e2e/harness/shared.ts b/e2e/harness/shared.ts index 32b650957..ed6dc2db6 100644 --- a/e2e/harness/shared.ts +++ b/e2e/harness/shared.ts @@ -4,7 +4,7 @@ import { knotReply, knotRecordFor, labels, ticketRecord, tickets } from "./api"; import { config, type Account } from "./config"; import { flow, type Flow } from "./flow"; import { deleteBranch, lsRemote, pushAnnotatedTag } from "./git"; -import { get, ok, selfUri, sleep, until, xrpc, type Reply, type Written } from "./net"; +import { get, ok, selfUri, until, xrpc, type Reply, type Written } from "./net"; import { bobbin, gitmirror, @@ -234,9 +234,10 @@ steps: export const workflowPath = ".tangled/workflows/ci.yml"; interface Pipeline { + id: string; commit: string; trigger?: { $type?: string }; - workflows?: { id: string }[]; + workflows?: { id: string; status: string }[]; } export const pullPipelines = async (repoDid: string, sha: string) => { @@ -249,16 +250,25 @@ export const pullPipelines = async (repoDid: string, sha: string) => { ); }; -// spindle reads the stream within a second or two, so this long without a pipeline means none -export const runsNothing = +// a pull from someone who can't push makes pipelines that wait for the collaborator's approval +export const waitsForApproval = (sha: (ctx: Ctx) => string, repoDid: (ctx: Ctx) => string = (ctx) => ctx.repo.did) => - async (ctx: Ctx) => { - await sleep(15_000); - expect( - await pullPipelines(repoDid(ctx), sha(ctx)), - "pull request pipelines for the head" - ).toEqual([]); - }; + (ctx: Ctx) => + until( + `spindle should block the pull pipelines at ${sha(ctx)}`, + 30_000, + async () => { + const found = await pullPipelines(repoDid(ctx), sha(ctx)); + const blocked = + found.length > 0 && + found.every((p) => p.workflows?.every((w) => w.status === "blocked")); + return [ + blocked, + `it listed ${JSON.stringify(found.map((p) => p.workflows))}`, + found + ]; + } + ); export const pipelineFor = (event: "push" | "pullRequest", sha: (ctx: Ctx) => string) => (ctx: Ctx) => diff --git a/e2e/specs/fork-pulls.spec.ts b/e2e/specs/fork-pulls.spec.ts index 9e9fa8208..de4448907 100644 --- a/e2e/specs/fork-pulls.spec.ts +++ b/e2e/specs/fork-pulls.spec.ts @@ -22,7 +22,7 @@ import { opensForkPull, reloadsUntil, resubmitsForkPatch, - runsNothing, + waitsForApproval, targetTookTheFeature, upstreamPull } from "../harness/shared"; @@ -108,27 +108,21 @@ forkedAhead("resubmitting a fork pull request after pushing to the fork's branch .sees(not(commitRow(3))); // spindle has no button for this yet, so it goes through the api -const runsByHand = async (ctx: Ctx) => { - const reply = await spindlePost(ctx.actor, "org.tangled.ci.triggerPipeline", { - repo: ctx.upstream.did, - trigger: { - $type: "org.tangled.event.pullRequest", - action: "opened", - sourceRepo: ctx.repo.did, - sourceBranch: branches.from, - targetBranch: branches.into, - sourceSha: forkHead(ctx), - pull: ctx.get(pullId).uri - } - }); - expect(reply.status, `spindle answered ${JSON.stringify(reply.body)}`).toBe(200); +const approvesByHand = async (ctx: Ctx) => { + for (const p of await waitsForApproval(forkHead, upstreamDid)(ctx)) { + const reply = await spindlePost(ctx.actor, "org.tangled.ci.approvePipeline", { + repo: ctx.upstream.did, + pipeline: p.id + }); + expect(reply.status, `spindle answered ${JSON.stringify(reply.body)}`).toBe(200); + } }; -forkedAhead("a fork pull request from someone who isn't a collaborator runs no pipeline", { +forkedAhead("a fork pull request from someone who isn't a collaborator waits for approval", { ci: true }) .use(opensForkPull(forkTitle)) - .step("no pipeline for the opened pull", runsNothing(forkHead, upstreamDid)) + .step("the opened pull's pipeline is blocked", waitsForApproval(forkHead, upstreamDid)) .push({ "e2e/more.txt": "more\n" }, { branch: branches.from }) .mirrored() .open(upstreamPull) @@ -137,14 +131,14 @@ forkedAhead("a fork pull request from someone who isn't a collaborator runs no p .changeState(pullId, "closed", testid("requestClose"), testid("changeState")) .changeState(pullId, "open", testid("changeState"), testid("requestClose")) .step( - "no pipeline after the resubmit, the close and the reopen", - runsNothing(forkHead, upstreamDid) + "still blocked after the resubmit, the close and the reopen", + waitsForApproval(forkHead, upstreamDid) ) .actor("owner") - .step("the owner runs the pull's pipeline by hand", runsByHand) - .step("spindle has a pull request pipeline at the fork's head", (ctx) => - spindle.hasPipeline(ctx.upstream.did, forkHead(ctx), "pullRequest") + .step("the owner approves the pull's pipelines", approvesByHand) + .step("spindle runs a workflow at the fork's head", (ctx) => + spindle.finishedWorkflow(ctx.upstream.did, forkHead(ctx)) ); // the hook belongs to the upstream, but the flow's repo is the fork diff --git a/e2e/specs/pull-pipelines.spec.ts b/e2e/specs/pull-pipelines.spec.ts index 8e67aab37..7105797ca 100644 --- a/e2e/specs/pull-pipelines.spec.ts +++ b/e2e/specs/pull-pipelines.spec.ts @@ -21,7 +21,7 @@ import { onBranches, pipelineFor, pullPipelines, - runsNothing, + waitsForApproval, workflow, workflowPath } from "../harness/shared"; @@ -78,14 +78,14 @@ withDone("closing a pull request whose branch is gone still runs at the old head .changeState(pullId, "closed", testid("requestClose"), testid("changeState")) .step("spindle runs the close workflow at the old head", runs("done.yml", head)); -onBranches("a pull from someone who isn't a collaborator runs nothing", { ci: true }) +onBranches("a pull from someone who isn't a collaborator waits for approval", { ci: true }) .actor("author") .pull(pullId, patchId, branches) - .step("no pipeline for the opened pull", runsNothing(head)) + .step("the opened pull's pipeline is blocked", waitsForApproval(head)) .open(page.pull(pullId)) .changeState(pullId, "closed", testid("requestClose"), testid("changeState")) .changeState(pullId, "open", testid("changeState"), testid("requestClose")) - .step("no pipeline after the close and the reopen", runsNothing(head)) + .step("still blocked after the close and the reopen", waitsForApproval(head)) .actor("owner") .push({ "e2e/more.txt": "more\n" }, { branch: branches.from }) .mirrored() @@ -94,4 +94,4 @@ onBranches("a pull from someone who isn't a collaborator runs nothing", { ci: tr .click(testid("resubmit")) .sends(patchId, knot, (ctx) => ({ patch: { ticket: rkeyOf(ctx, pullId) } }), "create") .trace(patchId, through(knot, hydrant, bobbin)) - .step("no pipeline for the resubmitted head", runsNothing(head)); + .step("the resubmitted head is blocked too", waitsForApproval(head)); diff --git a/e2e/specs/refusals.spec.ts b/e2e/specs/refusals.spec.ts index 551875587..6a568e1f2 100644 --- a/e2e/specs/refusals.spec.ts +++ b/e2e/specs/refusals.spec.ts @@ -106,14 +106,14 @@ flow( .rejects( "running a workflow the repo doesn't have", (ctx) => - spindlePost(ctx.actor, `${CI}.triggerPipeline`, { + spindlePost(ctx.actor, "org.tangled.event.notifyEvent", { repo: ctx.repo.did, - trigger: { + event: { $type: `${CI}.trigger.manual`, repo: ctx.repo.did, - commit: ctx.pushed.sha - }, - workflows: ["nope.yml"] + commit: ctx.pushed.sha, + workflows: ["nope.yml"] + } }), 400 ); -- 2.54.0