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
2 changes: 2 additions & 0 deletions packages/gittensory-miner/lib/claim-ledger-cli.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type ParsedClaimClaimArgs =
repoFullName: string;
issueNumber: number;
note: string | undefined;
dryRun: boolean;
json: boolean;
}
| { error: string };
Expand All @@ -13,6 +14,7 @@ export type ParsedClaimReleaseArgs =
| {
repoFullName: string;
issueNumber: number;
dryRun: boolean;
json: boolean;
}
| { error: string };
Expand Down
41 changes: 37 additions & 4 deletions packages/gittensory-miner/lib/claim-ledger-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { CLAIM_STATUSES, openClaimLedger } from "./claim-ledger.js";
import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js";

const CLAIM_CLAIM_USAGE =
"Usage: gittensory-miner claim claim <owner/repo> <issue#> [--note <text>] [--json]";
const CLAIM_RELEASE_USAGE = "Usage: gittensory-miner claim release <owner/repo> <issue#> [--json]";
"Usage: gittensory-miner claim claim <owner/repo> <issue#> [--note <text>] [--dry-run] [--json]";
const CLAIM_RELEASE_USAGE = "Usage: gittensory-miner claim release <owner/repo> <issue#> [--dry-run] [--json]";
const CLAIM_LIST_USAGE =
"Usage: gittensory-miner claim list [--repo <owner/repo>] [--status active|released|expired] [--json]";

Expand All @@ -27,7 +27,7 @@ function parseIssueNumberArg(value, usage) {
}

export function parseClaimClaimArgs(args) {
const options = { json: false, note: undefined };
const options = { json: false, note: undefined, dryRun: false };
const positional = [];

for (let index = 0; index < args.length; index += 1) {
Expand All @@ -36,6 +36,11 @@ export function parseClaimClaimArgs(args) {
options.json = true;
continue;
}
// #4847: reports what a real claim would do and returns before opening the claim ledger at all.
if (token === "--dry-run") {
options.dryRun = true;
continue;
}
if (token === "--note") {
const note = args[index + 1];
if (!note || note.startsWith("-")) {
Expand Down Expand Up @@ -64,19 +69,24 @@ export function parseClaimClaimArgs(args) {
repoFullName: repo.repoFullName,
issueNumber: issue.issueNumber,
note: options.note,
dryRun: options.dryRun,
json: options.json,
};
}

export function parseClaimReleaseArgs(args) {
const options = { json: false };
const options = { json: false, dryRun: false };
const positional = [];

for (const token of args) {
if (token === "--json") {
options.json = true;
continue;
}
if (token === "--dry-run") {
options.dryRun = true;
continue;
}
if (token.startsWith("-")) {
return { error: `Unknown option: ${token}` };
}
Expand All @@ -95,6 +105,7 @@ export function parseClaimReleaseArgs(args) {
return {
repoFullName: repo.repoFullName,
issueNumber: issue.issueNumber,
dryRun: options.dryRun,
json: options.json,
};
}
Expand Down Expand Up @@ -187,6 +198,18 @@ export function runClaimClaim(args, options = {}) {
return reportCliFailure(argsWantJson(args), parsed.error);
}

if (parsed.dryRun) {
const dryRunResult = { outcome: "dry_run", repoFullName: parsed.repoFullName, issueNumber: parsed.issueNumber, note: parsed.note ?? null };
if (parsed.json) {
console.log(JSON.stringify(dryRunResult, null, 2));
} else {
console.log(
`DRY RUN: would claim ${parsed.repoFullName}#${parsed.issueNumber}${parsed.note ? ` (note: ${parsed.note})` : ""}. No claim-ledger write was made.`,
);
}
return 0;
}

try {
return withClaimLedger(options, (claimLedger) => {
const claim = claimLedger.claimIssue(
Expand All @@ -212,6 +235,16 @@ export function runClaimRelease(args, options = {}) {
return reportCliFailure(argsWantJson(args), parsed.error);
}

if (parsed.dryRun) {
const dryRunResult = { outcome: "dry_run", repoFullName: parsed.repoFullName, issueNumber: parsed.issueNumber };
if (parsed.json) {
console.log(JSON.stringify(dryRunResult, null, 2));
} else {
console.log(`DRY RUN: would release the claim on ${parsed.repoFullName}#${parsed.issueNumber}. No claim-ledger write was made.`);
}
return 0;
}

try {
return withClaimLedger(options, (claimLedger) => {
const claim = claimLedger.releaseClaim(parsed.repoFullName, parsed.issueNumber);
Expand Down
24 changes: 12 additions & 12 deletions packages/gittensory-miner/lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,21 @@ export function printHelp(input) {
" gittensory-miner migrate [--json] Apply pending schema migrations to existing local stores",
" gittensory-miner metrics Print prediction-calibration counters in Prometheus text format",
" 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 discover <owner/repo> [<owner/repo>...] [--json]",
" gittensory-miner discover --search <query> [--json] Fan out, rank, and enqueue candidates",
" gittensory-miner manage poll <owner/repo> <pr#> [--branch <name>] [--dry-run] [--json]",
" gittensory-miner discover <owner/repo> [<owner/repo>...] [--dry-run] [--json]",
" gittensory-miner discover --search <query> [--dry-run] [--json] Fan out, rank, and enqueue candidates",
" gittensory-miner attempt <owner/repo> <issue#> --miner-login <login> [--base <branch>] [--live] [--dry-run] [--json]",
" gittensory-miner loop <owner/repo> [<owner/repo>...] --miner-login <login> [--base <branch>] [--live] [--dry-run]",
" gittensory-miner loop --search <query> --miner-login <login> [--max-cycles <n>] [--cycle-delay-ms <ms>] [--dry-run] [--json]",
" Autonomous discover->claim->attempt->reenter loop",
" 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 claim-batch [--global-wip <n>] [--per-repo-wip <n>] [--json]",
" gittensory-miner queue done <owner/repo> <identifier> [--json]",
" gittensory-miner queue release <owner/repo> <identifier> [--json] Return a claimed item to the queue",
" gittensory-miner queue requeue <owner/repo> <identifier> [--json] Put a completed item back on the queue",
" gittensory-miner claim claim <owner/repo> <issue#> [--note <text>] [--json]",
" gittensory-miner claim release <owner/repo> <issue#> [--json]",
" gittensory-miner queue next [--dry-run] [--json] Claim the highest-priority queued item",
" gittensory-miner queue claim-batch [--global-wip <n>] [--per-repo-wip <n>] [--dry-run] [--json]",
" gittensory-miner queue done <owner/repo> <identifier> [--dry-run] [--json]",
" gittensory-miner queue release <owner/repo> <identifier> [--dry-run] [--json] Return a claimed item to the queue",
" gittensory-miner queue requeue <owner/repo> <identifier> [--dry-run] [--json] Put a completed item back on the queue",
" gittensory-miner claim claim <owner/repo> <issue#> [--note <text>] [--dry-run] [--json]",
" gittensory-miner claim release <owner/repo> <issue#> [--dry-run] [--json]",
" gittensory-miner claim list [--repo <owner/repo>] [--status active|released|expired] [--json]",
" gittensory-miner ledger list [--repo <owner/repo>] [--since <seq>] [--type <eventType>] [--json]",
" gittensory-miner ledger metrics Print event-ledger counters in Prometheus text format",
Expand All @@ -47,8 +47,8 @@ export function printHelp(input) {
" gittensory-miner feasibility <claimStatus> <duplicateClusterRisk> <issueStatus> [--not-found] [--json]",
" gittensory-miner hooks check --tool <name> --input <json> [--json]",
" gittensory-miner state get <owner/repo> [--json]",
" gittensory-miner state set <owner/repo> <idle|discovering|planning|preparing> [--json]",
" gittensory-miner orb export [--enable] [--json] Build the opt-in anonymized telemetry batch",
" gittensory-miner state set <owner/repo> <idle|discovering|planning|preparing> [--dry-run] [--json]",
" gittensory-miner orb export [--enable] [--dry-run] [--json] Build the opt-in anonymized telemetry batch",
"",
"Options:",
" --no-update-check Skip the npm registry version nudge (also GITTENSORY_MINER_NO_UPDATE_CHECK=1)",
Expand Down
1 change: 1 addition & 0 deletions packages/gittensory-miner/lib/discover-cli.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type ParsedDiscoverArgs =
| {
targets: FanoutTarget[];
search: string | null;
dryRun: boolean;
json: boolean;
/** Present only when `--api-base-url` is supplied (#4784); threads the tenant's forge host to the fan-out. */
apiBaseUrl?: string;
Expand Down
52 changes: 50 additions & 2 deletions packages/gittensory-miner/lib/discover-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { initPortfolioQueueStore } from "./portfolio-queue.js";
import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js";

const DISCOVER_USAGE =
"Usage: gittensory-miner discover <owner/repo> [<owner/repo>...] | --search <query> [--json] [--api-base-url <url>] [--token-env <VAR>]";
"Usage: gittensory-miner discover <owner/repo> [<owner/repo>...] | --search <query> [--dry-run] [--json] [--api-base-url <url>] [--token-env <VAR>]";

const MAX_DISCOVER_TITLE_DISPLAY_LENGTH = 240;
const OSC_SEQUENCE_PATTERN = /\u001b\][\s\S]*?(?:\u0007|\u001b\\)/g;
Expand Down Expand Up @@ -43,7 +43,7 @@ export function parseDiscoverArgs(args) {
// `--api-base-url` and `--token-env` (#4784) thread the tenant's forge host and credential env var into the
// fan-out; they are kept off the parsed result unless supplied, so callers that pass neither see the exact
// pre-#4784 `{ targets, search, json }` shape.
const options = { json: false, search: null, apiBaseUrl: null, tokenEnv: null };
const options = { json: false, dryRun: false, search: null, apiBaseUrl: null, tokenEnv: null };
const targets = [];

for (let index = 0; index < args.length; index += 1) {
Expand All @@ -52,6 +52,11 @@ export function parseDiscoverArgs(args) {
options.json = true;
continue;
}
// #4847: fetches + ranks exactly as a real run, but skips opening any local store and makes zero writes.
if (token === "--dry-run") {
options.dryRun = true;
continue;
}
if (token === "--search") {
const query = args[index + 1];
if (!query || query.startsWith("-")) return { error: DISCOVER_USAGE };
Expand Down Expand Up @@ -91,6 +96,7 @@ export function parseDiscoverArgs(args) {
return {
targets,
search: options.search,
dryRun: options.dryRun,
json: options.json,
...(options.apiBaseUrl !== null ? { apiBaseUrl: options.apiBaseUrl } : {}),
...(options.tokenEnv !== null ? { tokenEnv: options.tokenEnv } : {}),
Expand Down Expand Up @@ -156,6 +162,48 @@ export async function runDiscover(args, options = {}) {
const rankIssues = options.rankCandidateIssuesWithSummary ?? rankCandidateIssuesWithSummary;
const enqueue = options.enqueueRankedDiscovery ?? enqueueRankedDiscovery;

// #4847: fetch + rank are read-only GitHub GETs and pure local computation, so a dry run still does them for
// real (that's the useful "what would this discover?" output) -- but it never opens any local store (portfolio
// queue, policy-doc cache, policy-verdict cache), since opening a not-yet-existing SQLite store file is itself
// a write. The ranked issues are fed through a no-op queue stub so enqueueRankedDiscovery's own classification
// logic (valid/invalid, below-min-rank) still runs for real, just without ever touching the real queue.
if (parsed.dryRun) {
const fanOutOptions = { apiBaseUrl, forge: options.forge, policyDocCache: null, policyVerdictCache: null };
try {
const fanOut =
parsed.search !== null
? await searchTargets(parsed.search, githubToken, fanOutOptions)
: await fetchTargets(parsed.targets, githubToken, fanOutOptions);
const rankedSummary = rankIssues(fanOut.issues, {
nowMs: options.nowMs,
goalSpecsByRepo: options.goalSpecsByRepo,
goalSpecContentByRepo: options.goalSpecContentByRepo,
});
const noopQueueStore = { enqueue: () => {} };
const enqueueSummary = enqueue(rankedSummary.issues, { queueStore: noopQueueStore });
const result = {
outcome: "dry_run",
fanOutCount: fanOut.issues.length,
warnings: fanOut.warnings,
rateLimitRemaining: fanOut.rateLimitRemaining,
rateLimitResetAt: fanOut.rateLimitResetAt,
ranked: rankedSummary.issues,
usedDefaultGoalSpec: rankedSummary.usedDefaultGoalSpec,
enqueueSummary,
};
if (parsed.json) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(renderDiscoverSummary(result));
console.log("\nDRY RUN: no portfolio-queue write was made.");
}
return 0;
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
return 2;
}
}

const ownsPortfolioQueue = options.initPortfolioQueue === undefined;
let portfolioQueue;
try {
Expand Down
1 change: 1 addition & 0 deletions packages/gittensory-miner/lib/manage-poll.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export type ParsedManagePollArgs =
repoFullName: string;
prNumber: number;
branch: string | null;
dryRun: boolean;
json: boolean;
}
| { error: string };
Expand Down
49 changes: 47 additions & 2 deletions packages/gittensory-miner/lib/manage-poll.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { initPortfolioQueueStore } from "./portfolio-queue.js";
import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js";

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

function parseRepoArg(value, usage) {
if (!value) return { error: usage };
Expand Down Expand Up @@ -61,7 +61,7 @@ export function buildManagePollEventPayload(prNumber, pollResult, options = {})
}

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

for (let index = 0; index < args.length; index += 1) {
Expand All @@ -70,6 +70,12 @@ export function parseManagePollArgs(args = []) {
options.json = true;
continue;
}
// #4847: still runs the real (read-only) CI-check-run poll, but skips the event-ledger append and
// portfolio-queue enqueue.
if (token === "--dry-run") {
options.dryRun = true;
continue;
}
if (token === "--branch") {
const branch = args[index + 1];
if (!branch || branch.startsWith("-")) return { error: MANAGE_POLL_USAGE };
Expand Down Expand Up @@ -166,6 +172,45 @@ export async function runManagePoll(args = [], options = {}) {
return reportCliFailure(argsWantJson(args), parsed.error);
}

// #4847: the CI-check-run poll itself is a real, read-only GitHub signal -- the useful "what would this
// record?" output -- so a dry run still performs it for real. It never opens the event ledger or portfolio
// queue, though: a no-op event ledger is fed through recordManagePollSnapshot so its own real payload-building
// logic still runs, just without ever writing to local storage (ensurePortfolioRow: false skips the queue
// enqueue the same way).
if (parsed.dryRun) {
const noopEventLedger = { appendEvent: () => null };
try {
const result = await recordManagePollSnapshot(
{ repoFullName: parsed.repoFullName, prNumber: parsed.prNumber, branch: parsed.branch },
{
eventLedger: noopEventLedger,
ensurePortfolioRow: false,
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,
},
);
const dryRunResult = { outcome: "dry_run", pollResult: result.pollResult, payload: result.payload };
if (parsed.json) {
console.log(JSON.stringify(dryRunResult, null, 2));
} else {
console.log(
`DRY RUN: ${result.payload.ciState} (${result.payload.gateVerdict}/${result.payload.outcome}). No event-ledger or portfolio-queue write was made.`,
);
}
return 0;
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
return 2;
}
}

const ownsEventLedger = options.initEventLedger === undefined;
const ownsPortfolioQueue = options.initPortfolioQueue === undefined;
const eventLedger = (options.initEventLedger ?? initEventLedger)();
Expand Down
2 changes: 1 addition & 1 deletion packages/gittensory-miner/lib/orb-export.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function collectOrbExportBatch(options?: {
enabled?: boolean;
}): OrbExportRow[] | null;

export type ParsedOrbExportArgs = { json: boolean; enable: boolean } | { error: string };
export type ParsedOrbExportArgs = { json: boolean; enable: boolean; dryRun: boolean } | { error: string };

export function parseOrbExportArgs(args: string[]): ParsedOrbExportArgs;

Expand Down
22 changes: 20 additions & 2 deletions packages/gittensory-miner/lib/orb-export.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,10 @@ export function collectOrbExportBatch({ store, eventLedger, enabled = ORB_EXPORT
return buildAnonymizedOrbBatch(outcomes, store.getOrCreateAnonSecret());
}

const ORB_EXPORT_USAGE = "Usage: gittensory-miner orb export [--enable] [--json]";
const ORB_EXPORT_USAGE = "Usage: gittensory-miner orb export [--enable] [--dry-run] [--json]";

export function parseOrbExportArgs(args) {
const options = { json: false, enable: false };
const options = { json: false, enable: false, dryRun: false };
for (const token of args) {
if (token === "--json") {
options.json = true;
Expand All @@ -147,6 +147,12 @@ export function parseOrbExportArgs(args) {
options.enable = true;
continue;
}
// #4847: openOrbExportStore() itself creates the local SQLite file (a real write) even before any secret is
// generated, so a dry run reports what would happen and returns before opening any store at all.
if (token === "--dry-run") {
options.dryRun = true;
continue;
}
return { error: ORB_EXPORT_USAGE };
}
return options;
Expand All @@ -161,6 +167,18 @@ export function runOrbExportCli(args, options = {}) {
return reportCliFailure(argsWantJson(args), parsed.error);
}

if (parsed.dryRun) {
const dryRunResult = { outcome: "dry_run", enabled: parsed.enable };
if (parsed.json) {
console.log(JSON.stringify(dryRunResult, null, 2));
} else if (parsed.enable) {
console.log("DRY RUN: would build and report an anonymized Orb export batch. No local writes were made.");
} else {
console.log("DRY RUN: orb export is opt-in and disabled — pass --enable to build an anonymized batch. No local writes were made.");
}
return 0;
}

// Open the stores INSIDE the try so a bad config path / SQLite open failure returns 2 instead of crashing the
// process; the finally guards each close with `?.` since either initializer may have thrown before assigning.
const ownsStore = options.openOrbExportStore === undefined;
Expand Down
Loading