/** * @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()}`);