forgejo
This commit is contained in:
parent
9af663a0c9
commit
f981518d88
13 changed files with 491 additions and 25 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@ node_modules/
|
|||
build-javascript
|
||||
cache/
|
||||
artifacts/
|
||||
secrets/
|
48
alpine.ts
48
alpine.ts
|
@ -2,22 +2,23 @@ import {
|
|||
chmod,
|
||||
copyFile,
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
opendir,
|
||||
rm,
|
||||
symlink,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { cwd } from "node:process";
|
||||
import { Fstab } from "./fstab.js";
|
||||
import { execFile } from "./helpers/better-api.js";
|
||||
import { PasswdEntry, sudoReadPasswd } from "./helpers/passwd.js";
|
||||
import { sudoWriteExecutable } from "./helpers/sudo.js";
|
||||
|
||||
export class Alpine {
|
||||
dir: string;
|
||||
private constructor({ dir }: { dir: string }) {
|
||||
this.dir = dir;
|
||||
}
|
||||
fstab: Fstab = new Fstab(this);
|
||||
|
||||
async mkdir(dir: string, opts?: { recursive: boolean }): Promise<void> {
|
||||
await mkdir(path.join(this.dir, dir), opts);
|
||||
|
@ -30,24 +31,11 @@ export class Alpine {
|
|||
await this.writeFile(filePath, content);
|
||||
await chmod(path.join(this.dir, filePath), 700);
|
||||
}
|
||||
async sudoWriteExecutable(filePath: string, content: string): Promise<void> {
|
||||
const dir = await mkdtemp(
|
||||
path.join(tmpdir(), "define-alpine-sudoWriteExecutable-")
|
||||
);
|
||||
try {
|
||||
const tmpFile = path.join(dir, path.basename(filePath));
|
||||
const finalPath = path.join(this.dir, filePath);
|
||||
await writeFile(tmpFile, content);
|
||||
await execFile("sudo", [
|
||||
"mkdir",
|
||||
"--parents",
|
||||
path.join(this.dir, path.dirname(filePath)),
|
||||
]);
|
||||
await execFile("sudo", ["mv", tmpFile, finalPath]);
|
||||
await execFile("sudo", ["chmod", "700", finalPath]);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
path(p: string): string {
|
||||
return path.join(this.dir, p);
|
||||
}
|
||||
sudoWriteExecutable(filePath: string, content: string): Promise<void> {
|
||||
return sudoWriteExecutable(this.path(filePath), content);
|
||||
}
|
||||
private getRelativeSymlink(
|
||||
target: string,
|
||||
|
@ -71,6 +59,22 @@ export class Alpine {
|
|||
await execFile("sudo", ["ln", "-s", target, filePath]);
|
||||
}
|
||||
|
||||
async userAdd(user: string): Promise<PasswdEntry> {
|
||||
await execFile("sudo", [
|
||||
"useradd",
|
||||
"--user-group",
|
||||
"--root",
|
||||
this.dir,
|
||||
user,
|
||||
]);
|
||||
const passwd = await sudoReadPasswd(this.path("/etc/passwd"));
|
||||
const entry = passwd.find((e) => e.name === user);
|
||||
if (!entry) {
|
||||
throw new Error("fatal(userAdd): no encontré el usuario " + user);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
async addPackages(packages: string[]): Promise<void> {
|
||||
await execFile("sudo", [
|
||||
"apk",
|
||||
|
@ -128,6 +132,8 @@ export class Alpine {
|
|||
...(packages || []),
|
||||
]);
|
||||
|
||||
return new Alpine({ dir });
|
||||
const alpine = new Alpine({ dir });
|
||||
await alpine.fstab.write();
|
||||
return alpine;
|
||||
}
|
||||
}
|
||||
|
|
52
forgejo/build.ts
Normal file
52
forgejo/build.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { reproRun } from "../helpers/repro-run.js";
|
||||
|
||||
const FORGEJO_VERSION = "v1.18.3-0";
|
||||
|
||||
// returns path to statically compiled binary
|
||||
export async function buildForgejo(): Promise<string> {
|
||||
const dir = "cache/forgejo";
|
||||
await mkdir(dir, { recursive: true });
|
||||
const versionFile = join(dir, "version");
|
||||
const output = join(dir, "rootfs/forgejo");
|
||||
try {
|
||||
if ((await readFile(versionFile, "utf-8")) === FORGEJO_VERSION)
|
||||
return output;
|
||||
} catch {}
|
||||
|
||||
{
|
||||
const buildScript = join(dir, "build");
|
||||
await writeFile(
|
||||
buildScript,
|
||||
`#!/bin/sh -e
|
||||
runprint() {
|
||||
echo "==> $@"
|
||||
"$@"
|
||||
}
|
||||
|
||||
runprint apk add --quiet git nodejs npm go make
|
||||
|
||||
# TODO: cachear clon de repo
|
||||
runprint git clone https://codeberg.org/forgejo/forgejo --branch '${FORGEJO_VERSION}' --depth 1 --single-branch
|
||||
cd forgejo
|
||||
|
||||
runprint env GOOS=linux GOARCH=amd64 LDFLAGS="-linkmode external -extldflags '-static' $LDFLAGS" TAGS="bindata sqlite sqlite_unlock_notify" make build
|
||||
mv gitea /forgejo
|
||||
`
|
||||
);
|
||||
await chmod(buildScript, 0o700);
|
||||
}
|
||||
await reproRun({
|
||||
cwd: dir,
|
||||
command: "/src/build",
|
||||
cache: [
|
||||
"/home/repro/.cache/go-build",
|
||||
"/home/repro/go",
|
||||
"/home/repro/.npm",
|
||||
],
|
||||
});
|
||||
await writeFile(versionFile, FORGEJO_VERSION);
|
||||
|
||||
return output;
|
||||
}
|
171
forgejo/index.ts
Normal file
171
forgejo/index.ts
Normal file
|
@ -0,0 +1,171 @@
|
|||
import { buildForgejo } from "./build.js";
|
||||
import { Alpine } from "../alpine.js";
|
||||
import { Runit } from "../runit/index.js";
|
||||
import { constants, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { loadForgejoSecretsFile } from "./secrets.js";
|
||||
import { sudoChmod, sudoChown, sudoCopy } from "../helpers/sudo.js";
|
||||
|
||||
export async function setupForgejo(alpine: Alpine, runit: Runit) {
|
||||
const bin = await buildForgejo();
|
||||
await sudoCopy(bin, join(alpine.dir, "/usr/local/bin/forgejo"));
|
||||
|
||||
await alpine.addPackages(["tzdata", "git"]);
|
||||
const entry = await alpine.userAdd("_forgejo");
|
||||
|
||||
// TODO: persistir
|
||||
await alpine.fstab.addTmpfs("/var/lib/forgejo", {
|
||||
uid: entry.uid,
|
||||
mode: "700",
|
||||
});
|
||||
|
||||
const secrets = await loadForgejoSecretsFile();
|
||||
const configPath = join(alpine.dir, "/etc/forgejo.conf");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`
|
||||
; see https://docs.gitea.io/en-us/config-cheat-sheet/ for additional documentation.
|
||||
|
||||
APP_NAME = cat /dev/null
|
||||
RUN_USER = _forgejo
|
||||
RUN_MODE = prod
|
||||
|
||||
[server]
|
||||
PROTOCOL = http
|
||||
DOMAIN = gitea.nulo.in
|
||||
ROOT_URL = https://gitea.nulo.in/
|
||||
HTTP_ADDR = 127.0.0.1
|
||||
HTTP_PORT = 3000
|
||||
UNIX_SOCKET_PERMISSION = 660
|
||||
|
||||
DISABLE_SSH = false
|
||||
START_SSH_SERVER = false
|
||||
SSH_PORT = 993
|
||||
;; Enable exposure of SSH clone URL to anonymous visitors, default is false
|
||||
SSH_EXPOSE_ANONYMOUS = true
|
||||
OFFLINE_MODE = true
|
||||
DISABLE_ROUTER_LOG = false
|
||||
STATIC_ROOT_PATH = /var/lib/forgejo
|
||||
APP_DATA_PATH = /var/lib/forgejo/data
|
||||
ENABLE_GZIP = true
|
||||
LANDING_PAGE = explore
|
||||
|
||||
LFS_START_SERVER = true
|
||||
LFS_JWT_SECRET = ${secrets.LFS_JWT_SECRET}
|
||||
|
||||
;; Doesn't work, setup under nginx
|
||||
;[cors]
|
||||
;ENABLED = true
|
||||
;ALLOW_DOMAIN = *
|
||||
;ALLOW_SUBDOMAIN = false
|
||||
;METHODS = GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS
|
||||
;MAX_AGE = 10m
|
||||
;ALLOW_CREDENTIALS = false
|
||||
;X_FRAME_OPTIONS = SAMEORIGIN
|
||||
|
||||
[log]
|
||||
LEVEL = Warn
|
||||
|
||||
[database]
|
||||
DB_TYPE = sqlite3
|
||||
PATH = data/forgejo.db
|
||||
|
||||
[security]
|
||||
INSTALL_LOCK = true
|
||||
SECRET_KEY = ${secrets.SECRET_KEY}
|
||||
INTERNAL_TOKEN = ${secrets.INTERNAL_TOKEN}
|
||||
PASSWORD_HASH_ALGO = argon2
|
||||
|
||||
[oauth2]
|
||||
ENABLE = true
|
||||
JWT_SECRET = ${secrets.OAUTH_JWT_SECRET}
|
||||
JWT_SIGNING_ALGORITHM = HS512
|
||||
|
||||
[service]
|
||||
REGISTER_EMAIL_CONFIRM = true
|
||||
DISABLE_REGISTRATION = true
|
||||
;; Mail notification
|
||||
ENABLE_NOTIFY_MAIL = true
|
||||
DEFAULT_KEEP_EMAIL_PRIVATE = true
|
||||
|
||||
[repository]
|
||||
ROOT=/var/lib/gitea/data/gitea-repositories
|
||||
;PREFERRED_LICENSES = Apache License 2.0,MIT License
|
||||
DEFAULT_BRANCH = antifascista
|
||||
ENABLE_PUSH_CREATE_USER = true
|
||||
ENABLE_PUSH_CREATE_ORG = true
|
||||
|
||||
[repository.pull-request]
|
||||
WORK_IN_PROGRESS_PREFIXES = WIP:,[WIP],Draft
|
||||
CLOSE_KEYWORDS = close,closes,closed,fix,fixes,fixed,resolve,resolves,resolved,cierra
|
||||
REOPEN_KEYWORDS = reopen,reopens,reopened,reabre
|
||||
|
||||
[project]
|
||||
PROJECT_BOARD_BASIC_KANBAN_TYPE = Hacer, Haciendo, Hecho
|
||||
PROJECT_BOARD_BUG_TRIAGE_TYPE = Needs Triage, Prioridad: Alta, Prioridad: Baja, Cerrado
|
||||
|
||||
[ui]
|
||||
REACTIONS = +1, -1, laugh, hooray, confused, heart, rocket, eyes, oh_no
|
||||
CUSTOM_EMOJIS = gitea, codeberg, gitlab, git, github, gogs, oh_no
|
||||
|
||||
DEFAULT_SHOW_FULL_NAME = true
|
||||
|
||||
[ui.meta]
|
||||
AUTHOR = Nulo
|
||||
DESCRIPTION = ¡Acá hacemos software, y más!
|
||||
KEYWORDS = go,git,self-hosted,forgejo
|
||||
|
||||
[mailer]
|
||||
ENABLED = true
|
||||
;; Prefix displayed before subject in mail
|
||||
;SUBJECT_PREFIX =
|
||||
;;
|
||||
;; As per RFC 8314 using Implicit TLS/SMTPS on port 465 (if supported) is recommended,
|
||||
;; otherwise STARTTLS on port 587 should be used.
|
||||
SMTP_ADDR = mail.riseup.net
|
||||
SMTP_PORT = 465
|
||||
;;
|
||||
;; Mail from address, RFC 5322. This can be just an email address, or the '"Name" <email@example.com>' format
|
||||
FROM = Forgejo <giteanuloin@riseup.net>
|
||||
USER = catdevnull
|
||||
PASSWD = ${secrets.EMAIL_PASSWORD}
|
||||
PROTOCOL = smtps
|
||||
|
||||
[session]
|
||||
;; 7 días
|
||||
SESSION_LIFE_TIME = 604800
|
||||
|
||||
[time]
|
||||
DEFAULT_UI_LOCATION = America/Argentina/Buenos_Aires
|
||||
|
||||
[webhook]
|
||||
ALLOWED_HOST_LIST=external,loopback
|
||||
|
||||
[indexer]
|
||||
REPO_INDEXER_ENABLED=true
|
||||
REPO_INDEXER_EXCLUDE=**.mp4,**.jpg
|
||||
`,
|
||||
{ mode: 0o600 }
|
||||
);
|
||||
await sudoChown(configPath, `${entry.uid}:${entry.gid}`);
|
||||
await runit.addService(
|
||||
"forgejo",
|
||||
`#!/bin/sh
|
||||
|
||||
# USER and HOME are needed because forgejo doesn't actually check the user it
|
||||
# runs as, but instead just grabs the variables from the variables.
|
||||
export USER=_forgejo
|
||||
export HOME=/var/lib/forgejo
|
||||
|
||||
umask 0027
|
||||
|
||||
# forgejo needs to run from its home for SSH to work properly
|
||||
# TODO: check if this does anything
|
||||
export FORGEJO_WORK_DIR="$HOME"
|
||||
|
||||
cd "$HOME"
|
||||
|
||||
exec chpst -u $USER:$USER /usr/local/bin/forgejo web --config /etc/forgejo.conf 2>&1
|
||||
`
|
||||
);
|
||||
}
|
19
forgejo/secrets.test.ts
Normal file
19
forgejo/secrets.test.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import assert from "node:assert";
|
||||
import test from "node:test";
|
||||
import { generateForgejoSecrets } from "./secrets.js";
|
||||
|
||||
function somewhatValidJwt(input: string): void {
|
||||
assert.equal(input.startsWith("ey"), true);
|
||||
assert.equal(input.match(/\./g)?.length, 2);
|
||||
assert.equal(input.match("\n"), null);
|
||||
}
|
||||
|
||||
test("can generate secrets", async () => {
|
||||
const secrets = await generateForgejoSecrets();
|
||||
somewhatValidJwt(secrets.INTERNAL_TOKEN);
|
||||
// https://github.com/go-gitea/gitea/blob/e81ccc406bf723a5a58d685e7782f281736affd4/modules/generate/generate.go#L43
|
||||
assert.equal(Buffer.from(secrets.LFS_JWT_SECRET, "base64").byteLength, 32);
|
||||
assert.equal(Buffer.from(secrets.OAUTH_JWT_SECRET, "base64").byteLength, 32);
|
||||
// https://github.com/go-gitea/gitea/blob/e81ccc406bf723a5a58d685e7782f281736affd4/modules/generate/generate.go#L62
|
||||
assert.equal(secrets.SECRET_KEY.length, 64);
|
||||
});
|
44
forgejo/secrets.ts
Normal file
44
forgejo/secrets.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { execFile } from "../helpers/better-api.js";
|
||||
import { generateSecretsFile, loadSecretsFile } from "../helpers/secrets.js";
|
||||
import { buildForgejo } from "./build.js";
|
||||
|
||||
export interface ForgejoSecrets {
|
||||
SECRET_KEY: string;
|
||||
INTERNAL_TOKEN: string;
|
||||
LFS_JWT_SECRET: string;
|
||||
OAUTH_JWT_SECRET: string;
|
||||
EMAIL_PASSWORD: string;
|
||||
}
|
||||
|
||||
export const loadForgejoSecretsFile =
|
||||
loadSecretsFile<ForgejoSecrets>("forgejo");
|
||||
export const generateForgejoSecretsFile = generateSecretsFile(
|
||||
"forgejo",
|
||||
generateForgejoSecrets
|
||||
);
|
||||
export async function generateForgejoSecrets(): Promise<ForgejoSecrets> {
|
||||
const bin = await buildForgejo();
|
||||
console.info(
|
||||
"Reemplaza la contraseña de mail en secrets/forgejo.json, ¡porfa!"
|
||||
);
|
||||
return {
|
||||
...Object.fromEntries(
|
||||
await Promise.all([
|
||||
...["SECRET_KEY", "INTERNAL_TOKEN", "LFS_JWT_SECRET"].map(
|
||||
async (kind) => [kind, await genSecret(bin, kind as any)]
|
||||
),
|
||||
genSecret(bin, "JWT_SECRET").then((s) => ["OAUTH_JWT_SECRET", s]),
|
||||
])
|
||||
),
|
||||
EMAIL_PASSWORD: "REEMPLAZAR POR CONTRASEÑA",
|
||||
};
|
||||
}
|
||||
|
||||
async function genSecret(
|
||||
bin: string,
|
||||
kind: "INTERNAL_TOKEN" | "JWT_SECRET" | "LFS_JWT_SECRET" | "SECRET_KEY"
|
||||
): Promise<string> {
|
||||
// XXX: crosscompilation?
|
||||
const { stdout } = await execFile(bin, ["generate", "secret", kind]);
|
||||
return stdout;
|
||||
}
|
40
fstab.ts
Normal file
40
fstab.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { Alpine } from "./alpine.js";
|
||||
import { sudoChmod, sudoMkdirP, sudoWriteFile } from "./helpers/sudo.js";
|
||||
|
||||
export class Fstab {
|
||||
private alpine: Alpine;
|
||||
constructor(alpine: Alpine) {
|
||||
this.alpine = alpine;
|
||||
}
|
||||
|
||||
private mounts: string[] = ["tmpfs /tmp tmpfs defaults 0 0"];
|
||||
async addMount(mount: string) {
|
||||
this.mounts.push(mount);
|
||||
await this.write();
|
||||
}
|
||||
async addTmpfs(path: string, opts: TmpfsOptions = {}) {
|
||||
const add = Object.entries(opts)
|
||||
.map(([key, val]) => `,${key}=${val}`)
|
||||
.join("");
|
||||
await this.addMount(`tmpfs ${path} tmpfs defaults,noexec,nosuid${add} 0 0`);
|
||||
await sudoMkdirP(this.alpine.path(path));
|
||||
}
|
||||
|
||||
// Writes fstab to disk.
|
||||
// Intended for internal use only, use addMount or addTmpfs instead
|
||||
async write() {
|
||||
const path = this.alpine.path("/etc/fstab");
|
||||
await sudoWriteFile(
|
||||
path,
|
||||
this.mounts.join("\n") +
|
||||
// Busybox mount no entiende la última línea sino
|
||||
"\n"
|
||||
);
|
||||
await sudoChmod(path, "600");
|
||||
}
|
||||
}
|
||||
interface TmpfsOptions {
|
||||
uid?: number;
|
||||
gid?: number;
|
||||
mode?: string;
|
||||
}
|
37
helpers/passwd.ts
Normal file
37
helpers/passwd.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { sudoReadFile } from "./sudo.js";
|
||||
|
||||
export interface PasswdEntry {
|
||||
name: string;
|
||||
password: string;
|
||||
uid: number;
|
||||
gid: number;
|
||||
gecos: string;
|
||||
directory: string;
|
||||
shell: string;
|
||||
}
|
||||
export function parsePasswd(content: string): PasswdEntry[] {
|
||||
const lines = content.split("\n");
|
||||
return lines
|
||||
.filter((line) => line.length > 0)
|
||||
.map((line, index) => {
|
||||
const values = line.split(":");
|
||||
if (values.length !== 7)
|
||||
throw new Error(
|
||||
`La línea ${index + 1} no tiene la cantidad correcta de partes`
|
||||
);
|
||||
const entry: PasswdEntry = {
|
||||
name: values[0],
|
||||
password: values[1],
|
||||
uid: parseInt(values[2]),
|
||||
gid: parseInt(values[3]),
|
||||
gecos: values[4],
|
||||
directory: values[5],
|
||||
shell: values[6],
|
||||
};
|
||||
return entry;
|
||||
});
|
||||
}
|
||||
export async function sudoReadPasswd(path: string): Promise<PasswdEntry[]> {
|
||||
const content = await sudoReadFile(path);
|
||||
return parsePasswd(content);
|
||||
}
|
23
helpers/repro-run.ts
Normal file
23
helpers/repro-run.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { Writable } from "node:stream";
|
||||
import { execFile } from "./better-api.js";
|
||||
|
||||
export async function reproRun(opts: {
|
||||
// cwd stores the code available inside the container as /src, and also
|
||||
// cache/ and rootfs/
|
||||
cwd: string;
|
||||
command: string;
|
||||
cache: string[];
|
||||
}): Promise<void> {
|
||||
const run = execFile("repro-run", { cwd: opts.cwd });
|
||||
if (!run.child.stdin) throw false;
|
||||
run.child.stdin.write(
|
||||
JSON.stringify({
|
||||
Command: opts.command,
|
||||
Cache: opts.cache,
|
||||
})
|
||||
);
|
||||
run.child.stdin.end();
|
||||
run.child.stdout?.pipe(process.stdout);
|
||||
run.child.stderr?.pipe(process.stderr);
|
||||
await run;
|
||||
}
|
22
helpers/secrets.ts
Normal file
22
helpers/secrets.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
const secretsFileName = (name: string) => join("secrets", name + ".json");
|
||||
export function loadSecretsFile<T>(name: string): () => Promise<T> {
|
||||
return async () => {
|
||||
const file = await readFile(secretsFileName(name), "utf-8");
|
||||
return JSON.parse(file);
|
||||
};
|
||||
}
|
||||
export function generateSecretsFile<T>(
|
||||
name: string,
|
||||
generate: () => Promise<T>
|
||||
): () => Promise<void> {
|
||||
return async () => {
|
||||
const secrets = await generate();
|
||||
await mkdir("secrets", { recursive: true });
|
||||
await writeFile(secretsFileName(name), JSON.stringify(secrets, null, 2), {
|
||||
flag: "wx",
|
||||
});
|
||||
};
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, dirname, join } from "node:path";
|
||||
import { getuid } from "node:process";
|
||||
import { execFile } from "./better-api.js";
|
||||
|
||||
|
@ -9,6 +12,42 @@ export async function sudoChownToRunningUser(path: string): Promise<void> {
|
|||
await sudoChown(path, "" + getuid());
|
||||
} else throw new Error("No tengo getuid");
|
||||
}
|
||||
export async function sudoChmod(path: string, mod: string): Promise<void> {
|
||||
await execFile("sudo", ["chmod", mod, path]);
|
||||
}
|
||||
export async function sudoRm(path: string): Promise<void> {
|
||||
await execFile("sudo", ["rm", path]);
|
||||
}
|
||||
export async function sudoMkdirP(path: string): Promise<void> {
|
||||
await execFile("sudo", ["mkdir", "-p", path]);
|
||||
}
|
||||
export async function sudoCopy(input: string, target: string): Promise<void> {
|
||||
await execFile("sudo", ["cp", "--reflink=auto", input, target]);
|
||||
}
|
||||
export async function sudoReadFile(path: string): Promise<string> {
|
||||
const { stdout } = await execFile("sudo", ["cat", path]);
|
||||
return stdout;
|
||||
}
|
||||
export async function sudoWriteFile(
|
||||
filePath: string,
|
||||
content: string
|
||||
): Promise<void> {
|
||||
const dir = await mkdtemp(
|
||||
join(tmpdir(), "define-alpine-sudoWriteExecutable-")
|
||||
);
|
||||
try {
|
||||
const tmpFile = join(dir, basename(filePath));
|
||||
await writeFile(tmpFile, content);
|
||||
await sudoMkdirP(dirname(filePath));
|
||||
await execFile("sudo", ["mv", tmpFile, filePath]);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
export async function sudoWriteExecutable(
|
||||
filePath: string,
|
||||
content: string
|
||||
): Promise<void> {
|
||||
await sudoWriteFile(filePath, content);
|
||||
await sudoChmod(filePath, "700");
|
||||
}
|
||||
|
|
12
index.ts
12
index.ts
|
@ -1,14 +1,21 @@
|
|||
import { mkdir, mkdtemp } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { cwd } from "node:process";
|
||||
import { cwd, exit } from "node:process";
|
||||
import { Alpine } from "./alpine.js";
|
||||
import { setupForgejo } from "./forgejo/index.js";
|
||||
import { generateForgejoSecretsFile } from "./forgejo/secrets.js";
|
||||
import { execFile } from "./helpers/better-api.js";
|
||||
import { sudoChownToRunningUser } from "./helpers/sudo.js";
|
||||
import { setupKernel } from "./kernel.js";
|
||||
import { runQemu } from "./qemu.js";
|
||||
import { Runit } from "./runit/index.js";
|
||||
|
||||
if (process.argv[2] === "generate-secrets") {
|
||||
await generateForgejoSecretsFile();
|
||||
exit(0);
|
||||
}
|
||||
|
||||
{
|
||||
console.time("Building");
|
||||
|
||||
|
@ -20,7 +27,10 @@ import { Runit } from "./runit/index.js";
|
|||
const rootfsDir = await mkdtemp(path.join(tmpdir(), "define-alpine-"));
|
||||
console.debug(rootfsDir);
|
||||
const alpine = await Alpine.makeWorld({ dir: rootfsDir });
|
||||
|
||||
await alpine.addPackages(["helix", "iproute2-ss", "socat"]);
|
||||
const runit = await Runit.setup(alpine);
|
||||
await setupForgejo(alpine, runit);
|
||||
const kernel = await setupKernel(alpine, kernelDir);
|
||||
|
||||
const squashfs = path.join(artifactsDir, "image.squashfs");
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"run": "esbuild --log-level=warning --target=node18 --sourcemap --outdir=build-javascript --outbase=. *.ts **/*.ts && node --enable-source-maps build-javascript/index.js",
|
||||
"build": "esbuild --log-level=warning --target=node18 --sourcemap --outdir=build-javascript --outbase=. *.ts **/*.ts",
|
||||
"run": "pnpm build && node --enable-source-maps build-javascript/index.js",
|
||||
"test": "pnpm build && node --enable-source-maps build-javascript/**/*.test.js",
|
||||
"tsc:check": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [],
|
||||
|
|
Loading…
Reference in a new issue