commit 5d4f3c096518cb4013c188c62d8813e2e26d8f39 Author: Noah Holt Date: Mon Aug 10 21:44:23 2026 -0500 Initial commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c24ff1a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +node_modules +dist +.git +.forgejo diff --git a/.forgejo/workflows/ci.yaml b/.forgejo/workflows/ci.yaml new file mode 100644 index 0000000..0847f7c --- /dev/null +++ b/.forgejo/workflows/ci.yaml @@ -0,0 +1,16 @@ +# Full pipeline: checks -> build+push image -> bump tag in gitops repo. +# The heavy lifting lives in nholtware/workflows; this file is all a new +# app needs. +on: + push: + branches: [main] + +jobs: + checks: + uses: nholtware/workflows/.forgejo/workflows/node-checks.yaml@main + deploy: + needs: checks + uses: nholtware/workflows/.forgejo/workflows/build-and-deploy.yaml@main + with: + app-name: demo-app + secrets: inherit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8b5bf16 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +# Build stage: full toolchain, compile TS -> dist/ +FROM node:22 AS build +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY tsconfig.json ./ +COPY src ./src +RUN npm run build + +# Runtime stage: no dev deps (this service has zero runtime deps) +FROM node:22-slim +WORKDIR /app +ENV NODE_ENV=production +ARG GIT_SHA=dev +ENV GIT_SHA=${GIT_SHA} +COPY --from=build /app/dist ./dist +COPY package.json ./ +USER node +EXPOSE 3000 +CMD ["node", "dist/index.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..6055316 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# demo-app + +Minimal TypeScript microservice proving out the homelab CI/CD pipeline: + +``` +push to main + -> Forgejo Actions (nholtware/workflows reusable workflows) + checks: npm ci + lint + test (docker label) + build: docker build + push -> 192.168.1.230:3000/nholtware/demo-app: + deploy: kustomize edit set image -> commit to nholtware/gitops + -> ArgoCD syncs gitops -> k3s + -> served by Traefik at Host: demo-app.k3s.local +``` + +## Endpoints + +- `GET /` — JSON with service name, git sha, pod hostname +- `GET /healthz` — liveness/readiness probe target + +## Local dev + +```sh +npm ci +npm test +npm run build && npm start # listens on :3000 (PORT to override) +``` diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7f632d8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,50 @@ +{ + "name": "demo-app", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "demo-app", + "version": "1.0.0", + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.6.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..257280a --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "demo-app", + "version": "1.0.0", + "description": "TypeScript microservice demonstrating the Forgejo Actions -> registry -> ArgoCD -> k3s pipeline", + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "test": "npm run build && node --test dist/index.test.js", + "lint": "tsc --noEmit" + }, + "engines": { + "node": ">=22" + }, + "devDependencies": { + "typescript": "^5.6.0", + "@types/node": "^22.0.0" + } +} diff --git a/src/greeting.ts b/src/greeting.ts new file mode 100644 index 0000000..592b023 --- /dev/null +++ b/src/greeting.ts @@ -0,0 +1,3 @@ +export function greeting(): string { + return "Hello from the homelab pipeline!"; +} diff --git a/src/index.test.ts b/src/index.test.ts new file mode 100644 index 0000000..c5ed6e8 --- /dev/null +++ b/src/index.test.ts @@ -0,0 +1,7 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { greeting } from "./greeting.js"; + +test("greeting mentions the homelab", () => { + assert.match(greeting(), /homelab/); +}); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..02261e2 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,32 @@ +import { createServer } from "node:http"; +import { hostname } from "node:os"; +import { greeting } from "./greeting.js"; + +const port = Number(process.env.PORT ?? 3000); +const sha = process.env.GIT_SHA ?? "dev"; + +const server = createServer((req, res) => { + if (req.url === "/healthz") { + res.writeHead(200, { "content-type": "text/plain" }); + res.end("ok"); + return; + } + res.writeHead(200, { "content-type": "application/json" }); + res.end( + JSON.stringify( + { + service: "demo-app", + message: greeting(), + sha, + pod: hostname(), + time: new Date().toISOString(), + }, + null, + 2, + ) + "\n", + ); +}); + +server.listen(port, () => { + console.log(`demo-app ${sha} listening on :${port}`); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..59b02f6 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src"] +}