Job Hunter Agent: CI/CD and Release Management
Every push to Job Hunter Agent is tested; every push to main is scanned by seven gates, signed, pinned by digest and rolled out by Argo CD without a manual step.
A one-person project could get away with docker build && kubectl apply. Job Hunter Agent goes through the same pipeline as everything else on the homelab platform instead, because the platform refuses unsigned images, and because the pipeline costs nothing to reuse: one uses: line in the repo’s CI.
Build automation
Step through it, and tick the box to see a failing gate:
- A push: A commit lands on main, or a pull request is opened. Small changes go straight to main; schema changes go through a PR so the migration is applied first.
- Three checks in parallel: Unit tests and ruff, the full flow against a fresh local Supabase with every migration applied, and a Docker build. A pull request stops after these.
- Only main deploys: The image job needs the unit and Supabase tests to pass, and runs only on main with the repo variable HOMELAB_DEPLOY set. The Docker build is a separate check.
- Seven security gates: On the homelab runner, the platform's reusable workflow runs Gitleaks, Semgrep, Bandit, Trivy, Checkov, Conftest and Hadolint. Any finding above the bar fails the job.
- A gate fails (fail): No image is built and nothing deploys; the running version stays. The scanners' reports are uploaded to the run so the finding can be fixed or suppressed by fingerprint.
- Build, scan, SBOM (pass): The image is built from digest-pinned base images, scanned again by Trivy as an image, and described by a CycloneDX SBOM from Syft.
- Push to Zot (pass): The image goes to the homelab's Zot registry as platform/job-hunter-agent, with registry credentials read from OpenBao for this run only.
- Sign and attest (pass): cosign signs the digest and attests the SBOM with a key that never leaves OpenBao's transit engine. The run's OpenBao token is revoked afterwards.
- Promote by digest (pass): A GitHub App commits the new digest to apps/job-hunter/kustomization.yaml in homelab-gitops. Git is the release record.
- Argo CD syncs (pass): The workflow pings Argo CD's webhook so it refreshes at once instead of at its next poll, and the app syncs to the new digest.
- Admission (pass): Kyverno admits the pod only if the image is signed, pinned by digest and from the allowed registry, and the pod meets the restricted security profile.
- Recreate rollout (pass): The old pod stops before the new one starts, because only one browser may use the LinkedIn profile. The session survives on the volume.
The repo’s ci.yaml has four jobs: test, supabase, docker on GitHub-hosted runners, and image, which calls homelab-gitops’ reusable build-image.yaml on the homelab runner. image runs only on main (never for pull requests) and only when the repo variable HOMELAB_DEPLOY is true. A workflow_dispatch trigger allows a manual rebuild.
Automated test execution
test runs pytest and ruff; supabase starts a local Supabase, applies every migration and runs the integration test; docker checks the Dockerfile still builds. The image job needs the test and Supabase jobs, so either one red stops the release. See Quality Assurance.
Static analysis and security scanning
| Gate | Looks at |
|---|---|
| Gitleaks | Secrets in the full git history |
| Semgrep | SAST rules |
| Bandit | Python-specific issues |
| Trivy | Dependencies, Dockerfiles and IaC, then the built image |
| Checkov | IaC, Dockerfiles and workflows |
| Conftest | The platform’s own policies (for example, base images pinned by digest) |
| Hadolint | Dockerfile style and pitfalls |
Two Gitleaks hits were false positives (a variable name, then the ignore file’s own comment). Both are suppressed by fingerprint in .gitleaksignore, so the rule still catches real secrets.
Artifact and container management
- Base images pinned by digest (
python:3.12-slim@sha256:…,uvcopied from a pinned image). uv sync --frozen --no-dev, runs as UID 10001, no writes needed (read-only root filesystem).- Pushed to Zot as
platform/job-hunter-agent; a CycloneDX SBOM from Syft is attested next to it. - Signed with cosign using a key held in OpenBao’s transit engine; Kyverno verifies signatures for
platform/*images.
Deployment automation
The image job’s last step commits the new digest to apps/job-hunter/kustomization.yaml in homelab-gitops (through a GitHub App, not a personal token) and calls Argo CD’s webhook so it syncs at once. The Deployment uses Recreate: the old pod stops before the new one starts, because only one Chromium may hold the LinkedIn profile.
Database deployment strategy
Schema changes don’t deploy themselves. The rule:
- Write an additive migration (
add column if not exists … default …) so the running image still works. - Open a PR; CI applies it to a fresh Supabase.
- Paste it into the Supabase SQL editor.
- Merge; the image deploys and starts using the new columns.
Skipping step 3 once meant profile saves failed until the column existed, which is why the order is written down.
Approval gates
There’s no human approval between main and production for app code; the gates are automated. Human gates exist where they matter: merging PRs in homelab-gitops and infra-local, applying migrations, and, in the product itself, approving every application.
Versioning and release notes
The image digest is the version; the homelab-gitops commit that promoted it is the release record, and its message names the job-hunter-agent commit. Release notes are the commit messages, written to say what changed and why. The app reports version 0.2.0 in its OpenAPI document, which nobody bumps.
No semantic version tags, the same as every other app on the platform. The build workflow tags each image with its commit SHA and deploys it by digest; the homelab-gitops commit that promotes it is the release record. A version number nobody bumps would add nothing.
Rollback and roll-forward procedures
- Roll back: revert the digest commit in homelab-gitops; Argo CD syncs the previous image. Because migrations are additive, the old image runs against the new schema.
- Roll forward: fix on
main; the pipeline promotes the new digest in a few minutes. - LinkedIn session: unaffected either way; it lives on the volume, not in the image.
1
2
git -C homelab-gitops revert <promote-commit> && git -C homelab-gitops push
kubectl --context k3d-platform -n job-hunter rollout status deploy/job-hunter
Release calendar
No calendar; releases happen when something’s ready. One soft rule: don’t deploy close to a profile’s scheduled time (08:00, 12:00, 16:00 IST on weekdays), because Recreate restarts the pod and a run in progress would stop. The scheduler keeps its jobs in memory, so a run that was due during the restart is skipped until that profile’s next slot.
Checklist
- Image job runs only on main and only with
HOMELAB_DEPLOY - All seven gates green; suppressions only by fingerprint
- Rollback by reverting the digest commit tried once
- Migrations applied before merge, every time
- No deploys within 15 minutes of a scheduled run
Decide on version tags (no: digests, like every other platform app)
