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
4,900 changes: 4,900 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions src/api/artifacts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import client from "./client";

const listRunArtifacts = async (
repo: string,
runId: number,
): Promise<Response> => {
return client.getTokenRequired(
`/repos/${repo}/actions/runs/${runId}/artifacts`,
);
};

const downloadArtifact = async (repo: string, artifactId: number) => {
return client.getTokenRequired(
`/repos/${repo}/actions/artifacts/${artifactId}/zip`,
);
};

export default {
listRunArtifacts,
downloadArtifact,
};
11 changes: 11 additions & 0 deletions src/api/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import client from "./client";

const listCaches = async (repo: string, key: string): Promise<Response> => {
const query = new URLSearchParams();
query.set("key", key);
query.set("per_page", String(client.getDefaultPerPage()));

return client.getTokenRequired(`/repos/${repo}/actions/caches?${query}`);
};

export default { listCaches };
26 changes: 26 additions & 0 deletions src/api/checks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import client from "./client";
import { GhitgudError } from "@/core/errors";

const toApiPath = (checkRunUrl: string): string => {
const match = checkRunUrl.match(/^https:\/\/api\.github\.com(\/.+)$/);
if (!match?.[1]) {
throw new GhitgudError("Unexpected check run URL format.");
}

return match[1];
};

const getCheckRun = async (checkRunUrl: string): Promise<Response> => {
return client.getTokenRequired(toApiPath(checkRunUrl));
};

const listCheckRunAnnotations = async (
checkRunUrl: string,
): Promise<Response> => {
return client.getTokenRequired(`${toApiPath(checkRunUrl)}/annotations`);
};

export default {
getCheckRun,
listCheckRunAnnotations,
};
22 changes: 22 additions & 0 deletions src/api/workflows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import client from "./client";

const getRun = async (repo: string, runId: number): Promise<Response> => {
return client.getTokenRequired(`/repos/${repo}/actions/runs/${runId}`);
};

const listRunJobs = async (repo: string, runId: number): Promise<Response> => {
return client.getTokenRequired(`/repos/${repo}/actions/runs/${runId}/jobs`);
};

const downloadRunLogs = async (
repo: string,
runId: number,
): Promise<Response> => {
return client.getTokenRequired(`/repos/${repo}/actions/runs/${runId}/logs`);
};

export default {
getRun,
listRunJobs,
downloadRunLogs,
};
9 changes: 9 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ import dates from "@/core/dates";
import output from "@/core/output";
import ghCommand from "@/commands/gh";
import prCommand from "@/commands/pr";
import runCommand from "@/commands/run";
import pingCommand from "@/commands/ping";
import reposCommand from "@/commands/repos";
import cacheCommand from "@/commands/cache";
import labelsCommand from "@/commands/labels";
import outputState from "@/core/output-state";
import configCommand from "@/commands/config";
import profileCommand from "@/commands/profile";
import insightsCommand from "@/commands/insights";
import mentionsCommand from "@/commands/mentions";
import workflowCommand from "@/commands/workflow";
import { ERROR_NO_TOKEN } from "@/core/constants";
import activityCommand from "@/commands/activity";
import { setTheme, initializeTheme } from "@/core/theme";
Expand Down Expand Up @@ -59,6 +62,9 @@ labelsCommand.register(program);
profileCommand.register(program);
configCommand.register(program);
prCommand.register(program);
workflowCommand.register(program);
cacheCommand.register(program);
runCommand.register(program);

program
.command("version")
Expand All @@ -79,6 +85,9 @@ Examples:
ghg repos report --org airscripts
ghg labels push
ghg profile detect
ghg workflow validate
ghg workflow preview
ghg run debug 123456
`,
);

Expand Down
51 changes: 51 additions & 0 deletions src/commands/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Command } from "commander";

import prompt from "@/core/prompt";
import command from "@/core/command";
import cacheService from "@/services/cache";

const register = (program: Command) => {
const cache = program
.command("cache")
.description("Inspect GitHub Actions caches.");

cache
.command("inspect")
.description("Inspect cache metadata by key.")
.argument("[key]", "Cache key or prefix")
.option("--repo <repo>", "Repository (owner/repo)")
.action(async (key: string | undefined, options: { repo?: string }) => {
const value =
key ??
(await prompt.text("Enter cache key to inspect:", {
placeholder: "linux-node-modules",
}));

await command.run(() => cacheService.inspect(value, options.repo));
});

cache
.command("download")
.description(
"Create a local cache debug bundle (metadata + related downloadable assets).",
)
.argument("[key]", "Cache key or prefix")
.option("--repo <repo>", "Repository (owner/repo)")
.option("--output-dir <path>", "Output directory for the debug bundle")
.action(
async (
key: string | undefined,
options: { repo?: string; outputDir?: string },
) => {
const value =
key ??
(await prompt.text("Enter cache key to download:", {
placeholder: "linux-node-modules",
}));

await command.run(() => cacheService.download(value, options));
},
);
};

export default { register };
36 changes: 36 additions & 0 deletions src/commands/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Command } from "commander";

import prompt from "@/core/prompt";
import command from "@/core/command";
import runService from "@/services/run";

const register = (program: Command) => {
const run = program
.command("run")
.description("Inspect and debug workflow runs.");

run
.command("debug")
.description("Fetch logs, artifacts, and annotations for a run.")
.argument("[run-id]", "Workflow run id")
.option("--repo <repo>", "Repository (owner/repo)")
.option("--output-dir <path>", "Output directory for debug bundle")
.action(
async (
runId: string | undefined,
options: { repo?: string; outputDir?: string },
) => {
const value =
runId ??
(await prompt.text("Enter workflow run id:", {
placeholder: "123456",
}));

await command.run(() =>
runService.debugRun(parseInt(value, 10), options),
);
},
);
};

export default { register };
28 changes: 28 additions & 0 deletions src/commands/workflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Command } from "commander";

import command from "@/core/command";
import workflowService from "@/services/workflow";

const register = (program: Command) => {
const workflow = program
.command("workflow")
.description("Validate and preview GitHub Actions workflows.");

workflow
.command("validate")
.description("Validate workflow files before pushing.")
.argument("[path]", "Optional workflow file path")
.action(async (targetPath?: string) => {
await command.run(() => workflowService.validate(targetPath));
});

workflow
.command("preview")
.description("Preview workflow job graph, runners, and matrix.")
.argument("[path]", "Optional workflow file path")
.action(async (targetPath?: string) => {
await command.run(() => workflowService.preview(targetPath));
});
};

export default { register };
15 changes: 15 additions & 0 deletions src/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,21 @@ export const ERROR_LABEL_SOURCE_REQUIRED =
"Either --template or --metadata must be provided.";

export const INFO_NO_NOTIFICATIONS = "No notifications found.";
export const DEFAULT_OUTPUT_DIR = ".ghitgud/actions";
export const WORKFLOW_DEFAULT_DIR = ".github/workflows";
export const WORKFLOW_FILE_EXTENSIONS = [".yml", ".yaml"] as const;

export const ERROR_WORKFLOW_NOT_FOUND =
"No workflow files were found in .github/workflows.";

export const ERROR_WORKFLOW_INVALID_YAML =
"Workflow file contains invalid YAML.";

export const ERROR_RUN_ID_REQUIRED = "Run id is required.";
export const ERROR_CACHE_KEY_REQUIRED = "Cache key is required.";

export const INFO_CACHE_METADATA_ONLY =
"Cache metadata found, but cache byte download is not available through the official API.";

export const PING_RESPONSE = "pong";
export const DEFAULT_REPOS_RETIRE_MONTHS = 12;
Expand Down
Loading