From 15ceeb5f9d86eed470ee5f49cdb3da92a1a3ce1a Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 7 Mar 2026 20:28:51 -0800 Subject: [PATCH 01/20] =?UTF-8?q?C2(jobsync):=20plan=20=E2=80=94=20Mikado?= =?UTF-8?q?=20cards=20for=20JobSync=20deployment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cards: - deploy-jobsync (goal): Deploy JobSync to ringtail k3s via ArgoCD - build-jobsync-container: Nix container build (buildLayeredImage) - mirror-jobsync: Mirror upstream to forge - integrate-jobsync-ollama: Wire up existing Ollama for AI features Co-Authored-By: Claude Opus 4.6 --- docs/how-to/how-to.md | 7 +++ .../how-to/jobsync/build-jobsync-container.md | 41 ++++++++++++++++ docs/how-to/jobsync/deploy-jobsync.md | 49 +++++++++++++++++++ .../jobsync/integrate-jobsync-ollama.md | 45 +++++++++++++++++ docs/how-to/jobsync/mirror-jobsync.md | 27 ++++++++++ 5 files changed, 169 insertions(+) create mode 100644 docs/how-to/jobsync/build-jobsync-container.md create mode 100644 docs/how-to/jobsync/deploy-jobsync.md create mode 100644 docs/how-to/jobsync/integrate-jobsync-ollama.md create mode 100644 docs/how-to/jobsync/mirror-jobsync.md diff --git a/docs/how-to/how-to.md b/docs/how-to/how-to.md index a9e096a..5f1bd4f 100644 --- a/docs/how-to/how-to.md +++ b/docs/how-to/how-to.md @@ -88,6 +88,13 @@ tags: - [[upgrade-dagger]] +## JobSync + +- [[deploy-jobsync]] +- [[build-jobsync-container]] +- [[mirror-jobsync]] +- [[integrate-jobsync-ollama]] + ## Forgejo Runner - [[upgrade-k8s-runner]] diff --git a/docs/how-to/jobsync/build-jobsync-container.md b/docs/how-to/jobsync/build-jobsync-container.md new file mode 100644 index 0000000..0e5ba77 --- /dev/null +++ b/docs/how-to/jobsync/build-jobsync-container.md @@ -0,0 +1,41 @@ +--- +title: Build JobSync Container +modified: 2026-03-07 +status: active +requires: + - mirror-jobsync +tags: + - how-to + - jobsync +--- + +# Build JobSync Container + +Build a nix container image for JobSync using `dockerTools.buildLayeredImage`, following the ntfy pattern. + +## Context + +JobSync is a Next.js standalone app with Prisma (SQLite). The nix build needs to: + +1. Fetch source from `forge.ops.eblu.me/mirrors/jobsync` (v1.1.4) +2. `buildNpmPackage` — install deps, run `prisma generate`, run `next build` +3. Package the standalone output with `nodejs` runtime into a layered image +4. Include an entrypoint that runs `prisma migrate deploy` before `node server.js` + +## Key Details + +- **Runtime dependency:** `nodejs_20` must be in the image (unlike Go apps that compile to static binaries) +- **Prisma native engine:** `prisma generate` produces a platform-specific query engine binary; the nix build targets `linux-x86_64` for ringtail +- **`npmDepsHash`:** Will need to be computed on first build (set to empty, let it fail, grab the hash) +- **Standalone output:** Next.js `output: "standalone"` produces a self-contained `server.js` with minimal `node_modules` + +## Files + +- `containers/jobsync/default.nix` — nix derivation +- `containers/jobsync/entrypoint.sh` — startup script (migrations + server) + +## Related + +- [[mirror-jobsync]] +- [[deploy-jobsync]] +- [[build-container-image]] diff --git a/docs/how-to/jobsync/deploy-jobsync.md b/docs/how-to/jobsync/deploy-jobsync.md new file mode 100644 index 0000000..b5bc39c --- /dev/null +++ b/docs/how-to/jobsync/deploy-jobsync.md @@ -0,0 +1,49 @@ +--- +title: Deploy JobSync +modified: 2026-03-07 +status: active +branch: mikado/jobsync +requires: + - build-jobsync-container + - integrate-jobsync-ollama +tags: + - how-to + - jobsync +--- + +# Deploy JobSync + +Deploy [JobSync](https://github.com/Gsync/jobsync) — a self-hosted job application tracker — to ringtail's k3s cluster via ArgoCD. + +## Context + +JobSync is a Next.js app with SQLite storage that provides job application tracking, resume management, and AI-powered resume review/job matching. It runs as a single container with persistent storage at `/data` (SQLite DB + uploaded files). + +## What This Card Covers + +With the container built and Ollama integration configured, this card wires up the deployment: + +- ArgoCD Application targeting `ringtail.tail8d86e.ts.net:6443` +- k8s manifests: Deployment, Service, Tailscale Ingress, PVC, ExternalSecret +- PVC using k3s local-path for `/data` (SQLite + resume uploads) +- ExternalSecret for `ENCRYPTION_KEY` and `AUTH_SECRET` from 1Password +- Caddy route: `jobsync.ops.eblu.me` → Tailscale ingress +- Service documentation + +## Environment Variables + +| Variable | Source | Purpose | +|----------|--------|---------| +| `DATABASE_URL` | Hardcoded | `file:/data/dev.db` | +| `AUTH_SECRET` | ExternalSecret | NextAuth session signing | +| `ENCRYPTION_KEY` | ExternalSecret | AES-256-GCM for stored API keys | +| `NEXTAUTH_URL` | Hardcoded | `https://jobsync.ops.eblu.me` | +| `AUTH_TRUST_HOST` | Hardcoded | `true` | +| `TZ` | Hardcoded | `America/Los_Angeles` | +| `OLLAMA_BASE_URL` | Hardcoded | `http://ollama.ollama.svc.cluster.local:11434` | + +## Related + +- [[build-jobsync-container]] +- [[integrate-jobsync-ollama]] +- [[deploy-k8s-service]] diff --git a/docs/how-to/jobsync/integrate-jobsync-ollama.md b/docs/how-to/jobsync/integrate-jobsync-ollama.md new file mode 100644 index 0000000..880c160 --- /dev/null +++ b/docs/how-to/jobsync/integrate-jobsync-ollama.md @@ -0,0 +1,45 @@ +--- +title: Integrate JobSync with Ollama +modified: 2026-03-07 +status: active +tags: + - how-to + - jobsync +--- + +# Integrate JobSync with Ollama + +Configure JobSync to use the existing Ollama deployment on ringtail for AI features (resume review, job matching). + +## Context + +Ollama already runs on ringtail's k3s cluster at `ollama.ollama.svc.cluster.local:11434` with several models available (qwen2.5:14b, deepseek-r1:14b, phi4:14b, gemma3:12b, qwen3.5:9b). + +JobSync supports Ollama natively via `OLLAMA_BASE_URL`. Since both services run on the same k3s cluster, this is a cluster-internal connection with no auth required. + +## Configuration + +Set in the JobSync deployment manifest: + +```yaml +env: + - name: OLLAMA_BASE_URL + value: "http://ollama.ollama.svc.cluster.local:11434" +``` + +## Verification + +After deployment: + +1. Open JobSync settings +2. Select Ollama as the AI provider +3. Verify model list populates from the Ollama instance +4. Test resume review with one of the available models + +## Model Recommendations + +For resume review and job matching, `qwen2.5:14b` or `gemma3:12b` are good choices — capable enough for structured text analysis without being overly slow on the RTX 4080. + +## Related + +- [[deploy-jobsync]] diff --git a/docs/how-to/jobsync/mirror-jobsync.md b/docs/how-to/jobsync/mirror-jobsync.md new file mode 100644 index 0000000..494f344 --- /dev/null +++ b/docs/how-to/jobsync/mirror-jobsync.md @@ -0,0 +1,27 @@ +--- +title: Mirror JobSync +modified: 2026-03-07 +status: active +tags: + - how-to + - jobsync +--- + +# Mirror JobSync + +Mirror the upstream [JobSync](https://github.com/Gsync/jobsync) repository to `forge.ops.eblu.me/mirrors/jobsync` for supply chain control. + +## Context + +JobSync is not in nixpkgs. Building a nix container requires fetching source from a controlled mirror on forge, following the same pattern as [[mirror-authentik-build-deps]] and the ntfy mirror. + +## Steps + +1. Create the mirror: `mise run mirror-create jobsync https://github.com/Gsync/jobsync.git` +2. Verify the mirror syncs: check `forge.ops.eblu.me/mirrors/jobsync` +3. Note the tag for v1.1.4 (current upstream release) + +## Related + +- [[build-jobsync-container]] +- [[manage-forgejo-mirrors]] -- 2.50.1 (Apple Git-155) From 60507ee7191335d636f7092419b2fb9e8e009730 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 7 Mar 2026 22:00:03 -0800 Subject: [PATCH 02/20] =?UTF-8?q?C2(jobsync):=20plan=20=E2=80=94=20update?= =?UTF-8?q?=20cards=20with=20learnings=20from=20first=20attempt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build-jobsync-container: document prisma devDep pruning pitfall, nix entrypoint path issue, and verification step. deploy-jobsync: document service-versions.yaml requirement, image tag format, and 1Password item already created. Co-Authored-By: Claude Opus 4.6 --- .../how-to/jobsync/build-jobsync-container.md | 22 +++++++++++++++++++ docs/how-to/jobsync/deploy-jobsync.md | 6 +++++ 2 files changed, 28 insertions(+) diff --git a/docs/how-to/jobsync/build-jobsync-container.md b/docs/how-to/jobsync/build-jobsync-container.md index 0e5ba77..e0b483b 100644 --- a/docs/how-to/jobsync/build-jobsync-container.md +++ b/docs/how-to/jobsync/build-jobsync-container.md @@ -29,6 +29,28 @@ JobSync is a Next.js standalone app with Prisma (SQLite). The nix build needs to - **`npmDepsHash`:** Will need to be computed on first build (set to empty, let it fail, grab the hash) - **Standalone output:** Next.js `output: "standalone"` produces a self-contained `server.js` with minimal `node_modules` +## Nix Container Pitfalls (learned from first attempt) + +### Prisma devDependency pruning + +`buildNpmPackage` runs `npm prune --omit=dev` between `buildPhase` and `installPhase`. The `prisma` CLI and `@prisma/engines` are devDependencies, so they get removed. But they're needed at runtime for `prisma migrate deploy` in the entrypoint. + +**Fix:** Save prisma packages to a temp directory during `postBuild` (before prune), then copy them into the output during `installPhase`. + +### Entrypoint filesystem paths + +Nix containers have no `/app` directory. The app lives at `/nix/store//app/` and the container's `WorkingDir` is set to that path. The entrypoint must not `cd /app` — it should rely on the `WorkingDir` set in the container config. + +### Verification + +The container must be tested before deployment. After building, verify: + +```sh +# Run a debug pod with the new image: +kubectl run debug --image= --restart=Never --command -- \ + sh -c 'ls node_modules/@prisma/engines/ && node -e "require(\"@prisma/engines\")"' +``` + ## Files - `containers/jobsync/default.nix` — nix derivation diff --git a/docs/how-to/jobsync/deploy-jobsync.md b/docs/how-to/jobsync/deploy-jobsync.md index b5bc39c..f2d9d05 100644 --- a/docs/how-to/jobsync/deploy-jobsync.md +++ b/docs/how-to/jobsync/deploy-jobsync.md @@ -42,6 +42,12 @@ With the container built and Ollama integration configured, this card wires up t | `TZ` | Hardcoded | `America/Los_Angeles` | | `OLLAMA_BASE_URL` | Hardcoded | `http://ollama.ollama.svc.cluster.local:11434` | +## Deployment Notes (learned from first attempt) + +- **`service-versions.yaml`:** Add a `jobsync` entry before committing container changes — the `container-version-check` pre-commit hook rejects commits touching `containers//` without a matching entry. +- **Image tag format:** `container-build-and-release` produces tags like `v1.1.4--nix`, not bare `v1.1.4`. Set `newTag` in `kustomization.yaml` to the full tag from `mise run container-list`. +- **1Password item:** "JobSync" in blumeops vault, with `auth_secret` and `encryption_key` fields (already created). + ## Related - [[build-jobsync-container]] -- 2.50.1 (Apple Git-155) From 92f0b190b8f0ddd37ba4909105a3a06d1c57e438 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 7 Mar 2026 22:00:57 -0800 Subject: [PATCH 03/20] C2(jobsync): close mirror-jobsync Mirror already exists at forge.ops.eblu.me/mirrors/jobsync from previous cycle. Co-Authored-By: Claude Opus 4.6 --- docs/how-to/jobsync/mirror-jobsync.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/how-to/jobsync/mirror-jobsync.md b/docs/how-to/jobsync/mirror-jobsync.md index 494f344..46a30cc 100644 --- a/docs/how-to/jobsync/mirror-jobsync.md +++ b/docs/how-to/jobsync/mirror-jobsync.md @@ -1,7 +1,6 @@ --- title: Mirror JobSync modified: 2026-03-07 -status: active tags: - how-to - jobsync -- 2.50.1 (Apple Git-155) From 874a967000480ce82618f91474bede6b2d687565 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 7 Mar 2026 22:08:38 -0800 Subject: [PATCH 04/20] =?UTF-8?q?C2(jobsync):=20impl=20=E2=80=94=20nix=20c?= =?UTF-8?q?ontainer=20derivation=20and=20entrypoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add containers/jobsync/default.nix (buildNpmPackage + dockerTools) and entrypoint.sh (prisma migrate + node server.js). Hashes are empty placeholders — will be filled from first build attempt on ringtail. Co-Authored-By: Claude Opus 4.6 --- containers/jobsync/default.nix | 112 +++++++++++++++++++++++++++++++ containers/jobsync/entrypoint.sh | 15 +++++ service-versions.yaml | 7 ++ 3 files changed, 134 insertions(+) create mode 100644 containers/jobsync/default.nix create mode 100644 containers/jobsync/entrypoint.sh diff --git a/containers/jobsync/default.nix b/containers/jobsync/default.nix new file mode 100644 index 0000000..a578009 --- /dev/null +++ b/containers/jobsync/default.nix @@ -0,0 +1,112 @@ +# Nix-built JobSync container +# Next.js job application tracker with Prisma/SQLite +# Built with dockerTools.buildLayeredImage for efficient layer caching +{ pkgs ? import { } }: + +let + version = "1.1.4"; + + src = pkgs.fetchgit { + url = "https://forge.ops.eblu.me/mirrors/jobsync.git"; + rev = "v${version}"; + hash = ""; + }; + + jobsync = pkgs.buildNpmPackage { + inherit src version; + pname = "jobsync"; + npmDepsHash = ""; + + nodejs = pkgs.nodejs_20; + + # Prisma needs to find the SQLite engine for the target platform + env = { + PRISMA_ENGINES_MIRROR = "https://binaries.prisma.sh"; + DATABASE_URL = "file:/tmp/build.db"; + NEXT_TELEMETRY_DISABLED = "1"; + }; + + buildPhase = '' + runHook preBuild + + # Generate Prisma client + npx prisma generate + + # Build Next.js + npm run build + + runHook postBuild + ''; + + # Save prisma packages before npm prune removes them (they're devDeps) + postBuild = '' + mkdir -p /tmp/prisma-save + cp -r node_modules/.prisma /tmp/prisma-save/ || true + cp -r node_modules/@prisma /tmp/prisma-save/ || true + cp -r node_modules/prisma /tmp/prisma-save/ || true + ''; + + installPhase = '' + runHook preInstall + + mkdir -p $out/app + + # Copy Next.js standalone output + cp -r .next/standalone/. $out/app/ + cp -r .next/static $out/app/.next/static + cp -r public $out/app/public + + # Copy Prisma schema and migrations for runtime migrate deploy + cp -r prisma $out/app/prisma + + # Restore prisma packages needed at runtime + cp -r /tmp/prisma-save/.prisma $out/app/node_modules/.prisma || true + cp -r /tmp/prisma-save/@prisma $out/app/node_modules/@prisma || true + cp -r /tmp/prisma-save/prisma $out/app/node_modules/prisma || true + + # Copy entrypoint + cp ${./entrypoint.sh} $out/app/entrypoint.sh + + runHook postInstall + ''; + + dontNpmBuild = true; + }; + + entrypoint = pkgs.writeShellScript "jobsync-entrypoint" '' + cd ${jobsync}/app + exec ${pkgs.bash}/bin/bash entrypoint.sh "$@" + ''; +in + +pkgs.dockerTools.buildLayeredImage { + name = "blumeops/jobsync"; + tag = "latest"; + + contents = [ + jobsync + pkgs.nodejs_20 + pkgs.cacert + pkgs.tzdata + pkgs.bash + pkgs.coreutils + ]; + + config = { + Entrypoint = [ "${entrypoint}" ]; + WorkingDir = "${jobsync}/app"; + Env = [ + "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" + "TZDIR=${pkgs.tzdata}/share/zoneinfo" + "NODE_ENV=production" + "PORT=3000" + "DATABASE_URL=file:/data/dev.db" + ]; + ExposedPorts = { + "3000/tcp" = { }; + }; + Volumes = { + "/data" = { }; + }; + }; +} diff --git a/containers/jobsync/entrypoint.sh b/containers/jobsync/entrypoint.sh new file mode 100644 index 0000000..c0dca24 --- /dev/null +++ b/containers/jobsync/entrypoint.sh @@ -0,0 +1,15 @@ +#!/bin/sh +set -e + +# Auto-generate AUTH_SECRET if not provided +if [ -z "$AUTH_SECRET" ]; then + AUTH_SECRET="$(node -e "console.log(require('crypto').randomBytes(32).toString('base64'))")" + export AUTH_SECRET + echo "AUTH_SECRET was not set — generated a temporary secret for this container." +fi + +# Run Prisma migrations +npx prisma migrate deploy + +# Start the Next.js server +exec node server.js diff --git a/service-versions.yaml b/service-versions.yaml index 3c75f49..2bf9419 100644 --- a/service-versions.yaml +++ b/service-versions.yaml @@ -155,6 +155,13 @@ services: current-version: "2026.2.0" upstream-source: https://github.com/goauthentik/authentik/releases + - name: jobsync + type: argocd + last-reviewed: null + current-version: "1.1.4" + upstream-source: https://github.com/Gsync/jobsync/releases + notes: Job application tracker; nix container on ringtail k3s + - name: ollama type: argocd last-reviewed: "2026-03-02" -- 2.50.1 (Apple Git-155) From b1616bc96be9288a4143c5e6ee65497ab18d1c1c Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 7 Mar 2026 22:28:31 -0800 Subject: [PATCH 05/20] =?UTF-8?q?C2(jobsync):=20impl=20=E2=80=94=20fix=20n?= =?UTF-8?q?ix=20derivation=20for=20ringtail=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use nixpkgs prisma-engines to avoid network downloads in nix sandbox. Patch out Google Fonts import (Inter) since sandbox blocks network; falls back to system sans-serif font stack. Co-Authored-By: Claude Opus 4.6 --- containers/jobsync/default.nix | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/containers/jobsync/default.nix b/containers/jobsync/default.nix index a578009..3000642 100644 --- a/containers/jobsync/default.nix +++ b/containers/jobsync/default.nix @@ -6,22 +6,41 @@ let version = "1.1.4"; + prismaEngines = pkgs.prisma-engines; + src = pkgs.fetchgit { url = "https://forge.ops.eblu.me/mirrors/jobsync.git"; rev = "v${version}"; - hash = ""; + hash = "sha256-59W5OF36yD67jEK5xa9jSL4EVN9RG+Ez/w9Mq2VykSA="; }; jobsync = pkgs.buildNpmPackage { inherit src version; pname = "jobsync"; - npmDepsHash = ""; + npmDepsHash = "sha256-yRNOxtz66qSlmfjR3QDPUQe0C8sdg06tBbuK1Ws1gEA="; nodejs = pkgs.nodejs_20; - # Prisma needs to find the SQLite engine for the target platform + nativeBuildInputs = [ pkgs.makeWrapper ]; + + # Patch out Google Fonts import (nix sandbox blocks network access). + # Replace with a simple object that provides the CSS variable class name. + postPatch = '' + substituteInPlace src/app/layout.tsx \ + --replace-fail 'import { Inter } from "next/font/google";' "" \ + --replace-fail 'const inter = Inter({ + subsets: ["latin"], + variable: "--font-inter", +});' 'const inter = { variable: "" };' + ''; + + # Point Prisma at nixpkgs-built engines (no network download needed) env = { - PRISMA_ENGINES_MIRROR = "https://binaries.prisma.sh"; + PRISMA_QUERY_ENGINE_LIBRARY = "${prismaEngines}/lib/libquery_engine.node"; + PRISMA_QUERY_ENGINE_BINARY = "${prismaEngines}/bin/query-engine"; + PRISMA_SCHEMA_ENGINE_BINARY = "${prismaEngines}/bin/schema-engine"; + PRISMA_FMT_BINARY = "${prismaEngines}/bin/prisma-fmt"; + PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING = "1"; DATABASE_URL = "file:/tmp/build.db"; NEXT_TELEMETRY_DISABLED = "1"; }; @@ -29,7 +48,7 @@ let buildPhase = '' runHook preBuild - # Generate Prisma client + # Generate Prisma client using nixpkgs engines npx prisma generate # Build Next.js @@ -85,6 +104,7 @@ pkgs.dockerTools.buildLayeredImage { contents = [ jobsync + prismaEngines pkgs.nodejs_20 pkgs.cacert pkgs.tzdata @@ -101,6 +121,9 @@ pkgs.dockerTools.buildLayeredImage { "NODE_ENV=production" "PORT=3000" "DATABASE_URL=file:/data/dev.db" + "PRISMA_QUERY_ENGINE_LIBRARY=${prismaEngines}/lib/libquery_engine.node" + "PRISMA_SCHEMA_ENGINE_BINARY=${prismaEngines}/bin/schema-engine" + "PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING=1" ]; ExposedPorts = { "3000/tcp" = { }; -- 2.50.1 (Apple Git-155) From 5b71bb2398d3907ca625e4b06e66c492e9f2afa7 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sun, 8 Mar 2026 08:30:51 -0700 Subject: [PATCH 06/20] C2(jobsync): close build-jobsync-container, integrate-jobsync-ollama build-jobsync-container: Updated with lessons learned (prisma-engines from nixpkgs, Google Fonts sandbox workaround, arm64 vs x86_64). integrate-jobsync-ollama: Configuration-only card, env var will be set in the deployment manifest. Co-Authored-By: Claude Opus 4.6 --- .../how-to/jobsync/build-jobsync-container.md | 33 +++++++++++-------- .../jobsync/integrate-jobsync-ollama.md | 3 +- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/docs/how-to/jobsync/build-jobsync-container.md b/docs/how-to/jobsync/build-jobsync-container.md index e0b483b..82f13e4 100644 --- a/docs/how-to/jobsync/build-jobsync-container.md +++ b/docs/how-to/jobsync/build-jobsync-container.md @@ -1,7 +1,6 @@ --- title: Build JobSync Container -modified: 2026-03-07 -status: active +modified: 2026-03-08 requires: - mirror-jobsync tags: @@ -25,11 +24,25 @@ JobSync is a Next.js standalone app with Prisma (SQLite). The nix build needs to ## Key Details - **Runtime dependency:** `nodejs_20` must be in the image (unlike Go apps that compile to static binaries) -- **Prisma native engine:** `prisma generate` produces a platform-specific query engine binary; the nix build targets `linux-x86_64` for ringtail -- **`npmDepsHash`:** Will need to be computed on first build (set to empty, let it fail, grab the hash) +- **Prisma native engine:** Use `pkgs.prisma-engines` from nixpkgs — do NOT let Prisma download engines at build time (nix sandbox blocks network) +- **`npmDepsHash`:** Computed on first build (set to empty, let it fail, grab the hash) - **Standalone output:** Next.js `output: "standalone"` produces a self-contained `server.js` with minimal `node_modules` -## Nix Container Pitfalls (learned from first attempt) +## Nix Container Pitfalls + +### Prisma engine downloads in nix sandbox + +Prisma tries to download platform-specific engine binaries during `prisma generate`. The nix sandbox blocks network access, causing the build to fail. + +**Fix:** Use `pkgs.prisma-engines` from nixpkgs and set environment variables: +- `PRISMA_QUERY_ENGINE_LIBRARY`, `PRISMA_QUERY_ENGINE_BINARY`, `PRISMA_SCHEMA_ENGINE_BINARY`, `PRISMA_FMT_BINARY` — point at nixpkgs engines +- `PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING=1` — tolerate minor version mismatch between nixpkgs engines and npm prisma package + +### Google Fonts blocked by nix sandbox + +`next/font/google` (Inter font) fetches from `fonts.googleapis.com` during `next build`. Nix sandbox blocks this. + +**Fix:** Patch `src/app/layout.tsx` in `postPatch` to replace the Google font import with a no-op object. The app falls back to system sans-serif. ### Prisma devDependency pruning @@ -41,15 +54,9 @@ JobSync is a Next.js standalone app with Prisma (SQLite). The nix build needs to Nix containers have no `/app` directory. The app lives at `/nix/store//app/` and the container's `WorkingDir` is set to that path. The entrypoint must not `cd /app` — it should rely on the `WorkingDir` set in the container config. -### Verification +### Build on ringtail, not via Dagger -The container must be tested before deployment. After building, verify: - -```sh -# Run a debug pod with the new image: -kubectl run debug --image= --restart=Never --command -- \ - sh -c 'ls node_modules/@prisma/engines/ && node -e "require(\"@prisma/engines\")"' -``` +The Dagger `build-nix` pipeline runs in a container matching the host architecture. On macOS (arm64), this produces `linux-arm64` builds, which won't run on ringtail (x86_64). Build directly on ringtail or use the CI workflow. ## Files diff --git a/docs/how-to/jobsync/integrate-jobsync-ollama.md b/docs/how-to/jobsync/integrate-jobsync-ollama.md index 880c160..65268a2 100644 --- a/docs/how-to/jobsync/integrate-jobsync-ollama.md +++ b/docs/how-to/jobsync/integrate-jobsync-ollama.md @@ -1,7 +1,6 @@ --- title: Integrate JobSync with Ollama -modified: 2026-03-07 -status: active +modified: 2026-03-08 tags: - how-to - jobsync -- 2.50.1 (Apple Git-155) From 2220944a15bf0abd96caddf89a6287f0b1ca6e91 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sun, 8 Mar 2026 08:33:13 -0700 Subject: [PATCH 07/20] =?UTF-8?q?C2(jobsync):=20impl=20=E2=80=94=20ArgoCD?= =?UTF-8?q?=20app,=20k8s=20manifests,=20Caddy=20route?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ArgoCD Application targeting ringtail k3s cluster. Manifests: Deployment, Service, Tailscale Ingress, PVC (local-path), ExternalSecret (1Password auth_secret + encryption_key). Caddy route: jobsync.ops.eblu.me -> jobsync.tail8d86e.ts.net. Ollama integration via OLLAMA_BASE_URL env var in deployment. Co-Authored-By: Claude Opus 4.6 --- ansible/roles/caddy/defaults/main.yml | 3 + argocd/apps/jobsync.yaml | 18 +++++ argocd/manifests/jobsync/deployment.yaml | 73 +++++++++++++++++++ argocd/manifests/jobsync/external-secret.yaml | 23 ++++++ .../manifests/jobsync/ingress-tailscale.yaml | 26 +++++++ argocd/manifests/jobsync/kustomization.yaml | 15 ++++ argocd/manifests/jobsync/pvc.yaml | 13 ++++ argocd/manifests/jobsync/service.yaml | 13 ++++ 8 files changed, 184 insertions(+) create mode 100644 argocd/apps/jobsync.yaml create mode 100644 argocd/manifests/jobsync/deployment.yaml create mode 100644 argocd/manifests/jobsync/external-secret.yaml create mode 100644 argocd/manifests/jobsync/ingress-tailscale.yaml create mode 100644 argocd/manifests/jobsync/kustomization.yaml create mode 100644 argocd/manifests/jobsync/pvc.yaml create mode 100644 argocd/manifests/jobsync/service.yaml diff --git a/ansible/roles/caddy/defaults/main.yml b/ansible/roles/caddy/defaults/main.yml index 464d331..931e2a0 100644 --- a/ansible/roles/caddy/defaults/main.yml +++ b/ansible/roles/caddy/defaults/main.yml @@ -85,6 +85,9 @@ caddy_services: - name: ntfy host: "ntfy.{{ caddy_domain }}" backend: "https://ntfy.tail8d86e.ts.net" + - name: jobsync + host: "jobsync.{{ caddy_domain }}" + backend: "https://jobsync.tail8d86e.ts.net" - name: ollama host: "ollama.{{ caddy_domain }}" backend: "https://ollama.tail8d86e.ts.net" diff --git a/argocd/apps/jobsync.yaml b/argocd/apps/jobsync.yaml new file mode 100644 index 0000000..11d8beb --- /dev/null +++ b/argocd/apps/jobsync.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: jobsync + namespace: argocd +spec: + project: default + source: + repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git + targetRevision: main + path: argocd/manifests/jobsync + destination: + server: https://ringtail.tail8d86e.ts.net:6443 + namespace: jobsync + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/argocd/manifests/jobsync/deployment.yaml b/argocd/manifests/jobsync/deployment.yaml new file mode 100644 index 0000000..833a9b8 --- /dev/null +++ b/argocd/manifests/jobsync/deployment.yaml @@ -0,0 +1,73 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: jobsync + namespace: jobsync +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: jobsync + template: + metadata: + labels: + app: jobsync + spec: + containers: + - name: jobsync + image: blumeops/jobsync:kustomized + ports: + - containerPort: 3000 + name: http + env: + - name: DATABASE_URL + value: "file:/data/dev.db" + - name: NEXTAUTH_URL + value: "https://jobsync.ops.eblu.me" + - name: AUTH_TRUST_HOST + value: "true" + - name: NEXT_TELEMETRY_DISABLED + value: "1" + - name: TZ + value: "America/Los_Angeles" + - name: OLLAMA_BASE_URL + value: "http://ollama.ollama.svc.cluster.local:11434" + - name: AUTH_SECRET + valueFrom: + secretKeyRef: + name: jobsync-secrets + key: auth_secret + - name: ENCRYPTION_KEY + valueFrom: + secretKeyRef: + name: jobsync-secrets + key: encryption_key + volumeMounts: + - name: data + mountPath: /data + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + livenessProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 30 + periodSeconds: 30 + readinessProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 10 + periodSeconds: 10 + volumes: + - name: data + persistentVolumeClaim: + claimName: jobsync-data diff --git a/argocd/manifests/jobsync/external-secret.yaml b/argocd/manifests/jobsync/external-secret.yaml new file mode 100644 index 0000000..e4ef3a2 --- /dev/null +++ b/argocd/manifests/jobsync/external-secret.yaml @@ -0,0 +1,23 @@ +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: jobsync-secrets + namespace: jobsync +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-blumeops + target: + name: jobsync-secrets + creationPolicy: Owner + data: + - secretKey: auth_secret + remoteRef: + key: JobSync + property: auth_secret + - secretKey: encryption_key + remoteRef: + key: JobSync + property: encryption_key diff --git a/argocd/manifests/jobsync/ingress-tailscale.yaml b/argocd/manifests/jobsync/ingress-tailscale.yaml new file mode 100644 index 0000000..a8e24c8 --- /dev/null +++ b/argocd/manifests/jobsync/ingress-tailscale.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: jobsync-tailscale + namespace: jobsync + annotations: + tailscale.com/proxy-class: "default" + tailscale.com/proxy-group: "ingress" + gethomepage.dev/enabled: "true" + gethomepage.dev/name: "JobSync" + gethomepage.dev/group: "Apps" + gethomepage.dev/icon: "mdi-briefcase-search" + gethomepage.dev/description: "Job application tracker" + gethomepage.dev/href: "https://jobsync.ops.eblu.me" + gethomepage.dev/pod-selector: "app=jobsync" +spec: + ingressClassName: tailscale + defaultBackend: + service: + name: jobsync + port: + number: 3000 + tls: + - hosts: + - jobsync diff --git a/argocd/manifests/jobsync/kustomization.yaml b/argocd/manifests/jobsync/kustomization.yaml new file mode 100644 index 0000000..00b26af --- /dev/null +++ b/argocd/manifests/jobsync/kustomization.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: jobsync +resources: + - pvc.yaml + - external-secret.yaml + - deployment.yaml + - service.yaml + - ingress-tailscale.yaml + +images: + - name: blumeops/jobsync + newName: registry.ops.eblu.me/blumeops/jobsync + newTag: "v1.1.4-b1616bc-nix" diff --git a/argocd/manifests/jobsync/pvc.yaml b/argocd/manifests/jobsync/pvc.yaml new file mode 100644 index 0000000..01ab796 --- /dev/null +++ b/argocd/manifests/jobsync/pvc.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: jobsync-data + namespace: jobsync +spec: + accessModes: + - ReadWriteOnce + storageClassName: local-path + resources: + requests: + storage: 5Gi diff --git a/argocd/manifests/jobsync/service.yaml b/argocd/manifests/jobsync/service.yaml new file mode 100644 index 0000000..dc2d73a --- /dev/null +++ b/argocd/manifests/jobsync/service.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: jobsync + namespace: jobsync +spec: + selector: + app: jobsync + ports: + - name: http + port: 3000 + targetPort: 3000 -- 2.50.1 (Apple Git-155) From 27039e7fe71d87c632908a8f9afc8edebcf7c914 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sun, 8 Mar 2026 08:39:42 -0700 Subject: [PATCH 08/20] =?UTF-8?q?C2(jobsync):=20impl=20=E2=80=94=20fix=20e?= =?UTF-8?q?ntrypoint=20to=20use=20local=20prisma=20binary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit npx is not available in the nix container. Call prisma directly via node node_modules/prisma/build/index.js instead. Co-Authored-By: Claude Opus 4.6 --- containers/jobsync/entrypoint.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/containers/jobsync/entrypoint.sh b/containers/jobsync/entrypoint.sh index c0dca24..a5e4d70 100644 --- a/containers/jobsync/entrypoint.sh +++ b/containers/jobsync/entrypoint.sh @@ -8,8 +8,8 @@ if [ -z "$AUTH_SECRET" ]; then echo "AUTH_SECRET was not set — generated a temporary secret for this container." fi -# Run Prisma migrations -npx prisma migrate deploy +# Run Prisma migrations (use local node_modules binary, not npx) +node node_modules/prisma/build/index.js migrate deploy # Start the Next.js server exec node server.js -- 2.50.1 (Apple Git-155) From 8d272eee83b0f8334752749da19a2a4d296c9d01 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sun, 8 Mar 2026 08:46:05 -0700 Subject: [PATCH 09/20] =?UTF-8?q?C2(jobsync):=20impl=20=E2=80=94=20update?= =?UTF-8?q?=20image=20tag=20to=20v1.1.4-27039e7-nix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Point kustomization at the container with the fixed entrypoint. Co-Authored-By: Claude Opus 4.6 --- argocd/manifests/jobsync/kustomization.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/argocd/manifests/jobsync/kustomization.yaml b/argocd/manifests/jobsync/kustomization.yaml index 00b26af..451fc71 100644 --- a/argocd/manifests/jobsync/kustomization.yaml +++ b/argocd/manifests/jobsync/kustomization.yaml @@ -12,4 +12,4 @@ resources: images: - name: blumeops/jobsync newName: registry.ops.eblu.me/blumeops/jobsync - newTag: "v1.1.4-b1616bc-nix" + newTag: "v1.1.4-27039e7-nix" -- 2.50.1 (Apple Git-155) From 846d879b40ba9b21079d77bc2b14226821e11a98 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sun, 8 Mar 2026 09:00:24 -0700 Subject: [PATCH 10/20] =?UTF-8?q?C2(jobsync):=20impl=20=E2=80=94=20fix=20@?= =?UTF-8?q?prisma/engines=20copy=20in=20installPhase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cp -r of @prisma/ into an existing node_modules/@prisma/ nested incorrectly. Use cp -rn with glob to merge contents instead. Co-Authored-By: Claude Opus 4.6 --- containers/jobsync/default.nix | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/containers/jobsync/default.nix b/containers/jobsync/default.nix index 3000642..855b962 100644 --- a/containers/jobsync/default.nix +++ b/containers/jobsync/default.nix @@ -78,9 +78,10 @@ let # Copy Prisma schema and migrations for runtime migrate deploy cp -r prisma $out/app/prisma - # Restore prisma packages needed at runtime - cp -r /tmp/prisma-save/.prisma $out/app/node_modules/.prisma || true - cp -r /tmp/prisma-save/@prisma $out/app/node_modules/@prisma || true + # Restore prisma packages needed at runtime (merge into existing @prisma/) + cp -r /tmp/prisma-save/.prisma/* $out/app/node_modules/.prisma/ 2>/dev/null || true + mkdir -p $out/app/node_modules/@prisma + cp -rn /tmp/prisma-save/@prisma/* $out/app/node_modules/@prisma/ 2>/dev/null || true cp -r /tmp/prisma-save/prisma $out/app/node_modules/prisma || true # Copy entrypoint -- 2.50.1 (Apple Git-155) From 6187ade18f8433b4c22fd0fe4bc30a9e0caf319c Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sun, 8 Mar 2026 09:06:16 -0700 Subject: [PATCH 11/20] =?UTF-8?q?C2(jobsync):=20impl=20=E2=80=94=20update?= =?UTF-8?q?=20image=20tag=20to=20v1.1.4-846d879-nix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Container with fixed @prisma/engines copy and local prisma binary. Co-Authored-By: Claude Opus 4.6 --- argocd/manifests/jobsync/kustomization.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/argocd/manifests/jobsync/kustomization.yaml b/argocd/manifests/jobsync/kustomization.yaml index 451fc71..970dde3 100644 --- a/argocd/manifests/jobsync/kustomization.yaml +++ b/argocd/manifests/jobsync/kustomization.yaml @@ -12,4 +12,4 @@ resources: images: - name: blumeops/jobsync newName: registry.ops.eblu.me/blumeops/jobsync - newTag: "v1.1.4-27039e7-nix" + newTag: "v1.1.4-846d879-nix" -- 2.50.1 (Apple Git-155) From fdac2e3699a2e37a4705288cb7fbf406830ad45e Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sun, 8 Mar 2026 09:10:11 -0700 Subject: [PATCH 12/20] =?UTF-8?q?C2(jobsync):=20impl=20=E2=80=94=20add=20/?= =?UTF-8?q?tmp=20and=20/data=20directories=20to=20nix=20container?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prisma's get-platform module requires /tmp for temp files. Nix containers don't create standard directories by default. Co-Authored-By: Claude Opus 4.6 --- containers/jobsync/default.nix | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/containers/jobsync/default.nix b/containers/jobsync/default.nix index 855b962..07360e6 100644 --- a/containers/jobsync/default.nix +++ b/containers/jobsync/default.nix @@ -113,6 +113,11 @@ pkgs.dockerTools.buildLayeredImage { pkgs.coreutils ]; + # Create writable directories that don't exist in nix containers + extraCommands = '' + mkdir -p tmp data + ''; + config = { Entrypoint = [ "${entrypoint}" ]; WorkingDir = "${jobsync}/app"; -- 2.50.1 (Apple Git-155) From 2384a15cbfcdda345b20051a2b2e822c8acedc03 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sun, 8 Mar 2026 09:14:39 -0700 Subject: [PATCH 13/20] =?UTF-8?q?C2(jobsync):=20impl=20=E2=80=94=20update?= =?UTF-8?q?=20image=20tag=20to=20v1.1.4-fdac2e3-nix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Container with /tmp directory fix for prisma get-platform. Co-Authored-By: Claude Opus 4.6 --- argocd/manifests/jobsync/kustomization.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/argocd/manifests/jobsync/kustomization.yaml b/argocd/manifests/jobsync/kustomization.yaml index 970dde3..7e59976 100644 --- a/argocd/manifests/jobsync/kustomization.yaml +++ b/argocd/manifests/jobsync/kustomization.yaml @@ -12,4 +12,4 @@ resources: images: - name: blumeops/jobsync newName: registry.ops.eblu.me/blumeops/jobsync - newTag: "v1.1.4-846d879-nix" + newTag: "v1.1.4-fdac2e3-nix" -- 2.50.1 (Apple Git-155) From 87930e5e313c922156b4d6b878e42ed4d26d48b1 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sun, 8 Mar 2026 09:23:28 -0700 Subject: [PATCH 14/20] =?UTF-8?q?C2(jobsync):=20impl=20=E2=80=94=20skip=20?= =?UTF-8?q?npm=20prune,=20copy=20prisma=20transitive=20deps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prisma CLI (devDep) has a deep transitive dependency tree that must be present at runtime for `migrate deploy`. Skip npm prune entirely and explicitly copy all prisma packages and their transitive deps into the output. Co-Authored-By: Claude Opus 4.6 --- containers/jobsync/default.nix | 35 +++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/containers/jobsync/default.nix b/containers/jobsync/default.nix index 07360e6..bd3f0a2 100644 --- a/containers/jobsync/default.nix +++ b/containers/jobsync/default.nix @@ -57,13 +57,12 @@ let runHook postBuild ''; - # Save prisma packages before npm prune removes them (they're devDeps) - postBuild = '' - mkdir -p /tmp/prisma-save - cp -r node_modules/.prisma /tmp/prisma-save/ || true - cp -r node_modules/@prisma /tmp/prisma-save/ || true - cp -r node_modules/prisma /tmp/prisma-save/ || true - ''; + # Skip npm prune — prisma CLI is a devDependency but needed at runtime + # for `migrate deploy`, and it has a deep transitive dependency tree + # (@prisma/config -> effect, c12, etc.) that's impractical to save/restore + # individually. + npmPruneFlags = [ ]; + dontNpmPrune = true; installPhase = '' runHook preInstall @@ -78,11 +77,25 @@ let # Copy Prisma schema and migrations for runtime migrate deploy cp -r prisma $out/app/prisma - # Restore prisma packages needed at runtime (merge into existing @prisma/) - cp -r /tmp/prisma-save/.prisma/* $out/app/node_modules/.prisma/ 2>/dev/null || true + # Copy prisma and generated client (kept by dontNpmPrune) + cp -r node_modules/.prisma $out/app/node_modules/.prisma || true + cp -r node_modules/prisma $out/app/node_modules/prisma || true + # Copy all @prisma/* packages (engines, config, get-platform, etc.) mkdir -p $out/app/node_modules/@prisma - cp -rn /tmp/prisma-save/@prisma/* $out/app/node_modules/@prisma/ 2>/dev/null || true - cp -r /tmp/prisma-save/prisma $out/app/node_modules/prisma || true + for pkg in node_modules/@prisma/*; do + name=$(basename "$pkg") + # Don't overwrite @prisma/client from standalone output + if [ ! -d "$out/app/node_modules/@prisma/$name" ]; then + cp -r "$pkg" "$out/app/node_modules/@prisma/$name" + fi + done + # Copy transitive deps of prisma CLI (effect, c12, etc.) + for dep in effect @effect c12 deepmerge-ts empathic confbox defu dotenv \ + exsolve giget jiti ohash pathe perfect-debounce chokidar; do + if [ -d "node_modules/$dep" ]; then + cp -r "node_modules/$dep" "$out/app/node_modules/$dep" 2>/dev/null || true + fi + done # Copy entrypoint cp ${./entrypoint.sh} $out/app/entrypoint.sh -- 2.50.1 (Apple Git-155) From 6b36d53bab10fbeb7f5f44f08440ab9b2e001e80 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sun, 8 Mar 2026 09:26:13 -0700 Subject: [PATCH 15/20] =?UTF-8?q?C2(jobsync):=20impl=20=E2=80=94=20simplif?= =?UTF-8?q?y:=20use=20npx=20-y=20for=20runtime=20prisma=20migrate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of bundling prisma CLI and its deep dependency tree in the nix image, use `npx -y prisma@6.19.0 migrate deploy` like upstream does. npx downloads prisma at container startup — network is available at runtime, only blocked during nix build. Co-Authored-By: Claude Opus 4.6 --- containers/jobsync/default.nix | 35 +++----------------------------- containers/jobsync/entrypoint.sh | 4 ++-- 2 files changed, 5 insertions(+), 34 deletions(-) diff --git a/containers/jobsync/default.nix b/containers/jobsync/default.nix index bd3f0a2..8177e84 100644 --- a/containers/jobsync/default.nix +++ b/containers/jobsync/default.nix @@ -21,10 +21,8 @@ let nodejs = pkgs.nodejs_20; - nativeBuildInputs = [ pkgs.makeWrapper ]; - - # Patch out Google Fonts import (nix sandbox blocks network access). - # Replace with a simple object that provides the CSS variable class name. + # Patch out Google Fonts import (nix sandbox blocks network access at + # build time). Replace with a simple object; app uses system sans-serif. postPatch = '' substituteInPlace src/app/layout.tsx \ --replace-fail 'import { Inter } from "next/font/google";' "" \ @@ -34,7 +32,7 @@ let });' 'const inter = { variable: "" };' ''; - # Point Prisma at nixpkgs-built engines (no network download needed) + # Point Prisma at nixpkgs-built engines (no network download in sandbox) env = { PRISMA_QUERY_ENGINE_LIBRARY = "${prismaEngines}/lib/libquery_engine.node"; PRISMA_QUERY_ENGINE_BINARY = "${prismaEngines}/bin/query-engine"; @@ -57,13 +55,6 @@ let runHook postBuild ''; - # Skip npm prune — prisma CLI is a devDependency but needed at runtime - # for `migrate deploy`, and it has a deep transitive dependency tree - # (@prisma/config -> effect, c12, etc.) that's impractical to save/restore - # individually. - npmPruneFlags = [ ]; - dontNpmPrune = true; - installPhase = '' runHook preInstall @@ -77,26 +68,6 @@ let # Copy Prisma schema and migrations for runtime migrate deploy cp -r prisma $out/app/prisma - # Copy prisma and generated client (kept by dontNpmPrune) - cp -r node_modules/.prisma $out/app/node_modules/.prisma || true - cp -r node_modules/prisma $out/app/node_modules/prisma || true - # Copy all @prisma/* packages (engines, config, get-platform, etc.) - mkdir -p $out/app/node_modules/@prisma - for pkg in node_modules/@prisma/*; do - name=$(basename "$pkg") - # Don't overwrite @prisma/client from standalone output - if [ ! -d "$out/app/node_modules/@prisma/$name" ]; then - cp -r "$pkg" "$out/app/node_modules/@prisma/$name" - fi - done - # Copy transitive deps of prisma CLI (effect, c12, etc.) - for dep in effect @effect c12 deepmerge-ts empathic confbox defu dotenv \ - exsolve giget jiti ohash pathe perfect-debounce chokidar; do - if [ -d "node_modules/$dep" ]; then - cp -r "node_modules/$dep" "$out/app/node_modules/$dep" 2>/dev/null || true - fi - done - # Copy entrypoint cp ${./entrypoint.sh} $out/app/entrypoint.sh diff --git a/containers/jobsync/entrypoint.sh b/containers/jobsync/entrypoint.sh index a5e4d70..4dc611f 100644 --- a/containers/jobsync/entrypoint.sh +++ b/containers/jobsync/entrypoint.sh @@ -8,8 +8,8 @@ if [ -z "$AUTH_SECRET" ]; then echo "AUTH_SECRET was not set — generated a temporary secret for this container." fi -# Run Prisma migrations (use local node_modules binary, not npx) -node node_modules/prisma/build/index.js migrate deploy +# Run Prisma migrations (npx -y downloads prisma if not in local node_modules) +npx -y prisma@6.19.0 migrate deploy # Start the Next.js server exec node server.js -- 2.50.1 (Apple Git-155) From 1124b07870e511b6d6a0334c87e9236b58932555 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sun, 8 Mar 2026 09:40:21 -0700 Subject: [PATCH 16/20] =?UTF-8?q?C2(jobsync):=20impl=20=E2=80=94=20update?= =?UTF-8?q?=20image=20tag=20to=20v1.1.4-6b36d53-nix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplified container with npx -y for prisma migrations. Co-Authored-By: Claude Opus 4.6 --- argocd/manifests/jobsync/kustomization.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/argocd/manifests/jobsync/kustomization.yaml b/argocd/manifests/jobsync/kustomization.yaml index 7e59976..0cec547 100644 --- a/argocd/manifests/jobsync/kustomization.yaml +++ b/argocd/manifests/jobsync/kustomization.yaml @@ -12,4 +12,4 @@ resources: images: - name: blumeops/jobsync newName: registry.ops.eblu.me/blumeops/jobsync - newTag: "v1.1.4-fdac2e3-nix" + newTag: "v1.1.4-6b36d53-nix" -- 2.50.1 (Apple Git-155) From e51ec83c41ac368cc40397494abfd505e0cd37a1 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sun, 8 Mar 2026 09:44:36 -0700 Subject: [PATCH 17/20] =?UTF-8?q?C2(jobsync):=20impl=20=E2=80=94=20add=20/?= =?UTF-8?q?usr/bin/env=20symlink=20for=20npx-installed=20scripts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit npx-downloaded prisma has `#!/usr/bin/env node` shebang. Nix containers lack FHS paths; create the symlink in extraCommands. Co-Authored-By: Claude Opus 4.6 --- containers/jobsync/default.nix | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/containers/jobsync/default.nix b/containers/jobsync/default.nix index 8177e84..198dd70 100644 --- a/containers/jobsync/default.nix +++ b/containers/jobsync/default.nix @@ -97,9 +97,10 @@ pkgs.dockerTools.buildLayeredImage { pkgs.coreutils ]; - # Create writable directories that don't exist in nix containers + # Create writable directories and FHS symlinks for nix container extraCommands = '' - mkdir -p tmp data + mkdir -p tmp data usr/bin + ln -s ${pkgs.coreutils}/bin/env usr/bin/env ''; config = { -- 2.50.1 (Apple Git-155) From c42d219cb6413a9e645e5a0dfe4a67b494147d05 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sun, 8 Mar 2026 09:45:49 -0700 Subject: [PATCH 18/20] =?UTF-8?q?C2(jobsync):=20impl=20=E2=80=94=20update?= =?UTF-8?q?=20image=20tag=20to=20v1.1.4-e51ec83-nix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Container with /usr/bin/env symlink for npx shebang resolution. Co-Authored-By: Claude Opus 4.6 --- argocd/manifests/jobsync/kustomization.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/argocd/manifests/jobsync/kustomization.yaml b/argocd/manifests/jobsync/kustomization.yaml index 0cec547..d0d0c84c 100644 --- a/argocd/manifests/jobsync/kustomization.yaml +++ b/argocd/manifests/jobsync/kustomization.yaml @@ -12,4 +12,4 @@ resources: images: - name: blumeops/jobsync newName: registry.ops.eblu.me/blumeops/jobsync - newTag: "v1.1.4-6b36d53-nix" + newTag: "v1.1.4-e51ec83-nix" -- 2.50.1 (Apple Git-155) From 9d1e7eff125434a602f49c728cfbe2f998041b77 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sun, 8 Mar 2026 10:57:02 -0700 Subject: [PATCH 19/20] =?UTF-8?q?C2(jobsync):=20close=20=E2=80=94=20deploy?= =?UTF-8?q?-jobsync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- docs/how-to/jobsync/deploy-jobsync.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/how-to/jobsync/deploy-jobsync.md b/docs/how-to/jobsync/deploy-jobsync.md index f2d9d05..c3596c9 100644 --- a/docs/how-to/jobsync/deploy-jobsync.md +++ b/docs/how-to/jobsync/deploy-jobsync.md @@ -1,7 +1,6 @@ --- title: Deploy JobSync -modified: 2026-03-07 -status: active +modified: 2026-03-08 branch: mikado/jobsync requires: - build-jobsync-container @@ -28,7 +27,6 @@ With the container built and Ollama integration configured, this card wires up t - PVC using k3s local-path for `/data` (SQLite + resume uploads) - ExternalSecret for `ENCRYPTION_KEY` and `AUTH_SECRET` from 1Password - Caddy route: `jobsync.ops.eblu.me` → Tailscale ingress -- Service documentation ## Environment Variables @@ -42,11 +40,13 @@ With the container built and Ollama integration configured, this card wires up t | `TZ` | Hardcoded | `America/Los_Angeles` | | `OLLAMA_BASE_URL` | Hardcoded | `http://ollama.ollama.svc.cluster.local:11434` | -## Deployment Notes (learned from first attempt) +## Deployment Notes - **`service-versions.yaml`:** Add a `jobsync` entry before committing container changes — the `container-version-check` pre-commit hook rejects commits touching `containers//` without a matching entry. - **Image tag format:** `container-build-and-release` produces tags like `v1.1.4--nix`, not bare `v1.1.4`. Set `newTag` in `kustomization.yaml` to the full tag from `mise run container-list`. - **1Password item:** "JobSync" in blumeops vault, with `auth_secret` and `encryption_key` fields (already created). +- **Nix container FHS:** Nix containers lack `/usr/bin/env` — add `ln -s ${pkgs.coreutils}/bin/env usr/bin/env` in `extraCommands`. Also `mkdir -p tmp` for `/tmp`. +- **Runtime migrations:** Use `npx -y prisma@ migrate deploy` — nix sandbox blocks network at build time but runtime has full network access. ## Related -- 2.50.1 (Apple Git-155) From ae72092ed78690d1199702f8933a0e498f795d17 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sun, 8 Mar 2026 11:00:06 -0700 Subject: [PATCH 20/20] =?UTF-8?q?C2(jobsync):=20finalize=20=E2=80=94=20rew?= =?UTF-8?q?ork=20cards=20into=20standalone=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete mirror-jobsync and integrate-jobsync-ollama (no standalone value). Rework build-jobsync-container and deploy-jobsync into operational reference documentation. Add changelog fragment. Co-Authored-By: Claude Opus 4.6 --- docs/changelog.d/mikado-jobsync.feature.md | 1 + docs/how-to/how-to.md | 2 - .../how-to/jobsync/build-jobsync-container.md | 61 ++++++++----------- docs/how-to/jobsync/deploy-jobsync.md | 49 ++++++++------- .../jobsync/integrate-jobsync-ollama.md | 44 ------------- docs/how-to/jobsync/mirror-jobsync.md | 26 -------- 6 files changed, 54 insertions(+), 129 deletions(-) create mode 100644 docs/changelog.d/mikado-jobsync.feature.md delete mode 100644 docs/how-to/jobsync/integrate-jobsync-ollama.md delete mode 100644 docs/how-to/jobsync/mirror-jobsync.md diff --git a/docs/changelog.d/mikado-jobsync.feature.md b/docs/changelog.d/mikado-jobsync.feature.md new file mode 100644 index 0000000..bdd3eb7 --- /dev/null +++ b/docs/changelog.d/mikado-jobsync.feature.md @@ -0,0 +1 @@ +Deploy JobSync to ringtail k3s — nix-built container, Tailscale Ingress, Caddy route at `jobsync.ops.eblu.me`, Ollama integration for AI features. diff --git a/docs/how-to/how-to.md b/docs/how-to/how-to.md index 5f1bd4f..0ca60a6 100644 --- a/docs/how-to/how-to.md +++ b/docs/how-to/how-to.md @@ -92,8 +92,6 @@ tags: - [[deploy-jobsync]] - [[build-jobsync-container]] -- [[mirror-jobsync]] -- [[integrate-jobsync-ollama]] ## Forgejo Runner diff --git a/docs/how-to/jobsync/build-jobsync-container.md b/docs/how-to/jobsync/build-jobsync-container.md index 82f13e4..de75915 100644 --- a/docs/how-to/jobsync/build-jobsync-container.md +++ b/docs/how-to/jobsync/build-jobsync-container.md @@ -1,70 +1,61 @@ --- title: Build JobSync Container modified: 2026-03-08 -requires: - - mirror-jobsync tags: - how-to - jobsync + - nix --- # Build JobSync Container -Build a nix container image for JobSync using `dockerTools.buildLayeredImage`, following the ntfy pattern. +Build and release the JobSync nix container image. -## Context +```fish +mise run container-release jobsync 1.1.4 +``` -JobSync is a Next.js standalone app with Prisma (SQLite). The nix build needs to: +The derivation is at `containers/jobsync/default.nix`. It uses `buildNpmPackage` for the Next.js app and `dockerTools.buildLayeredImage` for the container. The entrypoint (`containers/jobsync/entrypoint.sh`) runs `prisma migrate deploy` then starts `node server.js`. -1. Fetch source from `forge.ops.eblu.me/mirrors/jobsync` (v1.1.4) -2. `buildNpmPackage` — install deps, run `prisma generate`, run `next build` -3. Package the standalone output with `nodejs` runtime into a layered image -4. Include an entrypoint that runs `prisma migrate deploy` before `node server.js` +## Upgrading JobSync -## Key Details +1. Update the forge mirror: `mise run mirror-sync jobsync` +2. Update `version` in `default.nix` to match the new upstream tag +3. Clear `hash` in `fetchgit` (set to `""`), build, grab the correct hash from the error +4. Clear `npmDepsHash` (set to `""`), build again, grab the correct hash +5. Check if `postPatch` still applies — the Google Fonts import may change between versions +6. `mise run container-release jobsync ` +7. Update `newTag` in `argocd/manifests/jobsync/kustomization.yaml` -- **Runtime dependency:** `nodejs_20` must be in the image (unlike Go apps that compile to static binaries) -- **Prisma native engine:** Use `pkgs.prisma-engines` from nixpkgs — do NOT let Prisma download engines at build time (nix sandbox blocks network) -- **`npmDepsHash`:** Computed on first build (set to empty, let it fail, grab the hash) -- **Standalone output:** Next.js `output: "standalone"` produces a self-contained `server.js` with minimal `node_modules` +## Nix + Prisma + Next.js Pitfalls -## Nix Container Pitfalls +### Prisma engine downloads blocked by sandbox -### Prisma engine downloads in nix sandbox +Prisma tries to download platform-specific engine binaries during `prisma generate`. The nix sandbox blocks network access at build time. -Prisma tries to download platform-specific engine binaries during `prisma generate`. The nix sandbox blocks network access, causing the build to fail. +**Fix:** Use `pkgs.prisma-engines` from nixpkgs and set env vars pointing at the nix store paths: `PRISMA_QUERY_ENGINE_LIBRARY`, `PRISMA_QUERY_ENGINE_BINARY`, `PRISMA_SCHEMA_ENGINE_BINARY`, `PRISMA_FMT_BINARY`. Set `PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING=1` to tolerate minor version mismatch. -**Fix:** Use `pkgs.prisma-engines` from nixpkgs and set environment variables: -- `PRISMA_QUERY_ENGINE_LIBRARY`, `PRISMA_QUERY_ENGINE_BINARY`, `PRISMA_SCHEMA_ENGINE_BINARY`, `PRISMA_FMT_BINARY` — point at nixpkgs engines -- `PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING=1` — tolerate minor version mismatch between nixpkgs engines and npm prisma package +### Google Fonts blocked by sandbox -### Google Fonts blocked by nix sandbox - -`next/font/google` (Inter font) fetches from `fonts.googleapis.com` during `next build`. Nix sandbox blocks this. +`next/font/google` fetches from `fonts.googleapis.com` during `next build`. **Fix:** Patch `src/app/layout.tsx` in `postPatch` to replace the Google font import with a no-op object. The app falls back to system sans-serif. -### Prisma devDependency pruning +### Missing FHS paths in nix containers -`buildNpmPackage` runs `npm prune --omit=dev` between `buildPhase` and `installPhase`. The `prisma` CLI and `@prisma/engines` are devDependencies, so they get removed. But they're needed at runtime for `prisma migrate deploy` in the entrypoint. +Nix containers lack `/usr/bin/env`, `/tmp`, etc. `npx`-downloaded packages use `#!/usr/bin/env node` shebangs. -**Fix:** Save prisma packages to a temp directory during `postBuild` (before prune), then copy them into the output during `installPhase`. +**Fix:** In `extraCommands`: `mkdir -p tmp data usr/bin` and `ln -s ${pkgs.coreutils}/bin/env usr/bin/env`. -### Entrypoint filesystem paths +### Runtime migrations via npx -Nix containers have no `/app` directory. The app lives at `/nix/store//app/` and the container's `WorkingDir` is set to that path. The entrypoint must not `cd /app` — it should rely on the `WorkingDir` set in the container config. +The nix sandbox blocks network at build time, but runtime has full network access. Use `npx -y prisma@ migrate deploy` in the entrypoint — npx downloads the prisma CLI on first run. ### Build on ringtail, not via Dagger -The Dagger `build-nix` pipeline runs in a container matching the host architecture. On macOS (arm64), this produces `linux-arm64` builds, which won't run on ringtail (x86_64). Build directly on ringtail or use the CI workflow. - -## Files - -- `containers/jobsync/default.nix` — nix derivation -- `containers/jobsync/entrypoint.sh` — startup script (migrations + server) +The Dagger `build-nix` pipeline runs in host architecture. On macOS (arm64), this produces arm64 images. Build on ringtail (x86_64) using the CI workflow or `mise run container-release`. ## Related -- [[mirror-jobsync]] - [[deploy-jobsync]] - [[build-container-image]] diff --git a/docs/how-to/jobsync/deploy-jobsync.md b/docs/how-to/jobsync/deploy-jobsync.md index c3596c9..6b72ad7 100644 --- a/docs/how-to/jobsync/deploy-jobsync.md +++ b/docs/how-to/jobsync/deploy-jobsync.md @@ -1,10 +1,6 @@ --- title: Deploy JobSync modified: 2026-03-08 -branch: mikado/jobsync -requires: - - build-jobsync-container - - integrate-jobsync-ollama tags: - how-to - jobsync @@ -12,21 +8,25 @@ tags: # Deploy JobSync -Deploy [JobSync](https://github.com/Gsync/jobsync) — a self-hosted job application tracker — to ringtail's k3s cluster via ArgoCD. +[JobSync](https://github.com/Gsync/jobsync) is a self-hosted job application tracker (Next.js + Prisma/SQLite) running on ringtail's k3s cluster via ArgoCD. -## Context +- **URL:** `https://jobsync.ops.eblu.me` +- **Auth:** Local accounts (email/password), no SSO +- **Storage:** 5Gi PVC at `/data` (SQLite DB + resume uploads) +- **AI:** Ollama at `ollama.ollama.svc.cluster.local:11434` -JobSync is a Next.js app with SQLite storage that provides job application tracking, resume management, and AI-powered resume review/job matching. It runs as a single container with persistent storage at `/data` (SQLite DB + uploaded files). +## Manifests -## What This Card Covers +All in `argocd/manifests/jobsync/`: -With the container built and Ollama integration configured, this card wires up the deployment: - -- ArgoCD Application targeting `ringtail.tail8d86e.ts.net:6443` -- k8s manifests: Deployment, Service, Tailscale Ingress, PVC, ExternalSecret -- PVC using k3s local-path for `/data` (SQLite + resume uploads) -- ExternalSecret for `ENCRYPTION_KEY` and `AUTH_SECRET` from 1Password -- Caddy route: `jobsync.ops.eblu.me` → Tailscale ingress +| File | Purpose | +|------|---------| +| `deployment.yaml` | Single-replica deployment | +| `service.yaml` | ClusterIP on port 3000 | +| `ingress-tailscale.yaml` | Tailscale Ingress (ProxyGroup) | +| `pvc.yaml` | 5Gi local-path for `/data` | +| `external-secret.yaml` | `auth_secret` + `encryption_key` from 1Password | +| `kustomization.yaml` | Image tag override | ## Environment Variables @@ -40,16 +40,21 @@ With the container built and Ollama integration configured, this card wires up t | `TZ` | Hardcoded | `America/Los_Angeles` | | `OLLAMA_BASE_URL` | Hardcoded | `http://ollama.ollama.svc.cluster.local:11434` | -## Deployment Notes +## Updating the Container -- **`service-versions.yaml`:** Add a `jobsync` entry before committing container changes — the `container-version-check` pre-commit hook rejects commits touching `containers//` without a matching entry. -- **Image tag format:** `container-build-and-release` produces tags like `v1.1.4--nix`, not bare `v1.1.4`. Set `newTag` in `kustomization.yaml` to the full tag from `mise run container-list`. -- **1Password item:** "JobSync" in blumeops vault, with `auth_secret` and `encryption_key` fields (already created). -- **Nix container FHS:** Nix containers lack `/usr/bin/env` — add `ln -s ${pkgs.coreutils}/bin/env usr/bin/env` in `extraCommands`. Also `mkdir -p tmp` for `/tmp`. -- **Runtime migrations:** Use `npx -y prisma@ migrate deploy` — nix sandbox blocks network at build time but runtime has full network access. +1. Build and push: `mise run container-release jobsync ` +2. Update `newTag` in `kustomization.yaml` to the full tag (e.g. `v1.1.4-e51ec83-nix`) +3. Sync: `argocd app sync jobsync` + +See [[build-jobsync-container]] for nix build details. + +## Notes + +- **1Password item:** "JobSync" in blumeops vault, fields `auth_secret` and `encryption_key` +- **Caddy route:** `jobsync.ops.eblu.me` → `https://jobsync.tail8d86e.ts.net` (in `ansible/roles/caddy/defaults/main.yml`) +- **`service-versions.yaml`:** Must have a `jobsync` entry or the pre-commit hook rejects container changes ## Related - [[build-jobsync-container]] -- [[integrate-jobsync-ollama]] - [[deploy-k8s-service]] diff --git a/docs/how-to/jobsync/integrate-jobsync-ollama.md b/docs/how-to/jobsync/integrate-jobsync-ollama.md deleted file mode 100644 index 65268a2..0000000 --- a/docs/how-to/jobsync/integrate-jobsync-ollama.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: Integrate JobSync with Ollama -modified: 2026-03-08 -tags: - - how-to - - jobsync ---- - -# Integrate JobSync with Ollama - -Configure JobSync to use the existing Ollama deployment on ringtail for AI features (resume review, job matching). - -## Context - -Ollama already runs on ringtail's k3s cluster at `ollama.ollama.svc.cluster.local:11434` with several models available (qwen2.5:14b, deepseek-r1:14b, phi4:14b, gemma3:12b, qwen3.5:9b). - -JobSync supports Ollama natively via `OLLAMA_BASE_URL`. Since both services run on the same k3s cluster, this is a cluster-internal connection with no auth required. - -## Configuration - -Set in the JobSync deployment manifest: - -```yaml -env: - - name: OLLAMA_BASE_URL - value: "http://ollama.ollama.svc.cluster.local:11434" -``` - -## Verification - -After deployment: - -1. Open JobSync settings -2. Select Ollama as the AI provider -3. Verify model list populates from the Ollama instance -4. Test resume review with one of the available models - -## Model Recommendations - -For resume review and job matching, `qwen2.5:14b` or `gemma3:12b` are good choices — capable enough for structured text analysis without being overly slow on the RTX 4080. - -## Related - -- [[deploy-jobsync]] diff --git a/docs/how-to/jobsync/mirror-jobsync.md b/docs/how-to/jobsync/mirror-jobsync.md deleted file mode 100644 index 46a30cc..0000000 --- a/docs/how-to/jobsync/mirror-jobsync.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: Mirror JobSync -modified: 2026-03-07 -tags: - - how-to - - jobsync ---- - -# Mirror JobSync - -Mirror the upstream [JobSync](https://github.com/Gsync/jobsync) repository to `forge.ops.eblu.me/mirrors/jobsync` for supply chain control. - -## Context - -JobSync is not in nixpkgs. Building a nix container requires fetching source from a controlled mirror on forge, following the same pattern as [[mirror-authentik-build-deps]] and the ntfy mirror. - -## Steps - -1. Create the mirror: `mise run mirror-create jobsync https://github.com/Gsync/jobsync.git` -2. Verify the mirror syncs: check `forge.ops.eblu.me/mirrors/jobsync` -3. Note the tag for v1.1.4 (current upstream release) - -## Related - -- [[build-jobsync-container]] -- [[manage-forgejo-mirrors]] -- 2.50.1 (Apple Git-155)