Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
f99e164
feat: added new messagebox buckets
pgaetani Jun 7, 2026
27dee17
refactor: shortened messagebox template conditional
pgaetani Jun 7, 2026
c6781a1
feat: extended messagebox endpoint request and response schemas
pgaetani Jun 7, 2026
6a4d499
feat: initial messagebox utils package and first antivirus upload for…
pgaetani Jun 7, 2026
cbcc393
test: messagebox unit test extension using faker
pgaetani Jun 7, 2026
805afa2
refactor: removal of asdf tool versions
pgaetani Jul 6, 2026
50fb2ea
build: added local rabbitmq option
pgaetani Jul 6, 2026
81e043a
feat: refactor messagebox attachment handling and antivirus scanning
pgaetani Jul 6, 2026
b4043bd
feat: add non-local debug configs and enhance celery settings
pgaetani Jul 6, 2026
75c17d4
fix(messagebox): don't resend a notification already past created
pgaetani Jul 16, 2026
fe5387f
fix(messagebox): stop resending stuck-sending notifications, persist …
pgaetani Jul 16, 2026
46d62b8
feat(messagebox): wire messagebox into the retention/BSN-purge pipeline
pgaetani Jul 16, 2026
b117c78
chore: add ebms-adapter-client dependency and NL dev tooling
pgaetani Jul 16, 2026
7a1c549
feat: add OIN column migration and validation for services
pgaetani Jul 16, 2026
4597042
feat(messagebox): add core ebms-adapter client, config, and celery wi…
pgaetani Jul 16, 2026
126733c
feat(messagebox): accept messagebox notifications via the v2 API
pgaetani Jul 16, 2026
b15fb4e
feat(messagebox): bill messagebox notifications as free
pgaetani Jul 16, 2026
abfc114
feat(messagebox): support multi-part precompiled letters and harden m…
pgaetani Jul 27, 2026
0e50a16
feat(messagebox): tag notifications on Sentry and enable structured logs
pgaetani Jul 30, 2026
18e4202
feat(letters): support ad-hoc PDF attachments on templated letter sends
pgaetani Jul 30, 2026
5022ccc
chore(dev): add create-letter-template command and VS Code tasks
pgaetani Jul 30, 2026
9ca0109
feat(messagebox): allow 0-2 attachments instead of 1-2
pgaetani Aug 5, 2026
842b1e9
feat(messagebox): allow overriding BerichtType per request via messag…
pgaetani Aug 5, 2026
45c26b7
fix(migrations_nl): renumber messagebox migrations past develop's 001…
pgaetani Aug 5, 2026
09824d5
fix: replace removed dao_utils.transaction() with _autocommit pattern
pgaetani Aug 5, 2026
9487a2a
fix(test-deps): pin pytest-env<1.2 to avoid --envfile clash with pyte…
pgaetani Aug 5, 2026
d279309
fix(billing): correct messagebox billing query bugs surfaced by rebase
pgaetani Aug 5, 2026
462a81f
fix(schemas): accept data_key kwarg in ServiceSchema.validate_oin
pgaetani Aug 5, 2026
7a7970f
fix(letters): restore MessageGroupId on templated-letter PDF dispatch
pgaetani Aug 5, 2026
897da5c
fix(tests): remove stray trailing comma breaking parametrize collection
pgaetani Aug 5, 2026
40d6d5d
fix(config): remove duplicate DevNL.CELERY definition
pgaetani Aug 5, 2026
581765f
chore: switch ebms-adapter-client from local editable to PyPI 0.3.0
pgaetani Aug 5, 2026
1eaa8bb
fix(config): remove redundant 'notifications' segment from CSV upload…
pgaetani Aug 10, 2026
fd5cd70
feat(migrations): support Alembic downgrade for automated Helm rollback
pgaetani Aug 10, 2026
b2c82da
ci: add pre-flight dry-run validation before Test env deploy
pgaetani Aug 10, 2026
0805730
ci: deploy Test env atomically with this PR's actual migration heads
pgaetani Aug 10, 2026
55c95af
ci: pass image tag to the new jobs-save/jobs-save-documents workers
pgaetani Aug 10, 2026
05e6d22
ci: pass image tag to the new service-callbacks-retry/messagebox workers
pgaetani Aug 10, 2026
8fa85ca
fix(tests): stop leaking ANTIVIRUS_ENABLED config across the test ses…
pgaetani Aug 11, 2026
0b8389e
fix(deps): bump pypdf to 6.15.0 to fix DoS advisories
pgaetani Aug 11, 2026
4bcb1fc
ci: checkout notifynl-charts-private before notifynl-api
pgaetani Aug 11, 2026
41ab76a
ci: use --rollback-on-failure instead of --atomic for Test deploy
pgaetani Aug 11, 2026
a665ada
ci: auto-compile requirements*.txt via pre-commit
pgaetani Aug 11, 2026
6c2f137
fix(deps): allow pypdf 6.15.0 past exclude-newer quarantine
pgaetani Aug 11, 2026
f4f1f9b
ci: replace bwvolleyball/k8s-toolkit with official azure actions
pgaetani Aug 11, 2026
7af9588
ci: base64-decode K8S_CONFIG before azure/k8s-set-context
pgaetani Aug 11, 2026
e3d884e
Merge branch 'develop' into feat/messagebox-transfer
pgaetani Aug 11, 2026
3228558
ci: pass image tag to migrations.apiUpgrade in Test deploy
pgaetani Aug 11, 2026
bec3108
feat(letters): add per-service letter address placement setting
pgaetani Aug 12, 2026
d300daa
fix(letters): resolve migration number clash and marshmallow validato…
pgaetani Aug 12, 2026
95841d6
chore: bump notifynl-utils to 101.2.1
pgaetani Aug 12, 2026
20e88c4
fix(messagebox): stop wrapping organisation_id in a tuple in POST res…
pgaetani Aug 12, 2026
4e1ff28
fix(services): bust SerialisedService redis cache on update/archive
pgaetani Aug 13, 2026
6503b67
docs(letters): mark letter and messagebox sending as supported in NL
pgaetani Aug 14, 2026
31fb3c4
docs(letters): document attachments and multi-file precompiled letter…
pgaetani Aug 14, 2026
2ce356b
docs(messagebox): rename berichtenbox endpoint/schemas to messagebox …
pgaetani Aug 14, 2026
395c7db
docs(templates): add TemplatePreviewResponse schema for the preview e…
pgaetani Aug 14, 2026
0126eb5
docs: document one_click_unsubscribe_url and estimated_delivery notif…
pgaetani Aug 14, 2026
b671a5d
docs(verify): expand verification endpoint response documentation
pgaetani Aug 14, 2026
6accfc0
docs: accept capitalized True for the batch-upload include filter
pgaetani Aug 14, 2026
339ee85
chore(deps): bump notifications-utils pin to 101.2.3
rodrigoAbril Aug 17, 2026
523f1bd
chore(deps): regenerate requirements_nl_test.txt for notifications-ut…
rodrigoAbril Aug 17, 2026
5324328
fix(organisation): use NotifyNL branding for platform-admin invite se…
rodrigoAbril Aug 20, 2026
358823b
fix(organisation): use NotifyNL branding in expired invitation error …
rodrigoAbril Aug 20, 2026
760a148
fix(organisation): translate expired invitation message to Dutch
rodrigoAbril Aug 20, 2026
18c5b5f
fix(service-invite): use NotifyNL branding in expired invitation erro…
rodrigoAbril Aug 20, 2026
45b4c16
fix(auth): point invalid-token error message at NotifyNL docs
rodrigoAbril Aug 20, 2026
ce03d43
fix(notifications): translate trial-mode error message to Dutch
rodrigoAbril Aug 20, 2026
4fc8903
fix(migrations): translate remaining system emails to Dutch/NotifyNL …
rodrigoAbril Aug 20, 2026
6271cf6
fix: keep DVLA letter-callback URL on the internal API host
pgaetani Aug 20, 2026
2368906
fix(letters): fix DVLA letter-volume email postage counts and transla…
rodrigoAbril Aug 20, 2026
10edf77
fix(letters): disable DVLA letter-volume report email
rodrigoAbril Aug 20, 2026
5150551
ci: add daily GHCR cleanup for stale -pr image tags
pgaetani Aug 21, 2026
8f4e221
chore(vscode): removal of recommended extensions
pgaetani Aug 21, 2026
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
69 changes: 69 additions & 0 deletions .github/workflows/ghcr-tag-cleanup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: GHCR tag cleanup

on:
schedule:
- cron: '23 2 * * *' # 02:23 UTC daily
workflow_dispatch: {}

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

permissions:
contents: read

env:
PACKAGE_OWNER: worth-nl
PACKAGE_NAME: notifynl-api
KEEP_PR_TAGS: 10

jobs:
cleanup:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Round up all image tags (daily report)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail

if ! gh api --paginate \
"/orgs/${PACKAGE_OWNER}/packages/container/${PACKAGE_NAME}/versions?per_page=100" \
> /tmp/pages.jsonl 2> /tmp/gh-api.err; then
echo "::warning::Could not list package versions for ${PACKAGE_OWNER}/${PACKAGE_NAME}"
cat /tmp/gh-api.err
echo "## GHCR tag report — ${PACKAGE_NAME}" >> "$GITHUB_STEP_SUMMARY"
echo "Could not list package versions (see job log)." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
jq -s 'add // []' /tmp/pages.jsonl > /tmp/versions.json

{
echo "## GHCR tag report — ${PACKAGE_NAME}"
echo
echo "_Run: $(date -u +'%Y-%m-%d %H:%M UTC') — $(jq 'length' /tmp/versions.json) total versions in \`ghcr.io/${PACKAGE_OWNER}/${PACKAGE_NAME}\`_"
echo
echo "| Tag(s) | Created (UTC) |"
echo "|---|---|"
jq -r '
sort_by(.created_at) | reverse | .[]
| [ ((.metadata.container.tags // []) | if length == 0 then "_(untagged)_" else join(", ") end), .created_at ]
| "| " + .[0] + " | " + .[1] + " |"
' /tmp/versions.json
} >> "$GITHUB_STEP_SUMMARY"

- name: Delete stale -pr tags (keep newest 10)
uses: snok/container-retention-policy@v3.1.0
with:
account: ${{ env.PACKAGE_OWNER }}
token: ${{ secrets.GITHUB_TOKEN }}
image-names: ${{ env.PACKAGE_NAME }}
image-tags: "*-pr"
tag-selection: tagged
cut-off: 1s
keep-n-most-recent: ${{ env.KEEP_PR_TAGS }}
dry-run: false
159 changes: 123 additions & 36 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -209,70 +209,157 @@ jobs:
contents: read
pull-requests: write

env:
HELM_SET_ARGS: >-
--set apps.api.image.tag=${{ needs.build.outputs.tag }}
--set apps.apiWorkerRetryTasks.image.tag=${{ needs.build.outputs.tag }}
--set apps.apiWorkerLetters.image.tag=${{ needs.build.outputs.tag }}
--set apps.apiWorkerJobs.image.tag=${{ needs.build.outputs.tag }}
--set apps.apiWorkerJobsSave.image.tag=${{ needs.build.outputs.tag }}
--set apps.apiWorkerJobsSaveDocuments.image.tag=${{ needs.build.outputs.tag }}
--set apps.apiWorkerResearch.image.tag=${{ needs.build.outputs.tag }}
--set apps.apiWorkerSender.image.tag=${{ needs.build.outputs.tag }}
--set apps.apiWorkerSenderLetters.image.tag=${{ needs.build.outputs.tag }}
--set apps.apiWorkerPeriodic.image.tag=${{ needs.build.outputs.tag }}
--set apps.apiWorkerReporting.image.tag=${{ needs.build.outputs.tag }}
--set apps.apiWorkerInternal.image.tag=${{ needs.build.outputs.tag }}
--set apps.apiWorkerReceipts.image.tag=${{ needs.build.outputs.tag }}
--set apps.apiWorkerServiceCallbacks.image.tag=${{ needs.build.outputs.tag }}
--set apps.apiWorkerServiceCallbacksRetry.image.tag=${{ needs.build.outputs.tag }}
--set apps.apiWorkerMessagebox.image.tag=${{ needs.build.outputs.tag }}
--set apps.apiWorkerReportRequestsNotificationsTasks.image.tag=${{ needs.build.outputs.tag }}
--set apps.celeryBeat.image.tag=${{ needs.build.outputs.tag }}
--set migrations.apiUpgrade.image.tag=${{ needs.build.outputs.tag }}

steps:
# Order matters: actions/checkout defaults to `clean: true`, which runs
# `git clean -ffdx` in its checkout path. A checkout with no `path:`
# (this one) targets the workspace root, and that clean can wipe a
# sibling directory checked out earlier by a separate actions/checkout
# step (https://github.com/orgs/community/discussions/27030) — so the
# root-targeting checkout must run first, before notifynl-api exists.
- uses: actions/checkout@v4
with:
repository: Worth-NL/notifynl-charts-private
ref: main
token: ${{ secrets.RELEASE_TOKEN }}
name: Checkout Worth-NL/notifynl-charts-private

- uses: bwvolleyball/k8s-toolkit@v1.0.0
- uses: actions/checkout@v4
with:
path: notifynl-api
name: Checkout notifynl-api (this PR)

# K8S_CONFIG is stored base64-encoded (shared with the other submodules'
# workflows, which still use bwvolleyball/k8s-toolkit and decode it
# internally). azure/k8s-set-context's `kubeconfig` method expects raw
# kubeconfig YAML, not base64, so decode it here first.
- name: Decode kubeconfig
id: decode-kubeconfig
run: |
{
echo "content<<KUBECONFIG_EOF"
echo "${{ secrets.K8S_CONFIG }}" | base64 -d
echo "KUBECONFIG_EOF"
} >> "$GITHUB_OUTPUT"

- uses: azure/k8s-set-context@v4
with:
method: kubeconfig
kubeconfig: ${{ steps.decode-kubeconfig.outputs.content }}

- uses: azure/setup-helm@v4
with:
config: ${{ secrets.K8S_CONFIG }}
version: v4.2.2

- name: Helm chart dependency build
run: helm dependency build
working-directory: ${{ github.workspace }}/notifynl-full

# notifynl-full pins migrations.apiUpgrade.{alphagovRevision,nlRevision}
# explicitly per release rather than implicitly running whatever HEAD
# the image bundles. A PR build's image can bundle newer migrations
# than the chart's currently pinned defaults, so derive the two heads
# from this PR's own migration trees (mirrors tests/test_migrations.py's
# "sorted glob, last file wins" head detection) and override them below.
- name: Determine migration revisions
id: migration-revisions
run: |
set -euo pipefail
alphagov_revision=$(basename "$(ls notifynl-api/migrations/versions/*.py | sort | tail -1)" .py)
nl_revision=$(basename "$(ls notifynl-api/migrations_nl/versions/*.py | sort | tail -1)" .py | cut -d_ -f1)
echo "alphagov-revision=$alphagov_revision" >> "$GITHUB_OUTPUT"
echo "nl-revision=$nl_revision" >> "$GITHUB_OUTPUT"

# Renders the chart and validates against the live cluster API/CRDs
# (required/fail template checks included) without persisting anything
# or scheduling any pods. Uses the exact same flags as the real deploy
# below so it can't drift out of sync and give a false pass.
- name: Pre-flight validation (dry-run, no cluster mutation)
id: helm-dry-run
run: |
set -x
helm upgrade --install notifynl notifynl-full/ \
-n ${{ vars.NAMESPACE }} \
--reset-then-reuse-values \
$HELM_SET_ARGS \
--set migrations.apiUpgrade.alphagovRevision=${{ steps.migration-revisions.outputs.alphagov-revision }} \
--set migrations.apiUpgrade.nlRevision=${{ steps.migration-revisions.outputs.nl-revision }} \
--rollback-on-failure \
--dry-run=server

- name: Deploy new tag
id: helm-deploy
run: |
set -x
helm upgrade --install notifynl notifynl-full/ \
-n ${{ vars.NAMESPACE }} \
--reuse-values \
--set apps.api.image.tag=${{ needs.build.outputs.tag }} \
--set apps.apiWorkerRetryTasks.image.tag=${{ needs.build.outputs.tag }} \
--set apps.apiWorkerLetters.image.tag=${{ needs.build.outputs.tag }} \
--set apps.apiWorkerJobs.image.tag=${{ needs.build.outputs.tag }} \
--set apps.apiWorkerResearch.image.tag=${{ needs.build.outputs.tag }} \
--set apps.apiWorkerSender.image.tag=${{ needs.build.outputs.tag }} \
--set apps.apiWorkerSenderLetters.image.tag=${{ needs.build.outputs.tag }} \
--set apps.apiWorkerPeriodic.image.tag=${{ needs.build.outputs.tag }} \
--set apps.apiWorkerReporting.image.tag=${{ needs.build.outputs.tag }} \
--set apps.apiWorkerInternal.image.tag=${{ needs.build.outputs.tag }} \
--set apps.apiWorkerReceipts.image.tag=${{ needs.build.outputs.tag }} \
--set apps.apiWorkerServiceCallbacks.image.tag=${{ needs.build.outputs.tag }} \
--set apps.apiWorkerReportRequestsNotificationsTasks.image.tag=${{ needs.build.outputs.tag }} \
--set apps.celeryBeat.image.tag=${{ needs.build.outputs.tag }} \
--wait \
--reset-then-reuse-values \
$HELM_SET_ARGS \
--set migrations.apiUpgrade.alphagovRevision=${{ steps.migration-revisions.outputs.alphagov-revision }} \
--set migrations.apiUpgrade.nlRevision=${{ steps.migration-revisions.outputs.nl-revision }} \
--rollback-on-failure \
--timeout 300s

- name: Comment on PR if deployment fails
if: failure() && steps.helm-deploy.outcome == 'failure' && github.event_name == 'pull_request'
if: failure() && (steps.helm-dry-run.outcome == 'failure' || steps.helm-deploy.outcome == 'failure') && github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const dryRunFailed = "${{ steps.helm-dry-run.outcome }}" === "failure";
const body = dryRunFailed
? [
"⚠️ **Test environment deployment blocked (pre-flight check failed)**",
"",
"- Environment: `Test`",
"- Image tag: `${{ needs.build.outputs.tag }}`",
"",
"The pre-flight `helm upgrade --dry-run=server` check failed, so the real deploy never ran — **the cluster was not touched**, nothing to clean up.",
"",
"Check the workflow logs for the Helm error output. Most likely cause: `notifynl-full` added a new value with no chart default that this release doesn't have set yet — seed it once with a manual `helm upgrade --set <key>=<value>`, and future deploys will carry it forward automatically."
].join("\n")
: [
"❌ **Test environment deployment failed**",
"",
"- Environment: `Test`",
"- Image tag: `${{ needs.build.outputs.tag }}`",
"",
"The pre-flight check passed but the real deploy failed — the cluster may be in a partial state.",
"",
"Check the workflow logs for the Helm error output.",
"",
"Check the test env k8s state/logs.",
"",
"Check if the helm release needs a rollback.",
"",
"Most likely issues:",
"- DB migration desync",
"- Package visibility",
"- ghcr.io credentials expiration or change"
].join("\n");
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: [
"❌ **Test environment deployment failed**",
"",
"- Environment: `Test`",
"- Image tag: `${{ needs.build.outputs.tag }}`",
"",
"Check the workflow logs for the Helm error output.",
"",
"Check the test env k8s state/logs.",
"",
"Check if the helm release needs a rollback.",
"",
"Most likely issues:",
"- DB migration desync",
"- Package visibility",
"- ghcr.io credentials expiration or change"
].join("\n")
body
})
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -896,7 +896,7 @@ target/
environment.sh
.envrc

celerybeat-schedule
celerybeat-schedule*

# CloudFoundry
.cf
Expand Down
19 changes: 19 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,22 @@ repos:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
rev: 0.12.3
hooks:
- id: pip-compile
name: pip-compile requirements.in
args: [requirements.in, -o, requirements.txt]
- id: pip-compile
name: pip-compile requirements_for_test.in
args: [requirements_for_test.in, -o, requirements_for_test.txt]
files: ^requirements_for_test\.(in|txt)$
- id: pip-compile
name: pip-compile requirements_nl.in
args: [requirements_nl.in, -o, requirements_nl.txt]
files: ^requirements_nl\.(in|txt)$
- id: pip-compile
name: pip-compile requirements_nl_test.in
args: [requirements_nl_test.in, -o, requirements_nl_test.txt]
files: ^requirements_nl_test\.(in|txt)$
1 change: 0 additions & 1 deletion .tool-versions

This file was deleted.

9 changes: 6 additions & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,13 @@
"run_celery.notify_celery",
"worker",
"-l",
"INFO",
"DEBUG",
"-E",
"-P",
"solo"
"solo",
"--without-gossip",
"--without-mingle",
"--without-heartbeat"
],
"consoleName": "API worker",
"preLaunchTask": "API pre-launch"
Expand All @@ -47,7 +50,7 @@
"run_celery.notify_celery",
"beat",
"-l",
"INFO"
"DEBUG"
],
"consoleName": "Celery beat",
"preLaunchTask": "API pre-launch"
Expand Down
19 changes: 17 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
{
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
"python.testing.pytestEnabled": true,
"python.defaultInterpreterPath": ".venv/bin/python",
"python.terminal.useEnvFile": true,
"python-envs.pythonProjects": [
{
"path": ".",
"envManager": "ms-python.python:venv",
"packageManager": "ms-python.python:pip"
}
]
],
"emeraldwalk.runonsave": {
"commands": [
{
"match": "requirements_nl_test\\.in$",
"cmd": "cd ${workspaceFolder} && uv pip compile requirements_nl_test.in -o requirements_nl_test.txt",
"isAsync": false
},
{
"match": "requirements_nl\\.in$",
"cmd": "cd ${workspaceFolder} && uv pip compile requirements_nl.in -o requirements_nl.txt && uv pip compile requirements_nl_test.in -o requirements_nl_test.txt",
"isAsync": false
}
]
}
}
Loading
Loading