mesh/merge-gate pass: builds gitea → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
The recheck's pending was written from the tools' bundle, its own process, past the writer's per-head chain: the first run's verdict mark stayed set, so a rechecked head never said where its new run builds, and an old run's verdict in flight or redelivered could land over the pending. The status tool is now served from the watcher's bundle and writes the merge check's pending through the writer, and a verdict whose ask (checked.id) predates the check now pending is dropped, weighed after a restart against the forge's own history of the gate (novox/hq issue 456).
418 lines
22 KiB
TypeScript
418 lines
22 KiB
TypeScript
// gitea's events. The tool runtime imports this once the broker is bound. It watches the forge and
|
|
// emits what appeared.
|
|
//
|
|
// Emits (novox/hq ADR 0041/0042):
|
|
// module.gitea.repo.created — a repository appeared, however it was made (push, web UI, or tool)
|
|
// module.gitea.pull.merged — a pull request was merged, however it was merged (web UI, API, or tool)
|
|
// module.gitea.pull.updated — an open pull request's head moved, opened or pushed to: the mesh checks it
|
|
// before it merges (novox/hq to-be 45 §9)
|
|
//
|
|
// module.gitea.pull.closed — a pull request closed unmerged: the delivery it was is stopped (novox/hq ADR 0239)
|
|
//
|
|
// Consumes mesh-controller.checked — a pull request's merge check, judged — and sets it as the head
|
|
// commit's statuses: `mesh/merge-gate`, the modules of the mesh's graph the change touches, and
|
|
// `mesh/repo-check`, the repository's own merge-check.sh (novox/hq ADR 0237 as amended); with a comment
|
|
// saying why when the gate is not a pass or the repository's own check failed.
|
|
//
|
|
// Consumes node-build-agent.started — a build machine took an ask — and says, on the head a pending check is of,
|
|
// which machine builds it since when (novox/hq issue 446). This module is the one writer of mesh/merge-gate.
|
|
//
|
|
// Serves gitea_commit_status from this bundle, not the tools', so a recheck's pending reaches that one writer
|
|
// (novox/hq issue 456).
|
|
//
|
|
// Every pull request the forge holds is announced, whatever its repository: the controller holds the
|
|
// module graph and decides what is checked — a repository is never asked to opt in.
|
|
//
|
|
// issue.opened is emitted from its tool (tools/index.ts). repo.created and pull.merged belong here: a
|
|
// repository or a merge is as often made by the web UI or a plain API call, which no tool sees, so
|
|
// polling is the only way to catch every path — and the only emitter, so a fact is never announced
|
|
// twice. The merge tool announced too until novox/hq issue 250, and every merge it made was heard twice.
|
|
//
|
|
// The polling is deliberately unhurried: an event a minute late is still an event, whereas hammering
|
|
// the forge for an immediacy nobody asked for is not.
|
|
|
|
import { emit, on } from "@novox/mesh-sdk/events";
|
|
import { registerModuleTools } from "@novox/mesh-sdk/tools";
|
|
import { commitStatusTool } from "./delivery.js";
|
|
import { GiteaClient, movedSince } from "./client.js";
|
|
import { CHECK_CONTEXT, REPO_CHECK_CONTEXT, commentFor, headsToAnnounce, statusesFor, type Announced, type Checked,
|
|
type RepoCheckFacts, reposToLook } from "./pulls.js";
|
|
import { GateWriter, clonedAs, type BuildStarted } from "./gate.js";
|
|
|
|
// Without a way to a token — configured, or mintable with the admin account (token.ts) — there is
|
|
// nothing to watch; log and stay quiet rather than crash the runtime. With one, the first poll mints
|
|
// or reuses the token, so the runtime's start also shows what it did about it.
|
|
let gitea: GiteaClient | null = null;
|
|
try {
|
|
gitea = GiteaClient.fromEnv();
|
|
} catch (err) {
|
|
console.log(`[gitea] not watching — ${err instanceof Error ? err.message : String(err)}`);
|
|
}
|
|
|
|
// New repositories, by diffing the repo list. Primed silently on the first look, or a restart would
|
|
// re-announce every existing repository as freshly created.
|
|
const seen = new Set<string>();
|
|
let primed = false;
|
|
async function pollRepos(client: GiteaClient): Promise<void> {
|
|
const repos = await client.listAllRepos();
|
|
for (const repo of repos) {
|
|
if (!seen.has(repo.full_name)) {
|
|
if (primed) {
|
|
await emit("repo.created", {
|
|
full_name: repo.full_name,
|
|
owner: repo.owner,
|
|
name: repo.name,
|
|
private: repo.private,
|
|
html_url: repo.html_url,
|
|
});
|
|
}
|
|
seen.add(repo.full_name);
|
|
}
|
|
}
|
|
primed = true;
|
|
}
|
|
|
|
// **A merge is announced whoever made it.** The merge tool below emits at the instant it acts; a
|
|
// merge made in the forge's own pages or over its API would emit nothing, and the mesh would go on
|
|
// believing every module current with its source (novox/hq 04-ISSUES/131). So merged pull requests
|
|
// are watched the way repositories are: what the forge holds, asked for on a tick, announced once.
|
|
// What has been announced is kept beside the module's state, so a restart does not announce the
|
|
// whole history again — and the first tick on a machine with no record announces nothing, because
|
|
// everything it sees then predates the watching.
|
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
const mergedRecord = process.env.MESH_GITEA_STATE_DIR ? join(process.env.MESH_GITEA_STATE_DIR, "merged-announced.json") : null;
|
|
const announced = new Set<string>();
|
|
let primedMerges = false;
|
|
// since is the moment the watching began: a merge made before it is history, whatever page of the
|
|
// forge's listing it surfaces on. Without it, an old merge past the first page — pushed into view
|
|
// as newer pull requests were updated — was announced as if it had just happened, and the mesh
|
|
// rebuilt everything built from that repository, once per old merge (2026-09-28).
|
|
let since = "";
|
|
if (mergedRecord && existsSync(mergedRecord)) {
|
|
try {
|
|
const kept = JSON.parse(readFileSync(mergedRecord, "utf8")) as string[] | { announced: string[]; since: string };
|
|
const list = Array.isArray(kept) ? kept : kept.announced;
|
|
for (const sha of list) announced.add(sha);
|
|
since = Array.isArray(kept) ? new Date().toISOString() : kept.since;
|
|
primedMerges = true;
|
|
} catch {
|
|
// An unreadable record is treated as no record: prime again rather than re-announce history.
|
|
}
|
|
}
|
|
function keepAnnounced(): void {
|
|
if (!mergedRecord) return;
|
|
mkdirSync(join(mergedRecord, ".."), { recursive: true });
|
|
const tmp = mergedRecord + ".tmp";
|
|
writeFileSync(tmp, JSON.stringify({ announced: [...announced].slice(-2000), since }));
|
|
renameSync(tmp, mergedRecord);
|
|
}
|
|
// **Only the repositories that moved** (novox/hq issue 250). A merge is a push, and a push moves the
|
|
// repository's update time; asking every repository for its pull requests on every tick took longer than the
|
|
// tick itself, so ticks piled up and a merge was announced minutes late. A repository unchanged since a
|
|
// minute before the last look is skipped — the minute absorbs the forge's clock against this one.
|
|
let lastLook = "";
|
|
const MARGIN_MS = 60_000;
|
|
async function pollMerged(client: GiteaClient): Promise<void> {
|
|
const began = new Date().toISOString();
|
|
const floor = lastLook && primedMerges ? new Date(Date.parse(lastLook) - MARGIN_MS).toISOString() : "";
|
|
const repos = movedSince(await client.listAllRepos(), floor);
|
|
let changed = false;
|
|
for (const repo of repos) {
|
|
const pulls = await client.listPullRequests(repo.owner, repo.name, { state: "closed", sort: "recentupdate", limit: "20" });
|
|
for (const pull of pulls) {
|
|
// Closed unmerged (novox/hq ADR 0239): announced once, since the watching began, so its delivery stops.
|
|
const closedKey = `closed:${repo.full_name}#${pull.number}`;
|
|
if (!pull.merged && pull.state === "closed" && !announced.has(closedKey)) {
|
|
if (primedMerges && !!pull.updated_at && !!since && pull.updated_at > since) {
|
|
await emit("pull.closed", { owner: repo.owner, repo: repo.name, number: pull.number, title: pull.title,
|
|
head: pull.head, head_sha: pull.head_sha, base: pull.base, html_url: pull.html_url });
|
|
console.log(`[gitea] announced ${repo.full_name}#${pull.number} closed unmerged`);
|
|
}
|
|
announced.add(closedKey);
|
|
changed = true;
|
|
continue;
|
|
}
|
|
if (!pull.merged || !pull.merge_commit_sha || announced.has(pull.merge_commit_sha)) continue;
|
|
// Announced only if merged since the watching began; recorded either way, so it is looked
|
|
// at once.
|
|
const fresh = !!pull.merged_at && !!since && pull.merged_at > since;
|
|
if (primedMerges && fresh) {
|
|
// What it changed, asked for only now: a module is rebuilt because a file inside its own
|
|
// directory moved, and without this every module built from a repository is rebuilt for a
|
|
// change to any of them (novox/hq 04-ISSUES/131).
|
|
const changed = await client.listPullFiles(repo.owner, repo.name, pull.number);
|
|
// Which of the directories they are in hold a module at the merge commit (novox/hq issue 278): a
|
|
// change inside one is that module's, held or not, and only a file in none is shared code. Not
|
|
// said when the list is cut or the forge could not be asked; the mesh then keeps its old rule.
|
|
let moduleDirs: string[] | null = null;
|
|
if (!changed.truncated) {
|
|
try {
|
|
moduleDirs = await client.moduleDirsAt(repo.owner, repo.name, pull.merge_commit_sha, changed.paths);
|
|
} catch (err) {
|
|
console.error(`[gitea] ${repo.full_name}#${pull.number}: which directories hold a module could not be read, ` +
|
|
`so the mesh reads its files by the old rule — ${err instanceof Error ? err.message : String(err)}`);
|
|
}
|
|
}
|
|
// The head it merged, and what its head was checked as (novox/hq ADR 0239): the delivery it was, made
|
|
// from the forge's word when its owner never heard the head.
|
|
let headChecks: Record<string, string> | null = null;
|
|
if (pull.head_sha) {
|
|
try {
|
|
headChecks = await client.commitStatuses(repo.owner, repo.name, pull.head_sha);
|
|
} catch (err) {
|
|
console.error(`[gitea] ${repo.full_name}#${pull.number}: its head's statuses could not be read — ${err instanceof Error ? err.message : err}`);
|
|
}
|
|
}
|
|
await emit("pull.merged", {
|
|
owner: repo.owner,
|
|
repo: repo.name,
|
|
number: pull.number,
|
|
title: pull.title,
|
|
body: pull.body,
|
|
head_sha: pull.head_sha,
|
|
...(headChecks ? { head_checks: headChecks } : {}),
|
|
head: pull.head,
|
|
base: pull.base,
|
|
merge_commit_sha: pull.merge_commit_sha,
|
|
merged_at: pull.merged_at,
|
|
clone_url: repo.clone_url,
|
|
html_url: pull.html_url,
|
|
paths: changed.paths,
|
|
paths_truncated: changed.truncated,
|
|
// Which of them the merge deleted (novox/hq ADR 0236): a module whose manifest went is forgotten,
|
|
// not built.
|
|
removed: changed.removed,
|
|
...(moduleDirs ? { module_dirs: moduleDirs, module_dirs_said: true } : {}),
|
|
});
|
|
// Said, because a trigger that fires silently is indistinguishable from one that did not
|
|
// fire (novox/hq 04-ISSUES/131) — this line is how an operator knows the mesh was told.
|
|
console.log(`[gitea] announced merge ${repo.full_name}#${pull.number} (${pull.merge_commit_sha.slice(0, 8)}) into ${pull.base}`);
|
|
}
|
|
announced.add(pull.merge_commit_sha);
|
|
changed = true;
|
|
}
|
|
}
|
|
if (!primedMerges) since = new Date().toISOString();
|
|
if (!primedMerges || changed) keepAnnounced();
|
|
primedMerges = true;
|
|
lastLook = began;
|
|
}
|
|
|
|
// **Every new head of an open pull request is announced, once** (novox/hq to-be 45 §9): the mesh checks it
|
|
// against every machine of its facts before it merges. Kept beside the merges' record, so a restart
|
|
// announces nothing twice; the first look on a machine with no record announces only what moved in the
|
|
// last day, so a forge's whole backlog is not checked at once.
|
|
const pullsRecord = process.env.MESH_GITEA_STATE_DIR ? join(process.env.MESH_GITEA_STATE_DIR, "pulls-announced.json") : null;
|
|
let heads: Announced = {};
|
|
let primedPulls = false;
|
|
if (pullsRecord && existsSync(pullsRecord)) {
|
|
try {
|
|
heads = (JSON.parse(readFileSync(pullsRecord, "utf8")) as { heads: Announced }).heads ?? {};
|
|
primedPulls = true;
|
|
} catch {
|
|
// An unreadable record is no record: the first look announces only the last day's.
|
|
}
|
|
}
|
|
function keepHeads(): void {
|
|
if (!pullsRecord) return;
|
|
mkdirSync(join(pullsRecord, ".."), { recursive: true });
|
|
const tmp = pullsRecord + ".tmp";
|
|
writeFileSync(tmp, JSON.stringify({ heads }));
|
|
renameSync(tmp, pullsRecord);
|
|
}
|
|
let lastPullLook = "";
|
|
// **A sweep now and then looks at every repository** (novox/hq issue 347), whatever moved: the floor is a
|
|
// heuristic, and a heuristic that misses is caught by the next sweep rather than never.
|
|
const SWEEP_MS = 10 * 60_000;
|
|
let lastSweep = 0;
|
|
async function pollPulls(client: GiteaClient): Promise<void> {
|
|
const began = new Date().toISOString();
|
|
const floor = lastPullLook ? new Date(Date.parse(lastPullLook) - MARGIN_MS).toISOString() : "";
|
|
const dayAgo = new Date(Date.now() - 24 * 3600_000).toISOString();
|
|
let changed = false;
|
|
// A pull request opened, reopened or retargeted moves its own update time and not its repository's: the
|
|
// pull requests updated since the floor are found by their own (issue 347). A search that fails is a sweep.
|
|
let sweep = Date.now() - lastSweep >= SWEEP_MS;
|
|
let updated: { owner: string; name: string; updated_at?: string }[] = [];
|
|
if (floor && !sweep) {
|
|
try {
|
|
updated = await client.searchOpenPulls(floor);
|
|
} catch (err) {
|
|
console.error(`[gitea] the pull requests updated lately could not be searched, so every repository is looked at — ${err instanceof Error ? err.message : err}`);
|
|
sweep = true;
|
|
}
|
|
}
|
|
if (sweep) lastSweep = Date.now();
|
|
for (const repo of reposToLook(await client.listAllRepos(), floor, updated, sweep)) {
|
|
const open = await client.listPullRequests(repo.owner, repo.name, { state: "open", sort: "recentupdate", limit: "20" });
|
|
for (const pull of headsToAnnounce(repo.full_name, open, heads)) {
|
|
const key = `${repo.full_name}#${pull.number}`;
|
|
const fresh = primedPulls || (!!pull.updated_at && pull.updated_at > dayAgo);
|
|
if (fresh) {
|
|
const files = await client.listPullFiles(repo.owner, repo.name, pull.number);
|
|
const head = String(pull.head_sha);
|
|
// What the controller maps the change onto the mesh's module graph with (novox/hq ADR 0237 as
|
|
// amended): which changed directories hold a module at the head — the merge's rule (issue 278) —
|
|
// and whether the head holds the repository's own merge-check.sh. Not said when it could not be
|
|
// read; the controller then reads the change as touching everything built from the repository.
|
|
let moduleDirs: string[] | null = null;
|
|
let mergeCheck: boolean | null = null;
|
|
try {
|
|
if (!files.truncated) moduleDirs = await client.moduleDirsAt(repo.owner, repo.name, head, files.paths);
|
|
mergeCheck = await client.holdsFile(repo.owner, repo.name, head, "merge-check.sh");
|
|
} catch (err) {
|
|
console.error(`[gitea] ${key}: what the head holds could not be read — ${err instanceof Error ? err.message : err}`);
|
|
}
|
|
await emit("pull.updated", {
|
|
owner: repo.owner,
|
|
repo: repo.name,
|
|
number: pull.number,
|
|
title: pull.title,
|
|
body: pull.body,
|
|
base: pull.base,
|
|
head: pull.head,
|
|
head_sha: pull.head_sha,
|
|
clone_url: repo.clone_url,
|
|
html_url: pull.html_url,
|
|
paths: files.paths,
|
|
paths_truncated: files.truncated,
|
|
removed: files.removed,
|
|
...(moduleDirs ? { module_dirs: moduleDirs, module_dirs_said: true } : {}),
|
|
...(mergeCheck !== null ? { merge_check: mergeCheck, merge_check_said: true } : {}),
|
|
});
|
|
// Said, so an operator knows the mesh was asked to check it.
|
|
console.log(`[gitea] announced ${key} at ${String(pull.head_sha).slice(0, 8)} to be checked before it merges`);
|
|
// Pending until the verdict comes, so the pull request says a check is running rather than nothing — and
|
|
// where it is: in the build queue from now, until a build machine takes it (novox/hq issue 446).
|
|
await gates.pending(repo.owner, repo.name, String(pull.head_sha));
|
|
}
|
|
heads[key] = String(pull.head_sha);
|
|
changed = true;
|
|
}
|
|
}
|
|
if (!primedPulls || changed) keepHeads();
|
|
primedPulls = true;
|
|
lastPullLook = began;
|
|
}
|
|
|
|
// **The verdict, set where the pull request shows it.** Every verdict is the head commit's status; one
|
|
// that is not a pass also leaves the check's own account as a comment, so the reason is read where the
|
|
// change is reviewed. An error — the check could not run — is the forge's `error`, never a success.
|
|
async function setVerdict(client: GiteaClient, event: { body: unknown }): Promise<void> {
|
|
const c = (event.body ?? {}) as Checked;
|
|
if (c.group) {
|
|
// A delivery group's composed check (novox/hq ADR 0239): its verdict is the group owner's to say, on each
|
|
// member's head, as mesh/delivery-group — never this head's merge gate.
|
|
return;
|
|
}
|
|
if (!c.owner || !c.repo || !c.commit || !c.verdict) {
|
|
console.error("[gitea] a merge check's verdict named no repository, commit or verdict; ignored");
|
|
return;
|
|
}
|
|
// Each status links to the pull request, where the delivery's view is kept (novox/hq ADR 0239).
|
|
let target: string | undefined;
|
|
let base: string | undefined;
|
|
if (c.number) {
|
|
const pull = await client.getPullRequest(c.owner, c.repo, c.number).catch(() => undefined);
|
|
target = pull?.html_url || undefined;
|
|
base = pull?.base || undefined;
|
|
}
|
|
const facts = await repoCheckFacts(client, c, base ?? c.plan?.base);
|
|
for (const status of statusesFor(c, facts)) {
|
|
await client.setCommitStatus(c.owner, c.repo, c.commit, target ? { ...status, target_url: target } : status);
|
|
}
|
|
const comment = commentFor(c, facts);
|
|
if (comment && c.number) await client.addComment(c.owner, c.repo, c.number, comment);
|
|
console.log(`[gitea] ${c.owner}/${c.repo}#${c.number ?? "?"} at ${c.commit.slice(0, 8)}: merge check ${c.verdict}`);
|
|
}
|
|
|
|
// **What only the forge knows of a repository check said as a warning** (novox/hq issue 293): whether the
|
|
// head holds a merge-check.sh at all, and — when it does not — whether the base branch's protection
|
|
// requires mesh/repo-check. Asked only for a warning; unknown is left undefined, which statusesFor reads
|
|
// as possibly required: a failure on a status nothing requires blocks nothing, a success on one that is
|
|
// required would let an untested repository merge.
|
|
async function repoCheckFacts(client: GiteaClient, c: Checked, base: string | undefined): Promise<RepoCheckFacts> {
|
|
if (c["repo-check"]?.verdict !== "warning") return {};
|
|
const defined = await client.holdsFile(c.owner, c.repo, c.commit, "merge-check.sh").catch(() => undefined);
|
|
if (defined !== false) return { defined };
|
|
const rules = await client.branchProtections(c.owner, c.repo).catch(() => undefined);
|
|
if (!rules) return { defined };
|
|
const rule = rules.find((p) => (p.rule_name ?? p.branch_name) === (base || "main"));
|
|
const required = !!rule?.enable_status_check && (rule.status_check_contexts ?? []).includes(REPO_CHECK_CONTEXT);
|
|
return { defined, required };
|
|
}
|
|
|
|
// **This module is the one writer of a head's mesh/merge-gate** (novox/hq issue 446): the announcement's pending words,
|
|
// where the check is when a build machine takes it, and the verdict — each head's writes in one chain, the verdict last
|
|
// (gate.ts). A second writer elsewhere raced the verdict and could leave a pass reading pending.
|
|
const gates = new GateWriter({
|
|
setCommitStatus: (owner, repo, sha, status) => (gitea as GiteaClient).setCommitStatus(owner, repo, sha, status),
|
|
gateOf: async (owner, repo, sha) => (await (gitea as GiteaClient).headChecks(owner, repo, sha))[CHECK_CONTEXT],
|
|
gateHistory: (owner, repo, sha) => (gitea as GiteaClient).contextHistory(owner, repo, sha, CHECK_CONTEXT),
|
|
});
|
|
|
|
// **The status tool is served here, beside the writer** (novox/hq issue 456): the runtime starts each bundle as its own
|
|
// process, so a recheck's pending written from the tools' bundle bypassed this writer's chain. Without a forge to
|
|
// reach, nothing is served, as the tools' bundle serves nothing then.
|
|
registerModuleTools("gitea", () => (gitea ? [commitStatusTool({
|
|
setCommitStatus: (owner, repo, sha, status) => (gitea as GiteaClient).setCommitStatus(owner, repo, sha, status),
|
|
gates,
|
|
})] : []));
|
|
|
|
// **A build machine took an ask** (the build seat's `started`, novox/hq ADR 0157): said on the head it checks, when
|
|
// the head is an open pull request's this module announced. Any other build — a branch, a merge's — says nothing.
|
|
async function buildStarted(event: { body: unknown }): Promise<void> {
|
|
const b = (event.body ?? {}) as BuildStarted;
|
|
const ref = String(b.ref ?? "").toLowerCase();
|
|
if (!/^[0-9a-f]{40}$/.test(ref)) return;
|
|
for (const [key, sha] of Object.entries(heads)) {
|
|
if (sha.toLowerCase() !== ref) continue;
|
|
const [owner, rest] = key.split("/");
|
|
const repo = rest?.split("#")[0] ?? "";
|
|
if (owner && repo && clonedAs(b.repository, owner, repo)) {
|
|
await gates.started(owner, repo, sha, b);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (gitea) {
|
|
const client = gitea;
|
|
// A poll that fails says so once, not once a minute: the same reason repeating (the forge not up
|
|
// yet, the admin account refused on a restored forge) is one fact, and a recovery is worth a line.
|
|
let failing: string | null = null;
|
|
const tick = (fn: () => Promise<void>, everyMs: number): void => {
|
|
// **One pass at a time** (novox/hq issue 250): the next pass is scheduled when this one has ended, so a
|
|
// pass that outlasts its interval delays the next instead of running beside it — two passes at once
|
|
// could each announce the same merge before either recorded it.
|
|
const run = (): void =>
|
|
void fn()
|
|
.then(() => {
|
|
if (failing !== null) console.log("[gitea] watching again");
|
|
failing = null;
|
|
})
|
|
.catch((err) => {
|
|
const why = err instanceof Error ? err.message : String(err);
|
|
if (why !== failing) console.error(`[gitea] not watching until this clears — ${why}`);
|
|
failing = why;
|
|
})
|
|
.finally(() => setTimeout(run, everyMs));
|
|
run();
|
|
};
|
|
tick(() => pollRepos(client), 60_000);
|
|
tick(() => pollMerged(client), 30_000);
|
|
tick(() => pollPulls(client), 30_000);
|
|
await on("mesh-controller.checked", (event) => {
|
|
const c = (event.body ?? {}) as Checked;
|
|
// A head's own verdict takes its turn after every write of that head's gate already asked for (gate.ts).
|
|
// A verdict of a check asked before the one now pending is an older run's, and dropped (novox/hq issue 456).
|
|
if (!c.group && c.owner && c.repo && c.commit) {
|
|
return gates.verdict(c.owner, c.repo, c.commit, () => setVerdict(client, event), c.id);
|
|
}
|
|
return setVerdict(client, event);
|
|
});
|
|
await on("node-build-agent.started", buildStarted);
|
|
console.log("[gitea] watching for new repositories and merged pull requests");
|
|
}
|