From 369992d66557771605bf1d06135b8e06c243d090 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Mon, 22 Dec 2025 10:28:26 -0800 Subject: [PATCH] feat: add script for conditional repo uploads --- src/gitea/uploadToReposConditionally.ts | 175 ++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 src/gitea/uploadToReposConditionally.ts diff --git a/src/gitea/uploadToReposConditionally.ts b/src/gitea/uploadToReposConditionally.ts new file mode 100644 index 0000000..90f540c --- /dev/null +++ b/src/gitea/uploadToReposConditionally.ts @@ -0,0 +1,175 @@ +/** + * @copyright NHCarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { input, select } from "@inquirer/prompts"; +import { paginatedFetch } from "../utils/paginatedFetch.js"; +import type { File, Repository } from "../interfaces/gitea.js"; + +const giteaToken = process.env.GITEA_TOKEN; +if (giteaToken === undefined) { + throw new Error("GITEA_TOKEN is not set"); +} + +const giteaUrl = "https://git.nhcarrigan.com"; + +/** + * Will be something like "actions.yml" or "gitea/actions.yml". + */ +const fileName = await input({ + message: + // eslint-disable-next-line stylistic/max-len -- Big boi string. + "Enter the name of the file to upload. Your file MUST be in the `data` directory in this repository. WITHOUT leading slash. Example: 'actions.yml' or 'gitea/actions.yml'", +}); +if (fileName === "") { + throw new Error("File name is not set"); +} + +const file = await readFile( + join(import.meta.dirname, "..", "..", "data", fileName), + "utf-8", +); + +/** + * Will be something like ".gitea/workflows/security.yml". + */ +const uploadPath = await input({ + message: + // eslint-disable-next-line stylistic/max-len -- Big boi string. + "Enter the PATH to upload the file to, WITHOUT leading slash. Example: '.gitea/workflows/security.yml'", +}); +if (uploadPath === "") { + throw new Error("Upload path is not set"); +} + +/** + * The file path to check for in each repo to determine if upload should proceed. + * Will be something like ".gitea/workflows/ci.yml" or "package.json". + */ +const conditionFilePath = await input({ + message: + // eslint-disable-next-line stylistic/max-len -- Big boi string. + "Enter the file path to check for in each repo (condition file). WITHOUT leading slash. Example: '.gitea/workflows/ci.yml' or 'package.json'", +}); +if (conditionFilePath === "") { + throw new Error("Condition file path is not set"); +} + +/** + * Whether to upload when the condition file exists or doesn't exist. + */ +const uploadCondition = await select({ + choices: [ + { name: "Upload if condition file EXISTS", value: "exists" }, + { name: "Upload if condition file DOES NOT EXIST", value: "not_exists" }, + ], + message: "When should the upload proceed?", +}); + +const orgs = [ "nhcarrigan", "nhcarrigan-private", "nhcarrigan-games" ]; + +let totalReposProcessed = 0; +let totalReposSucceeded = 0; +let totalReposFailed = 0; +let totalReposSkipped = 0; + +for (const org of orgs) { + console.log(`\n=== Fetching repositories for org: ${org} ===`); + const repos = await paginatedFetch>(`${giteaUrl}/api/v1/orgs/${org}/repos`, 100, { headers: { authorization: `Bearer ${giteaToken}` } }); + console.log(`Found ${repos.length.toString()} repositories in ${org}`); + + for (const repo of repos) { + totalReposProcessed = totalReposProcessed + 1; + console.log(`Checking condition file in ${org}/${repo.name}`); + + // Check if condition file exists + const conditionFileResponse = await fetch(`${giteaUrl}/api/v1/repos/${org}/${repo.name}/contents/${conditionFilePath}`, { + headers: { + authorization: `Bearer ${giteaToken}`, + }, + method: "GET", + }); + + const conditionFileExists = conditionFileResponse.ok; + const shouldUpload = uploadCondition === "exists" + ? conditionFileExists + : !conditionFileExists; + + if (!shouldUpload) { + totalReposSkipped = totalReposSkipped + 1; + console.log(`Skipping ${org}/${repo.name} (condition file ${conditionFileExists + ? "exists" + : "does not exist"}, but upload condition is "${uploadCondition}")`); + continue; + } + + console.log(`Condition met for ${org}/${repo.name}, proceeding with upload`); + console.log(`Checking if upload file exists in ${org}/${repo.name}`); + const fileResponse = await fetch(`${giteaUrl}/api/v1/repos/${org}/${repo.name}/contents/${uploadPath}`, { + headers: { + authorization: `Bearer ${giteaToken}`, + }, + method: "GET", + }); + if (fileResponse.ok) { + console.log(`File already exists in ${org}/${repo.name}`); + const fileData: File = await fileResponse.json(); + console.log(`Updating ${fileName} in ${org}/${repo.name}`); + const response = await fetch(`${giteaUrl}/api/v1/repos/${org}/${repo.name}/contents/${uploadPath}`, { + body: JSON.stringify({ + branch: repo.default_branch, + content: Buffer.from(file).toString("base64"), + message: `feat: automated upload of ${uploadPath}`, + sha: fileData.sha, + }), + headers: { + "authorization": `Bearer ${giteaToken}`, + // eslint-disable-next-line @typescript-eslint/naming-convention -- Standard header convention. + "content-type": "application/json", + }, + method: "PUT", + }); + if (!response.ok) { + totalReposFailed = totalReposFailed + 1; + console.error(`Failed to update ${fileName} in ${org}/${repo.name}: ${response.statusText}`); + console.error(await response.text()); + continue; + } + totalReposSucceeded = totalReposSucceeded + 1; + console.log(`Updated ${fileName} in ${org}/${repo.name}`); + continue; + } + console.log(`Uploading ${fileName} to ${org}/${repo.name}`); + const response = await fetch(`${giteaUrl}/api/v1/repos/${org}/${repo.name}/contents/${uploadPath}`, { + body: JSON.stringify({ + branch: repo.default_branch, + content: Buffer.from(file).toString("base64"), + message: `feat: automated upload of ${uploadPath}`, + }), + headers: { + "authorization": `Bearer ${giteaToken}`, + // eslint-disable-next-line @typescript-eslint/naming-convention -- Standard header convention. + "content-type": "application/json", + }, + method: "POST", + }); + if (!response.ok) { + totalReposFailed = totalReposFailed + 1; + console.error(`Failed to upload ${fileName} to ${org}/${repo.name}: ${response.statusText}`); + console.error(await response.text()); + continue; + } + totalReposSucceeded = totalReposSucceeded + 1; + console.log(`Uploaded ${fileName} to ${org}/${repo.name}`); + } +} + +console.log(`\n=== Summary ===`); +console.log(`Total repositories processed: ${totalReposProcessed.toString()}`); +console.log(`Successfully uploaded/updated: ${totalReposSucceeded.toString()}`); +console.log(`Failed: ${totalReposFailed.toString()}`); +console.log(`Skipped (condition not met): ${totalReposSkipped.toString()}`); +