import { createServerFn } from "@tanstack/react-start"; import { requireViewer, viewerMiddleware } from "./middleware"; import { WORKFLOW_DIR } from "~/server/ci/spec"; import { badRequest } from "~/server/errors"; import { listRefs, resolveRevision } from "~/server/git/refs"; import { withStore } from "~/server/git/store"; import type { GitStore } from "~/server/git/store"; import { commitFileChanges } from "~/server/git/web-commit"; import { limitWrite } from "~/server/limits"; import { requireRepoAccess } from "~/server/repos"; import { repoRef } from "~/lib/validation"; import type { RepoRow } from "~/lib/types"; export const GITHUB_WORKFLOW_DIR = ".github/workflows"; const YAML_FILE = /\.ya?ml$/; async function workflowNames(store: GitStore, treeSha: string, directory: string): Promise> { const entry = await store.entryAtPath(treeSha, directory); if (!entry || entry.type !== "tree") return new Map(); const files = (await store.listTree(entry.sha, directory)).filter( (file) => file.type === "blob" && YAML_FILE.test(file.name), ); return new Map(files.map((file) => [file.name, file.sha])); } /** GitHub workflow files on the default branch that have no same-named file in `.gitfor/workflows` yet. */ async function importable(repo: RepoRow) { const revision = resolveRevision(await listRefs(repo), repo.default_branch); if (!revision) return { headSha: null, files: [] as Array<{ name: string; sha: string }> }; return withStore(repo.artifacts_name, async (store) => { const commit = await store.commit(revision.sha); if (!commit) return { headSha: revision.sha, files: [] }; const [github, existing] = await Promise.all([ workflowNames(store, commit.treeSha, GITHUB_WORKFLOW_DIR), workflowNames(store, commit.treeSha, WORKFLOW_DIR), ]); const files = [...github].filter(([name]) => !existing.has(name)).map(([name, sha]) => ({ name, sha })); return { headSha: revision.sha, files }; }); } /** Names of `.github/workflows` files that could be copied into `.gitfor/workflows`. */ export const listImportableWorkflows = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(repoRef) .handler(async ({ data, context }) => { const { repo } = await requireRepoAccess(data.owner, data.repo, context.viewer, "read"); const { files } = await importable(repo); return files.map((file) => file.name); }); /** Copies every importable `.github/workflows` file into `.gitfor/workflows` in one commit on the default branch. */ export const importGithubWorkflows = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); const { repo, permission } = await requireRepoAccess(data.owner, data.repo, viewer, "write"); const { headSha, files } = await importable(repo); if (files.length === 0) throw badRequest(`No workflows to import from ${GITHUB_WORKFLOW_DIR}`); const changes = await withStore(repo.artifacts_name, (store) => Promise.all( files.map(async (file) => { const content = await store.blob(file.sha); if (!content) throw badRequest(`Could not read ${GITHUB_WORKFLOW_DIR}/${file.name}`); return { path: `${WORKFLOW_DIR}/${file.name}`, content }; }), ), ); await commitFileChanges({ repo, permission, viewer, branch: repo.default_branch, changes, message: `Import workflows from ${GITHUB_WORKFLOW_DIR}`, expectedHeadSha: headSha, newBranch: null, }); return { imported: files.map((file) => file.name) }; });