Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/gittensory-miner/bin/gittensory-miner.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { createRequire } from "node:module";
import { printHelp, printVersion, runCli } from "../lib/cli.js";
import { runDenyCheck } from "../lib/deny-check.js";
import { runManagePoll } from "../lib/manage-poll.js";
import { runManageStatus } from "../lib/manage-status.js";
import { runQueueCli } from "../lib/portfolio-queue-cli.js";
import { runStateCli } from "../lib/run-state-cli.js";
Expand Down Expand Up @@ -78,6 +79,12 @@ if (cliArgs[0] === "state") {
process.exit(exitCode);
}

if (cliArgs[0] === "manage" && cliArgs[1] === "poll") {
const exitCode = await runManagePoll(cliArgs.slice(2));
await awaitOpportunisticUpdateCheck(updateCheck);
process.exit(exitCode);
}

const exitCode = runCli(cliArgs, { packageName });
await awaitOpportunisticUpdateCheck(updateCheck);
process.exit(exitCode);
1 change: 1 addition & 0 deletions packages/gittensory-miner/lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export function printHelp(input) {
" gittensory-miner status [--json] Show installed versions + local state paths",
" gittensory-miner doctor [--json] Check this laptop is set up correctly",
" gittensory-miner manage status [--json] Show managed PR rows from local portfolio + ledger",
" gittensory-miner manage poll <owner/repo> <pr#> [--branch <name>] [--json]",
" gittensory-miner queue list [--repo <owner/repo>] [--json] List portfolio backlog rows",
" gittensory-miner queue next [--json] Claim the highest-priority queued item",
" gittensory-miner queue done <owner/repo> <identifier> [--json]",
Expand Down
78 changes: 78 additions & 0 deletions packages/gittensory-miner/lib/manage-poll.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { PollCheckRunsOptions, PollCheckRunsResult } from "./ci-poller.js";
import type { EventLedger, LedgerEntry } from "./event-ledger.js";
import type { PortfolioQueueStore } from "./portfolio-queue.js";

export type ManagePollInput = {
repoFullName: string;
prNumber: number;
branch?: string | null;
};

export type ManagePollEventPayload = {
prNumber: number;
branch: string | null;
ciState: PollCheckRunsResult["conclusion"];
gateVerdict: string;
outcome: string;
lastPolledAt: string;
};

export type ManagePollRecordResult = {
pollResult: PollCheckRunsResult;
payload: ManagePollEventPayload;
event: LedgerEntry;
};

export type ParsedManagePollArgs =
| {
repoFullName: string;
prNumber: number;
branch: string | null;
json: boolean;
}
| { error: string };

export function mapPollConclusionToGateVerdict(
conclusion: PollCheckRunsResult["conclusion"],
): string;

export function mapPollConclusionToOutcome(conclusion: PollCheckRunsResult["conclusion"]): string;

export function buildManagePollEventPayload(
prNumber: number,
pollResult: PollCheckRunsResult,
options?: { branch?: string | null; lastPolledAt?: string },
): ManagePollEventPayload;

export function parseManagePollArgs(args?: string[]): ParsedManagePollArgs;

export function recordManagePollSnapshot(
input: ManagePollInput,
options: {
eventLedger: EventLedger;
portfolioQueue?: PortfolioQueueStore;
ensurePortfolioRow?: boolean;
pollCheckRuns?: (
repoFullName: string,
prNumber: number,
options?: PollCheckRunsOptions,
) => Promise<PollCheckRunsResult>;
lastPolledAt?: string;
} & PollCheckRunsOptions,
): Promise<ManagePollRecordResult>;

export function runManagePoll(
args?: string[],
options?: {
initEventLedger?: () => EventLedger;
initPortfolioQueue?: () => PortfolioQueueStore;
ensurePortfolioRow?: boolean;
pollCheckRuns?: (
repoFullName: string,
prNumber: number,
options?: PollCheckRunsOptions,
) => Promise<PollCheckRunsResult>;
githubToken?: string;
lastPolledAt?: string;
} & PollCheckRunsOptions,
): Promise<number>;
210 changes: 210 additions & 0 deletions packages/gittensory-miner/lib/manage-poll.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { pollCheckRuns } from "./ci-poller.js";
import { initEventLedger } from "./event-ledger.js";
import {
MANAGE_PR_UPDATE_EVENT,
formatManagedPrIdentifier,
} from "./manage-status.js";
import { initPortfolioQueueStore } from "./portfolio-queue.js";

const MANAGE_POLL_USAGE =
"Usage: gittensory-miner manage poll <owner/repo> <pr#> [--branch <name>] [--json]";

function parseRepoArg(value, usage) {
if (!value) return { error: usage };
const trimmed = value.trim();
const [owner, repo, extra] = trimmed.split("/");
if (!owner || !repo || extra !== undefined) {
return { error: "Repository must be in owner/repo form." };
}
return { repoFullName: `${owner}/${repo}` };
}

export function mapPollConclusionToGateVerdict(conclusion) {
switch (conclusion) {
case "success":
return "pass";
case "failure":
return "block";
default:
return "advisory";
}
}

export function mapPollConclusionToOutcome(conclusion) {
switch (conclusion) {
case "success":
return "ready";
case "failure":
return "needs-work";
default:
return "open";
}
}

export function buildManagePollEventPayload(prNumber, pollResult, options = {}) {
if (!Number.isInteger(prNumber) || prNumber <= 0) throw new Error("invalid_pr_number");
if (!pollResult || typeof pollResult !== "object") throw new Error("invalid_poll_result");
const branch = typeof options.branch === "string" && options.branch.trim() ? options.branch.trim() : null;
const lastPolledAt =
typeof options.lastPolledAt === "string" && options.lastPolledAt.trim()
? options.lastPolledAt.trim()
: new Date().toISOString();
return {
prNumber,
branch,
ciState: pollResult.conclusion,
gateVerdict: mapPollConclusionToGateVerdict(pollResult.conclusion),
outcome: mapPollConclusionToOutcome(pollResult.conclusion),
lastPolledAt,
};
}

export function parseManagePollArgs(args = []) {
const options = { json: false, branch: null };
const positional = [];

for (let index = 0; index < args.length; index += 1) {
const token = args[index];
if (token === "--json") {
options.json = true;
continue;
}
if (token === "--branch") {
const branch = args[index + 1];
if (!branch || branch.startsWith("-")) return { error: MANAGE_POLL_USAGE };
options.branch = branch;
index += 1;
continue;
}
if (token.startsWith("-")) return { error: `Unknown option: ${token}` };
positional.push(token);
}

if (positional.length !== 2) return { error: MANAGE_POLL_USAGE };

const repo = parseRepoArg(positional[0], MANAGE_POLL_USAGE);
if ("error" in repo) return repo;

const prNumber = Number(positional[1]);
if (!Number.isInteger(prNumber) || prNumber <= 0) {
return { error: "Pull request number must be a positive integer." };
}

return {
repoFullName: repo.repoFullName,
prNumber,
...options,
};
}

function ensureManagedPrRow(portfolioQueue, repoFullName, prNumber) {
const identifier = formatManagedPrIdentifier(prNumber);
const exists = portfolioQueue
.listQueue(repoFullName)
.some((entry) => entry.identifier === identifier);
if (!exists) {
portfolioQueue.enqueue({ repoFullName, identifier, priority: 0 });
}
}

/**
* Poll GitHub check runs for a managed PR and append a `manage_pr_update` snapshot to the local event ledger.
* Completes the manage-status data path introduced in #2325 / #3070 using the CI poller from #2323.
*/
export async function recordManagePollSnapshot(input, options = {}) {
if (!input || typeof input !== "object") throw new Error("invalid_manage_poll_input");
const repoFullName = typeof input.repoFullName === "string" ? input.repoFullName.trim() : "";
const [owner, repo, extra] = repoFullName.split("/");
if (!owner || !repo || extra !== undefined) throw new Error("invalid_repo_full_name");
if (!Number.isInteger(input.prNumber) || input.prNumber <= 0) throw new Error("invalid_pr_number");

const eventLedger = options.eventLedger;
if (!eventLedger || typeof eventLedger.appendEvent !== "function") {
throw new Error("invalid_event_ledger");
}

const portfolioQueue = options.portfolioQueue;
if (options.portfolioQueue !== undefined) {
if (!portfolioQueue || typeof portfolioQueue.enqueue !== "function") {
throw new Error("invalid_portfolio_queue");
}
}

const pollCheckRunsFn = options.pollCheckRuns ?? pollCheckRuns;
const pollResult = await pollCheckRunsFn(repoFullName, input.prNumber, {
apiBaseUrl: options.apiBaseUrl,
fetchFn: options.fetchFn,
githubToken: options.githubToken ?? "",
maxAttempts: options.maxAttempts,
minIntervalMs: options.minIntervalMs,
maxIntervalMs: options.maxIntervalMs,
sleepFn: options.sleepFn,
});

const payload = buildManagePollEventPayload(input.prNumber, pollResult, {
branch: input.branch,
lastPolledAt: options.lastPolledAt,
});

if ((options.ensurePortfolioRow ?? true) && portfolioQueue) {
ensureManagedPrRow(portfolioQueue, repoFullName, input.prNumber);
}

const event = eventLedger.appendEvent({
type: MANAGE_PR_UPDATE_EVENT,
repoFullName,
payload,
});

return { pollResult, payload, event };
}

export async function runManagePoll(args = [], options = {}) {
const parsed = parseManagePollArgs(args);
if ("error" in parsed) {
console.error(parsed.error);
return 2;
}

const ownsEventLedger = options.initEventLedger === undefined;
const ownsPortfolioQueue = options.initPortfolioQueue === undefined;
const eventLedger = (options.initEventLedger ?? initEventLedger)();
const portfolioQueue = (options.initPortfolioQueue ?? initPortfolioQueueStore)();

try {
const result = await recordManagePollSnapshot(
{
repoFullName: parsed.repoFullName,
prNumber: parsed.prNumber,
branch: parsed.branch,
},
{
eventLedger,
portfolioQueue,
ensurePortfolioRow: options.ensurePortfolioRow ?? true,
pollCheckRuns: options.pollCheckRuns,
fetchFn: options.fetchFn,
githubToken: options.githubToken ?? process.env.GITHUB_TOKEN ?? "",
apiBaseUrl: options.apiBaseUrl,
maxAttempts: options.maxAttempts,
minIntervalMs: options.minIntervalMs,
maxIntervalMs: options.maxIntervalMs,
sleepFn: options.sleepFn,
lastPolledAt: options.lastPolledAt,
},
);

if (parsed.json) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(`${result.payload.ciState} (${result.payload.gateVerdict}/${result.payload.outcome})`);
}
return 0;
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
return 2;
} finally {
if (ownsEventLedger) eventLedger.close();
if (ownsPortfolioQueue) portfolioQueue.close();
}
}
2 changes: 1 addition & 1 deletion packages/gittensory-miner/lib/manage-status.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { initEventLedger } from "./event-ledger.js";
import { initPortfolioQueueStore } from "./portfolio-queue.js";

/** Event vocabulary for manage-phase PR snapshots written by future CI pollers. (#2325) */
/** Event vocabulary for manage-phase PR snapshots written by manage poll. (#2325) */
export const MANAGE_PR_UPDATE_EVENT = "manage_pr_update";
export const MANAGED_PR_IDENTIFIER_PREFIX = "pr:";

Expand Down
2 changes: 1 addition & 1 deletion packages/gittensory-miner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"lib"
],
"scripts": {
"build": "node --check bin/gittensory-miner.js && node --check lib/cli.js && node --check lib/deny-check.js && node --check lib/run-state-cli.js && node --check lib/update-check.js && node --check lib/opportunity-fanout.js && node --check lib/ci-poller.js && node --check lib/run-state.js && node --check lib/deny-hooks.js && node --check lib/event-ledger.js && node --check lib/claim-ledger.js && node --check lib/claim-ledger-expiry.js && node --check lib/portfolio-queue.js && node --check lib/portfolio-queue-cli.js && node --check lib/portfolio-discovery.js && node --check lib/opportunity-ranker.js && node --check lib/plan-store.js && node --check lib/rejection-templates.js && node --check lib/governor-ledger.js && node --check lib/manage-status.js && node --check lib/status.js"
"build": "node --check bin/gittensory-miner.js && node --check lib/cli.js && node --check lib/deny-check.js && node --check lib/run-state-cli.js && node --check lib/update-check.js && node --check lib/opportunity-fanout.js && node --check lib/ci-poller.js && node --check lib/run-state.js && node --check lib/deny-hooks.js && node --check lib/event-ledger.js && node --check lib/claim-ledger.js && node --check lib/claim-ledger-expiry.js && node --check lib/portfolio-queue.js && node --check lib/portfolio-queue-cli.js && node --check lib/portfolio-discovery.js && node --check lib/opportunity-ranker.js && node --check lib/plan-store.js && node --check lib/rejection-templates.js && node --check lib/governor-ledger.js && node --check lib/manage-status.js && node --check lib/manage-poll.js && node --check lib/status.js"
},
"dependencies": {
"@jsonbored/gittensory-engine": "0.1.0"
Expand Down
Loading
Loading