feat: add scripts to manage git repos
Node.js CI / Lint and Test (push) Failing after 23s

This commit is contained in:
2025-12-11 17:40:44 -08:00
parent dd294879b9
commit 275fa2e579
5 changed files with 447 additions and 2 deletions
+70
View File
@@ -0,0 +1,70 @@
/**
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { input } 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 "/.gitea/workflows/security.yml".
*/
const deletePath = await input({
message:
// eslint-disable-next-line stylistic/max-len -- Big boi string.
"Enter the PATH to delete the file from, WITHOUT leading slash. Example: '.gitea/workflows/security.yml'",
});
if (deletePath === "") {
throw new Error("Delete path is not set");
}
const orgs = [ "nhcarrigan", "nhcarrigan-private", "nhcarrigan-games" ];
for (const org of orgs) {
const repos = await paginatedFetch<Array<Repository>>(`${giteaUrl}/api/v1/orgs/${org}/repos`, 100, { headers: { authorization: `Bearer ${giteaToken}` } });
for (const repo of repos) {
console.log(`Checking if file exists in ${org}/${repo.name}`);
const fileResponse = await fetch(`${giteaUrl}/api/v1/repos/${org}/${repo.name}/contents/${deletePath}`, {
headers: {
authorization: `Bearer ${giteaToken}`,
},
method: "GET",
});
if (fileResponse.ok) {
console.log(`File exists in ${org}/${repo.name}, deleting...`);
const fileData: File = await fileResponse.json();
console.log(`Deleting ${deletePath} from ${org}/${repo.name}`);
const response = await fetch(`${giteaUrl}/api/v1/repos/${org}/${repo.name}/contents/${deletePath}`, {
body: JSON.stringify({
branch: repo.default_branch,
message: `feat: automated delete of ${deletePath}`,
sha: fileData.sha,
}),
headers: {
"authorization": `Bearer ${giteaToken}`,
// eslint-disable-next-line @typescript-eslint/naming-convention -- Standard header convention.
"content-type": "application/json",
},
method: "DELETE",
});
if (!response.ok) {
console.error(`Failed to delete ${deletePath} from ${org}/${repo.name}: ${response.statusText}`);
console.error(await response.text());
continue;
}
console.log(`Deleted ${deletePath} from ${org}/${repo.name}`);
continue;
}
console.log(`File does not exist in ${org}/${repo.name}, skipping...`);
}
}
+108
View File
@@ -0,0 +1,108 @@
/**
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { input } 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");
}
const orgs = [ "nhcarrigan", "nhcarrigan-private", "nhcarrigan-games" ];
for (const org of orgs) {
const repos = await paginatedFetch<Array<Repository>>(`${giteaUrl}/api/v1/orgs/${org}/repos`, 100, { headers: { authorization: `Bearer ${giteaToken}` } });
for (const repo of repos) {
console.log(`Checking if 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) {
console.error(`Failed to update ${fileName} in ${org}/${repo.name}: ${response.statusText}`);
console.error(await response.text());
continue;
}
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) {
console.error(`Failed to upload ${fileName} to ${org}/${repo.name}: ${response.statusText}`);
console.error(await response.text());
continue;
}
console.log(`Uploaded ${fileName} to ${org}/${repo.name}`);
}
}