#!/usr/bin/env node import { createHash } from "node:crypto"; import { spawnSync } from "node:child_process"; import { cp, lstat, mkdir, mkdtemp, readFile, rm, writeFile, } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const scriptDir = dirname(fileURLToPath(import.meta.url)); const fixtureRoot = resolve(scriptDir, "fixtures/gitea"); const artifactDir = resolve( process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(scriptDir, "../deploy-artifacts"), ); const [patchId = "gitea-fresh-install-20260813-001", ...extra] = process.argv.slice(2); if (extra.length || !/^[A-Za-z0-9._-]{1,96}$/.test(patchId)) { throw new Error("usage: build-gitea-fresh-install-artifact.mjs [patch-id]"); } const composeRelative = "docker-compose.gitea.yml"; const descriptorRelative = "deployment/gitea-fresh-install-v1.json"; const files = [composeRelative, descriptorRelative]; const stage = await mkdtemp(join(tmpdir(), "nodedc-gitea-artifact-")); const payload = join(stage, "payload"); const target = join(artifactDir, `nodedc-gitea-${patchId}.tgz`); await assertFixtureContract(); try { await mkdir(payload, { recursive: true }); for (const relative of files) { const source = resolve(fixtureRoot, relative); const sourceStat = await lstat(source); if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) { throw new Error(`gitea_fixture_type_rejected:${relative}`); } const destination = join(payload, relative); await mkdir(dirname(destination), { recursive: true }); await cp(source, destination, { force: true, verbatimSymlinks: true }); } await writeFile( join(stage, "manifest.env"), `id=${patchId}\ncomponent=gitea\ntype=app-overlay\n`, "utf8", ); await writeFile(join(stage, "files.txt"), `${files.join("\n")}\n`, "utf8"); await mkdir(artifactDir, { recursive: true }); const tar = spawnSync( "python3", ["-c", canonicalTarScript(), target, stage], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }, ); if (tar.status !== 0) { throw new Error(`tar_failed:${tar.stderr || tar.stdout}`); } const digest = sha256(await readFile(target)); console.log(JSON.stringify({ ok: true, patchId, artifact: target, sha256: digest, component: "gitea", entries: files, services: ["gitea"], image: "docker.gitea.com/gitea:1.27.1-rootless@sha256:89dc3c214b3992e5bb01e05ad21139d7a8b302d3ea3d8942d3f7e904e92af148", installMode: "fresh-only", database: "fresh-sqlite-only", lfs: "disabled-pending-reviewed-restore-transition", transport: "unix:/run/gitea/gitea.sock", networkMode: "none", minimumComposeVersion: "2.20.1", preserved: ["legacy-gitea-root-unread-and-untouched"], excluded: [ "secrets", "runtime-data", "database", "repositories", "users", "tokens", "ssh-keys", "hooks", ], }, null, 2)); } finally { await rm(stage, { recursive: true, force: true }); } async function assertFixtureContract() { const composeBytes = await readFile(resolve(fixtureRoot, composeRelative)); const compose = composeBytes.toString("utf8"); const descriptor = JSON.parse( await readFile(resolve(fixtureRoot, descriptorRelative), "utf8"), ); const expectedImage = "docker.gitea.com/gitea:1.27.1-rootless@sha256:89dc3c214b3992e5bb01e05ad21139d7a8b302d3ea3d8942d3f7e904e92af148"; const required = [ `image: ${expectedImage}`, "platform: linux/amd64", "pull_policy: never", "network_mode: none", 'user: "1000:1000"', "stop_grace_period: 30s", "driver: json-file", 'max-size: "10m"', 'max-file: "3"', "GITEA__server__PROTOCOL: http+unix", "GITEA__server__HTTP_ADDR: /run/gitea/gitea.sock", 'GITEA__server__UNIX_SOCKET_PERMISSION: "0666"', "GITEA__server__LOCAL_ROOT_URL: http://unix/", 'GITEA__server__DISABLE_SSH: "true"', 'GITEA__server__LFS_START_SERVER: "false"', 'GITEA__server__LFS_ALLOW_PURE_SSH: "false"', "GITEA__security__SECRET_KEY_URI: file:/run/secrets/gitea_secret_key", "GITEA__security__INTERNAL_TOKEN_URI: file:/run/secrets/gitea_internal_token", "GITEA__security__TWO_FACTOR_AUTH: enforced", 'GITEA__security__REVERSE_PROXY_LIMIT: "1"', "GITEA__security__ALLOWED_HOST_LIST: loopback", "GITEA__security__REVERSE_PROXY_TRUSTED_PROXIES: 127.0.0.0/8,::1/128", 'GITEA__service__DISABLE_REGISTRATION: "true"', 'GITEA__service__ENABLE_REVERSE_PROXY_AUTHENTICATION: "false"', 'GITEA__service__ENABLE_REVERSE_PROXY_AUTHENTICATION_API: "false"', 'GITEA__service__ENABLE_REVERSE_PROXY_AUTO_REGISTRATION: "false"', 'GITEA__service__ENABLE_BASIC_AUTHENTICATION: "false"', 'GITEA__admin__DISABLE_REGULAR_ORG_CREATION: "true"', "GITEA__admin__USER_DISABLED_FEATURES: deletion,manage_ssh_keys,manage_gpg_keys,change_username", 'GITEA__security__DISABLE_GIT_HOOKS: "true"', 'GITEA__security__DISABLE_WEBHOOKS: "true"', 'GITEA__repository__DISABLE_MIGRATIONS: "true"', 'GITEA__packages__ENABLED: "false"', 'GITEA__oauth2__ENABLED: "false"', 'GITEA__openid__ENABLE_OPENID_SIGNIN: "false"', 'GITEA__cron.update_checker__ENABLED: "false"', "source: /volume1/docker/nodedc-gitea/socket", "target: /run/gitea", "create_host_path: false", "read_only: true", "no-new-privileges:true", ]; for (const fragment of required) { if (!compose.includes(fragment)) { throw new Error(`gitea_compose_boundary_missing:${fragment}`); } } for (const forbidden of [ "4022", "2222:2222", "0.0.0.0:3000", "ports:", "networks:", "/var/run/docker.sock", "/volume1/docker/gitea", "privileged: true", "pull_policy: always", "__FILE", "GITEA__security__SECRET_KEY:", "GITEA__security__INTERNAL_TOKEN:", "GITEA__server__LFS_JWT_SECRET:", "GITEA__server__LFS_JWT_SECRET_URI", "gitea_lfs_jwt_secret", "lfs-jwt-secret", "GITEA__server__REVERSE_PROXY_LIMIT", "GITEA__server__REVERSE_PROXY_TRUSTED_PROXIES", "GITEA__security__ENABLE_REVERSE_PROXY_AUTHENTICATION", "GITEA__security__ENABLE_REVERSE_PROXY_AUTHENTICATION_API", "GITEA__security__ENABLE_REVERSE_PROXY_AUTO_REGISTRATION", "GITEA__service__DISABLE_REGULAR_ORG_CREATION", "GITEA__service__USER_DISABLED_FEATURES", ]) { if (compose.includes(forbidden)) { throw new Error(`gitea_compose_boundary_violation:${forbidden}`); } } if ( descriptor.schemaVersion !== "nodedc.gitea.fresh-install.v1" || descriptor.action !== "fresh-install" || descriptor.component !== "gitea" || descriptor.compose?.sha256 !== sha256(composeBytes) || descriptor.runtime?.image !== expectedImage || descriptor.runtime?.minimumComposeVersion !== "2.20.1" || descriptor.runtime?.lfs !== "disabled-pending-reviewed-restore-transition" || descriptor.runtime?.transport !== "unix:/run/gitea/gitea.sock" || descriptor.runtime?.networkMode !== "none" || descriptor.runtime?.logging !== "bounded-json-file-10m-x3" || descriptor.runtime?.stopGracePeriod !== "30s" || descriptor.trust?.artifactSecrets !== "forbidden" || descriptor.trust?.legacyRootAccess !== "forbidden" ) { throw new Error("gitea_descriptor_contract_mismatch"); } } function sha256(bytes) { return createHash("sha256").update(bytes).digest("hex"); } function canonicalTarScript() { return [ "import gzip,io,pathlib,sys,tarfile", "root=pathlib.Path(sys.argv[2])", "with open(sys.argv[1],'wb') as out:", " with gzip.GzipFile(filename='',mode='wb',fileobj=out,compresslevel=9,mtime=0) as gz:", " with tarfile.open(fileobj=gz,mode='w',format=tarfile.PAX_FORMAT) as tar:", " for top in ('manifest.env','files.txt','payload'):", " p=root/top; paths=[p]+(sorted(p.rglob('*')) if p.is_dir() else [])", " for x in paths:", " info=tar.gettarinfo(str(x),arcname=x.relative_to(root).as_posix())", " info.uid=info.gid=0; info.uname=info.gname='root'; info.mtime=0; info.mode=0o755 if info.isdir() else 0o644", " with (open(x,'rb') if info.isfile() else io.BytesIO()) as src: tar.addfile(info,src if info.isfile() else None)", ].join("\n"); }