Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ on:
pull_request:
branches: [main]

# Supersede in-flight runs on the same ref: a newer push always re-validates,
# so finishing the older ~5-minute run only burns a runner (same rationale as
# mutation.yml). On main this cancels runs for already-superseded merges too —
# the newest run still validates the final state.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
ci:
runs-on: ubuntu-latest
Expand Down
9 changes: 5 additions & 4 deletions .github/workflows/mutation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,11 @@ jobs:
id: stryker
run: npx stryker run
env:
# DATABASE_URL comes from the job-level env.
MONGODB_URI: mongodb://localhost:27017/hrmanager_audit_test
NEXTAUTH_SECRET: test-secret-for-ci
NEXTAUTH_URL: http://localhost:3000
# DATABASE_URL comes from the job-level env. Mongo needs no URL here —
# server tests use mongodb-memory-server. TOTP_ENCRYPTION_KEY keys
# totpCrypto for related tests that don't set their own (this was
# previously the legacy NEXTAUTH_SECRET fallback name).
TOTP_ENCRYPTION_KEY: test-secret-for-ci
NODE_ENV: test
continue-on-error: true

Expand Down
11 changes: 11 additions & 0 deletions k8s/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,17 @@ secrets:

---

## Scheduled work

Background maintenance runs through the CRON_SECRET-gated `/api/cron/*` routes,
mirroring the Vercel deployment's `vercel.json` crons:

- **Raw manifests** — `cronjob.yaml` ships two CronJobs (audit-outbox drain at
03:00, pg-boss job drain at 03:30). They read `CRON_SECRET` from
`hrmanager-secret`, so set that key before applying.
- **Helm** — disabled by default. Set `secrets.data.CRON_SECRET`, then deploy
with `--set cron.enabled=true` (schedules and paths under `cron.jobs`).

## Architecture notes

| Resource | Details |
Expand Down
44 changes: 44 additions & 0 deletions k8s/helm/hrmanager/templates/cronjob.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{{- if .Values.cron.enabled }}
{{- range .Values.cron.jobs }}
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: {{ include "hrmanager.fullname" $ }}-cron-{{ .name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "hrmanager.labels" $ | nindent 4 }}
app.kubernetes.io/component: cron
spec:
schedule: {{ .schedule | quote }}
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
activeDeadlineSeconds: 600
backoffLimit: 1
template:
spec:
restartPolicy: Never
containers:
- name: curl
image: {{ $.Values.cron.image }}
env:
- name: CRON_SECRET
valueFrom:
secretKeyRef:
name: {{ include "hrmanager.secretName" $ }}
key: CRON_SECRET
# The route accepts GET or POST with the same Bearer secret.
args:
- -fsS
- --max-time
- "300"
- -X
- POST
- -H
- "Authorization: Bearer $(CRON_SECRET)"
- http://{{ include "hrmanager.fullname" $ }}-svc:{{ $.Values.service.port }}{{ .path }}
{{- end }}
{{- end }}
15 changes: 14 additions & 1 deletion k8s/helm/hrmanager/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,22 @@ secrets:
AUTH_GITHUB_SECRET: ""
# The app reads MONGODB_URL — with any other name the audit trail no-ops.
MONGODB_URL: ""
# Optional: gates /api/cron/* if scheduled via CronJob/external scheduler.
# Required when cron.enabled=true (gates /api/cron/*).
# CRON_SECRET: ""

# -- Scheduled work (mirrors vercel.json's crons). Disabled by default because
# the endpoints 500/401 without secrets.data.CRON_SECRET — set that, then enable.
cron:
enabled: false
image: curlimages/curl:8.10.1
jobs:
- name: audit-drain
path: /api/cron/audit-drain
schedule: "0 3 * * *"
- name: jobs-drain
path: /api/cron/jobs
schedule: "30 3 * * *"

# -- Liveness probe configuration
livenessProbe:
httpGet:
Expand Down
82 changes: 82 additions & 0 deletions k8s/manifests/cronjob.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: hrmanager-cron-audit-drain
namespace: hrmanager
labels:
app.kubernetes.io/name: hrmanager
app.kubernetes.io/component: cron
app.kubernetes.io/managed-by: kubectl
spec:
# Mirrors vercel.json: durability backstop for the opportunistic after() drain.
schedule: "0 3 * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
activeDeadlineSeconds: 600
backoffLimit: 1
template:
spec:
restartPolicy: Never
containers:
- name: curl
image: curlimages/curl:8.10.1
env:
- name: CRON_SECRET
valueFrom:
secretKeyRef:
name: hrmanager-secret
key: CRON_SECRET
# The route accepts GET or POST with the same Bearer secret.
args:
- -fsS
- --max-time
- "300"
- -X
- POST
- -H
- "Authorization: Bearer $(CRON_SECRET)"
- http://hrmanager-svc:3000/api/cron/audit-drain
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: hrmanager-cron-jobs-drain
namespace: hrmanager
labels:
app.kubernetes.io/name: hrmanager
app.kubernetes.io/component: cron
app.kubernetes.io/managed-by: kubectl
spec:
# Mirrors vercel.json: drains the pg-boss queue (cleanup, audit exports).
schedule: "30 3 * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
activeDeadlineSeconds: 600
backoffLimit: 1
template:
spec:
restartPolicy: Never
containers:
- name: curl
image: curlimages/curl:8.10.1
env:
- name: CRON_SECRET
valueFrom:
secretKeyRef:
name: hrmanager-secret
key: CRON_SECRET
args:
- -fsS
- --max-time
- "300"
- -X
- POST
- -H
- "Authorization: Bearer $(CRON_SECRET)"
- http://hrmanager-svc:3000/api/cron/jobs
6 changes: 3 additions & 3 deletions k8s/manifests/secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,6 @@ data:
# (with any other name the audit trail silently no-ops).
# Replace with: echo -n 'mongodb://user:password@host:27017/hrmanager_audit' | base64
MONGODB_URL: "cmVwbGFjZV93aXRoX2Jhc2U2NF9lbmNvZGVkX3ZhbHVl"
# Optional: gates the /api/cron/* endpoints (audit drain, job drain, cleanup)
# if you schedule them with a CronJob or an external scheduler.
# CRON_SECRET: "cmVwbGFjZV93aXRoX2Jhc2U2NF9lbmNvZGVkX3ZhbHVl"
# Gates the /api/cron/* endpoints — required by the CronJobs in cronjob.yaml
# (audit drain + job drain). Generate with: openssl rand -base64 32
CRON_SECRET: "cmVwbGFjZV93aXRoX2Jhc2U2NF9lbmNvZGVkX3ZhbHVl"
Loading