generated from nhcarrigan/template
This commit is contained in:
@@ -1,4 +1,19 @@
|
|||||||
|
# Crowdin
|
||||||
|
CROWDIN_PROJECT_ID="op://Environment Variables - Development/Scripts/Crowdin Project ID"
|
||||||
|
CROWDIN_API_URL="op://Environment Variables - Development/Scripts/Crowdin API Url"
|
||||||
|
CROWDIN_TOKEN="op://Environment Variables - Development/Scripts/Crowdin Token"
|
||||||
|
|
||||||
|
# Github
|
||||||
|
GITHUB_TOKEN="op://Environment Variables - Development/Scripts/GitHub Token"
|
||||||
|
|
||||||
|
# Discord
|
||||||
|
DISCORD_TOKEN="op://Environment Variables - Development/Scripts/Discord Token"
|
||||||
|
DISCORD_CLIENT_ID="op://Private/Guild Counter/client id"
|
||||||
|
DISCORD_CLIENT_SECRET="op://Private/Guild Counter/client secret"
|
||||||
|
|
||||||
|
# AWS
|
||||||
AWS_ACCESS_KEY_ID="op://Private/Hetzner/S3 Access Key ID"
|
AWS_ACCESS_KEY_ID="op://Private/Hetzner/S3 Access Key ID"
|
||||||
AWS_SECRET_ACCESS_KEY="op://Private/Hetzner/S3 Secret Access Key"
|
AWS_SECRET_ACCESS_KEY="op://Private/Hetzner/S3 Secret Access Key"
|
||||||
DISCORD_CLIENT_ID="op://Private/Guild Counter/client id"
|
|
||||||
DISCORD_CLIENT_SECRET="op://Private/Guild Counter/client secret"
|
# Gitea
|
||||||
|
GITEA_TOKEN="op://Private/Gitea/token"
|
||||||
@@ -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...`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
/**
|
||||||
|
* @copyright NHCarrigan
|
||||||
|
* @license Naomi's Public License
|
||||||
|
* @author Naomi Carrigan
|
||||||
|
*/
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention -- This is a Gitea interface. The responses from the API are not something we can control. */
|
||||||
|
|
||||||
|
interface Repository extends Record<string, unknown> {
|
||||||
|
allow_fast_forward_only_merge: boolean;
|
||||||
|
allow_merge_commits: boolean;
|
||||||
|
allow_rebase: boolean;
|
||||||
|
allow_rebase_explicit: boolean;
|
||||||
|
allow_rebase_update: boolean;
|
||||||
|
allow_squash_merge: boolean;
|
||||||
|
archived: boolean;
|
||||||
|
archived_at: string;
|
||||||
|
avatar_url: string;
|
||||||
|
clone_url: string;
|
||||||
|
created_at: string;
|
||||||
|
default_allow_maintainer_edit: boolean;
|
||||||
|
default_branch: string;
|
||||||
|
default_delete_branch_after_merge: boolean;
|
||||||
|
default_merge_style: string;
|
||||||
|
description: string;
|
||||||
|
empty: boolean;
|
||||||
|
external_tracker: {
|
||||||
|
external_tracker_format: string;
|
||||||
|
external_tracker_regexp_pattern: string;
|
||||||
|
external_tracker_style: string;
|
||||||
|
external_tracker_url: string;
|
||||||
|
};
|
||||||
|
external_wiki: {
|
||||||
|
external_wiki_url: string;
|
||||||
|
};
|
||||||
|
fork: boolean;
|
||||||
|
forks_count: number;
|
||||||
|
full_name: string;
|
||||||
|
has_actions: boolean;
|
||||||
|
has_issues: boolean;
|
||||||
|
has_packages: boolean;
|
||||||
|
has_projects: boolean;
|
||||||
|
has_pull_requests: boolean;
|
||||||
|
has_releases: boolean;
|
||||||
|
has_wiki: boolean;
|
||||||
|
html_url: string;
|
||||||
|
id: number;
|
||||||
|
ignore_whitespace_conflicts: boolean;
|
||||||
|
internal: boolean;
|
||||||
|
internal_tracker: {
|
||||||
|
allow_only_contributors_to_track_time: boolean;
|
||||||
|
enable_issue_dependencies: boolean;
|
||||||
|
enable_time_tracker: boolean;
|
||||||
|
};
|
||||||
|
language: string;
|
||||||
|
languages_url: string;
|
||||||
|
licenses: Array<string>;
|
||||||
|
link: string;
|
||||||
|
mirror: boolean;
|
||||||
|
mirror_interval: string;
|
||||||
|
mirror_updated: string;
|
||||||
|
name: string;
|
||||||
|
object_format_name: string;
|
||||||
|
open_issues_count: number;
|
||||||
|
open_pr_counter: number;
|
||||||
|
original_url: string;
|
||||||
|
owner: {
|
||||||
|
active: boolean;
|
||||||
|
avatar_url: string;
|
||||||
|
created: string;
|
||||||
|
description: string;
|
||||||
|
email: string;
|
||||||
|
followers_count: number;
|
||||||
|
following_count: number;
|
||||||
|
full_name: string;
|
||||||
|
html_url: string;
|
||||||
|
id: number;
|
||||||
|
is_admin: boolean;
|
||||||
|
language: string;
|
||||||
|
last_login: string;
|
||||||
|
location: string;
|
||||||
|
login: string;
|
||||||
|
login_name: string;
|
||||||
|
prohibit_login: boolean;
|
||||||
|
restricted: boolean;
|
||||||
|
source_id: number;
|
||||||
|
starred_repos_count: number;
|
||||||
|
visibility: string;
|
||||||
|
website: string;
|
||||||
|
};
|
||||||
|
parent: string;
|
||||||
|
permissions: {
|
||||||
|
admin: boolean;
|
||||||
|
pull: boolean;
|
||||||
|
push: boolean;
|
||||||
|
};
|
||||||
|
private: boolean;
|
||||||
|
projects_mode: string;
|
||||||
|
release_counter: number;
|
||||||
|
repo_transfer: {
|
||||||
|
doer: {
|
||||||
|
active: boolean;
|
||||||
|
avatar_url: string;
|
||||||
|
created: string;
|
||||||
|
description: string;
|
||||||
|
email: string;
|
||||||
|
followers_count: number;
|
||||||
|
following_count: number;
|
||||||
|
full_name: string;
|
||||||
|
html_url: string;
|
||||||
|
id: number;
|
||||||
|
is_admin: boolean;
|
||||||
|
language: string;
|
||||||
|
last_login: string;
|
||||||
|
location: string;
|
||||||
|
login: string;
|
||||||
|
login_name: string;
|
||||||
|
prohibit_login: boolean;
|
||||||
|
restricted: boolean;
|
||||||
|
source_id: number;
|
||||||
|
starred_repos_count: number;
|
||||||
|
visibility: string;
|
||||||
|
website: string;
|
||||||
|
};
|
||||||
|
recipient: {
|
||||||
|
active: boolean;
|
||||||
|
avatar_url: string;
|
||||||
|
created: string;
|
||||||
|
description: string;
|
||||||
|
email: string;
|
||||||
|
followers_count: number;
|
||||||
|
following_count: number;
|
||||||
|
full_name: string;
|
||||||
|
html_url: string;
|
||||||
|
id: number;
|
||||||
|
is_admin: boolean;
|
||||||
|
language: string;
|
||||||
|
last_login: string;
|
||||||
|
location: string;
|
||||||
|
login: string;
|
||||||
|
login_name: string;
|
||||||
|
prohibit_login: boolean;
|
||||||
|
restricted: boolean;
|
||||||
|
source_id: number;
|
||||||
|
starred_repos_count: number;
|
||||||
|
visibility: string;
|
||||||
|
website: string;
|
||||||
|
};
|
||||||
|
teams: Array<{
|
||||||
|
can_create_org_repo: boolean;
|
||||||
|
description: string;
|
||||||
|
id: number;
|
||||||
|
includes_all_repositories: boolean;
|
||||||
|
name: string;
|
||||||
|
organization: {
|
||||||
|
avatar_url: string;
|
||||||
|
description: string;
|
||||||
|
email: string;
|
||||||
|
full_name: string;
|
||||||
|
id: number;
|
||||||
|
location: string;
|
||||||
|
name: string;
|
||||||
|
repo_admin_change_team_access: boolean;
|
||||||
|
username: string;
|
||||||
|
visibility: string;
|
||||||
|
website: string;
|
||||||
|
};
|
||||||
|
permission: string;
|
||||||
|
units: Array<string>;
|
||||||
|
units_map: {
|
||||||
|
"repo.code": string;
|
||||||
|
"repo.ext_issues": string;
|
||||||
|
"repo.ext_wiki": string;
|
||||||
|
"repo.issues": string;
|
||||||
|
"repo.projects": string;
|
||||||
|
"repo.pulls": string;
|
||||||
|
"repo.releases": string;
|
||||||
|
"repo.wiki": string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
size: number;
|
||||||
|
ssh_url: string;
|
||||||
|
stars_count: number;
|
||||||
|
template: boolean;
|
||||||
|
topics: Array<string>;
|
||||||
|
updated_at: string;
|
||||||
|
url: string;
|
||||||
|
watchers_count: number;
|
||||||
|
website: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface File {
|
||||||
|
_links: {
|
||||||
|
git: string;
|
||||||
|
html: string;
|
||||||
|
self: string;
|
||||||
|
};
|
||||||
|
content: string;
|
||||||
|
download_url: string;
|
||||||
|
encoding: string;
|
||||||
|
git_url: string;
|
||||||
|
html_url: string;
|
||||||
|
last_author_date: string;
|
||||||
|
last_commit_sha: string;
|
||||||
|
last_committer_date: string;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
sha: string;
|
||||||
|
size: number;
|
||||||
|
submodule_git_url: string;
|
||||||
|
target: string;
|
||||||
|
type: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { Repository, File };
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* @copyright NHCarrigan
|
||||||
|
* @license Naomi's Public License
|
||||||
|
* @author Naomi Carrigan
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a paginated resource from a URL. Automatically handles pagination,
|
||||||
|
* and returns the complete data as an array.
|
||||||
|
* @type {Array<Record<string, unknown>>} T - The type of data returned from the API endpoint. This should be an array of objects.
|
||||||
|
* @param url - The URL to fetch.
|
||||||
|
* @param limit - The number of items to fetch per page.
|
||||||
|
* @param options - The standard fetch options object.
|
||||||
|
* @returns The complete data as type T.
|
||||||
|
*/
|
||||||
|
export const paginatedFetch = async <T extends Array<Record<string, unknown>>>(
|
||||||
|
url: string,
|
||||||
|
limit: number,
|
||||||
|
options: RequestInit = {},
|
||||||
|
): Promise<T> => {
|
||||||
|
let page = 1;
|
||||||
|
let offset = 0;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- This is a workaround to avoid type errors.
|
||||||
|
const data: T = [] as unknown as T;
|
||||||
|
let request = await fetch(`${url}?limit=${limit.toString()}&page=${page.toString()}&offset=${offset.toString()}`, options);
|
||||||
|
let response: T = await request.json();
|
||||||
|
data.push(...response);
|
||||||
|
while (response.length >= limit) {
|
||||||
|
page = page + 1;
|
||||||
|
offset = offset + limit;
|
||||||
|
request = await fetch(`${url}?limit=${limit.toString()}&page=${page.toString()}&offset=${offset.toString()}`, options);
|
||||||
|
response = await request.json();
|
||||||
|
data.push(...response);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user