feature: sync issue links via webhook, poll as fallback, detect spec codes in bodies #28

Merged
Gilles merged 2 commits from feat/issue-link-webhook-sync into master 2026-10-31 21:03:50 +00:00
Owner

Closes #27.

Second slice of the Forgejo issue <-> spec link feature, on top of thierryade/backend#26 (now merged).

  • ForgejoRepoWebhook model (one per repo, events=["issues"]), registered lazily by apps.specs.forgejo.ensure_webhook when a Project.default_repo is set/changed or a link targets an unseen repo; deregistered (remove_webhook_if_unused) once the last link on a repo is removed.
  • POST /api/webhooks/forgejo/issues/<repo_full_name>/: verifies the delivery's HMAC signature against the repo's own secret, then (B1) syncs cached_state/cached_title on every existing link to that issue, and (B2) on an opened/edited delivery, detects a spec code typed on its own Spec: CODE / Thierryade: CODE line -- creates a detected_in_body link for a new match, drops one whose code disappeared (never a manual/issue_button link).
  • apps.specs.tasks.poll_stale_issue_links (Celery beat, every 15 min): re-fetches every stale (repo, issue) pair, same B1/B2 rules -- the polling fallback for a missed/never-registered webhook.
  • apps.specs.tasks.sync_confirmation_comment: posts (or edits in place) a confirmation comment on the issue, pointing back at the spec in Thierryade -- queued on link creation or a cached_state change.
  • New THIERRYADE_BASE_URL setting + CELERY_BEAT_SCHEDULE (no django-celery-beat -- incompatible with this project's Django pin -- so Celery's own file-based scheduler, a new thierryade-beat service). Also wires FORGEJO_API_TOKEN_FILE into deploy/docker-compose.yml/README, missed in PR 1.

Test plan

  • manage.py test -- 197/197 passing (35 new)
  • pylint apps config -- 10.00/10
  • autopep8 --diff clean
  • bandit -r apps config -- 0 issues
  • Migration applies cleanly on a fresh database
  • Audited every link-creation test for a real outbound Forgejo call -- caught and mocked ensure_webhook/sync_confirmation_comment in a couple of PR 1 tests this PR wired into the same paths, which would otherwise have hit the live git.stationmyr.net instance

Unverified against the real Forgejo instance: the webhook registration payload shape and the signature header name (X-Gitea-Signature vs X-Forgejo-Signature) follow Gitea/Forgejo's documented convention, not confirmed end-to-end yet -- worth a real smoke test once deployed.


🤖 Generated with Claude Code

https://claude.ai/code/session_014HiJrn6J6vhyFfC2qyY4uh

Closes #27. Second slice of the Forgejo issue <-> spec link feature, on top of thierryade/backend#26 (now merged). - `ForgejoRepoWebhook` model (one per repo, `events=["issues"]`), registered lazily by `apps.specs.forgejo.ensure_webhook` when a `Project.default_repo` is set/changed or a link targets an unseen repo; deregistered (`remove_webhook_if_unused`) once the last link on a repo is removed. - `POST /api/webhooks/forgejo/issues/<repo_full_name>/`: verifies the delivery's HMAC signature against the repo's own secret, then (B1) syncs `cached_state`/`cached_title` on every existing link to that issue, and (B2) on an `opened`/`edited` delivery, detects a spec code typed on its own `Spec: CODE` / `Thierryade: CODE` line -- creates a `detected_in_body` link for a new match, drops one whose code disappeared (never a manual/`issue_button` link). - `apps.specs.tasks.poll_stale_issue_links` (Celery beat, every 15 min): re-fetches every stale (repo, issue) pair, same B1/B2 rules -- the polling fallback for a missed/never-registered webhook. - `apps.specs.tasks.sync_confirmation_comment`: posts (or edits in place) a confirmation comment on the issue, pointing back at the spec in Thierryade -- queued on link creation or a `cached_state` change. - New `THIERRYADE_BASE_URL` setting + `CELERY_BEAT_SCHEDULE` (no `django-celery-beat` -- incompatible with this project's Django pin -- so Celery's own file-based scheduler, a new `thierryade-beat` service). Also wires `FORGEJO_API_TOKEN_FILE` into `deploy/docker-compose.yml`/README, missed in PR 1. ## Test plan - [x] `manage.py test` -- 197/197 passing (35 new) - [x] `pylint apps config` -- 10.00/10 - [x] `autopep8 --diff` clean - [x] `bandit -r apps config` -- 0 issues - [x] Migration applies cleanly on a fresh database - [x] Audited every link-creation test for a real outbound Forgejo call -- caught and mocked `ensure_webhook`/`sync_confirmation_comment` in a couple of PR 1 tests this PR wired into the same paths, which would otherwise have hit the live `git.stationmyr.net` instance **Unverified against the real Forgejo instance**: the webhook registration payload shape and the signature header name (`X-Gitea-Signature` vs `X-Forgejo-Signature`) follow Gitea/Forgejo's documented convention, not confirmed end-to-end yet -- worth a real smoke test once deployed. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_014HiJrn6J6vhyFfC2qyY4uh
feature: sync issue links via webhook, poll as fallback, detect spec codes in bodies
All checks were successful
ci / test (pull_request) Successful in 3m31s
ci / e2e (pull_request) Has been skipped
ci / docker (pull_request) Has been skipped
ci / deploy (pull_request) Has been skipped
3906c9e1f9
Closes #27.

Builds on thierryade/backend#26 (not yet merged -- this branch is
stacked on feat/spec-issue-links, base targets it rather than master).

- ForgejoRepoWebhook model (one per repo, events=["issues"]), registered
  lazily by apps.specs.forgejo.ensure_webhook when a Project.default_repo
  is set/changed or a link targets an unseen repo; deregistered
  (remove_webhook_if_unused) once the last link on a repo is removed.
- POST /api/webhooks/forgejo/issues/<repo_full_name>/: verifies the
  delivery's HMAC signature against the repo's own secret, then (B1)
  syncs cached_state/cached_title on every existing link to that issue,
  and (B2) on an opened/edited delivery, detects a spec code typed on
  its own "Spec: CODE" / "Thierryade: CODE" line -- creates a
  detected_in_body link for a new match, drops one whose code
  disappeared (never a manual/issue_button link).
- apps.specs.tasks.poll_stale_issue_links (Celery beat, every 15 min):
  re-fetches every stale (repo, issue) pair, same B1/B2 rules -- the
  polling fallback for a missed/never-registered webhook.
- apps.specs.tasks.sync_confirmation_comment: posts (or edits in
  place) a confirmation comment on the issue, pointing back at the
  spec in Thierryade -- queued on link creation or a cached_state
  change.
- New THIERRYADE_BASE_URL setting + CELERY_BEAT_SCHEDULE (no
  django-celery-beat -- incompatible with this project's Django pin --
  so Celery's own file-based scheduler, a new thierryade-beat service).
  Also wires FORGEJO_API_TOKEN_FILE into deploy/docker-compose.yml and
  the README, missed in PR 1.

## Test plan
- [x] `manage.py test` -- 197/197 passing (35 new)
- [x] `pylint apps config` -- 10.00/10
- [x] `autopep8 --diff` clean
- [x] `bandit -r apps config` -- 0 issues
- [x] Migration applies cleanly on a fresh database
- [x] Audited every link-creation test for a real outbound Forgejo
  call -- caught and mocked ensure_webhook/sync_confirmation_comment
  in a couple of PR 1 tests that this PR wired into the same paths,
  which would otherwise have hit the live git.stationmyr.net instance

**Unverified against the real Forgejo instance**: the webhook
registration payload shape and the signature header name
(X-Gitea-Signature vs X-Forgejo-Signature) follow Gitea/Forgejo's
documented convention, not confirmed end-to-end yet -- worth a real
smoke test once deployed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HiJrn6J6vhyFfC2qyY4uh
Author
Owner
<!-- argus-link-bot:thierryade/backend --> 📊 [Rapports Argus pour cette PR](https://reports.stationmyr.net/thierryade/backend/pr/28)
fix: use django-celery-beat-next for CELERY_BEAT_SCHEDULE until upstream supports Django 6.1
All checks were successful
ci / test (pull_request) Successful in 3m24s
ci / e2e (pull_request) Has been skipped
ci / docker (pull_request) Has been skipped
ci / deploy (pull_request) Has been skipped
71bb26deff
Replaces the file-based Celery scheduler with DatabaseScheduler --
django-celery-beat-next, not the official django-celery-beat (every
recent release of it caps Django at <6.1, incompatible with this
project's Django>=6.1,<6.2 pin). Same django_celery_beat app
label/import name and DB schema, an unofficial drop-in fork tracking
upstream's own unreleased Django 6.1 support.

- INSTALLED_APPS += "django_celery_beat", CELERY_BEAT_SCHEDULER =
  "django_celery_beat.schedulers:DatabaseScheduler".
- CELERY_BEAT_SCHEDULE now only bootstraps the PeriodicTask row on
  the beat process's first tick -- editable from /admin afterward
  without a redeploy, unlike the file-based scheduler it replaces.
- thierryade-beat now needs postgres (DatabaseScheduler reads/writes
  its schedule there), so it gets DATABASE_URL_FILE + the postgres
  network, same shape as thierryade-worker.

Same temporary-workaround framing as argus/backend's own switch to
this fork earlier today -- revert to upstream django-celery-beat on
both once it officially supports Django 6.1 (same app label, no
migration to redo).

## Test plan
- [x] `manage.py test` -- 197/197 passing (no new tests -- a dependency
  swap behind an already-tested setting, not new behavior)
- [x] `pylint apps config` -- 10.00/10
- [x] `autopep8 --diff` clean
- [x] `bandit -r apps config` -- 0 issues
- [x] `makemigrations --check` -- no changes (django_celery_beat ships
  its own migrations, applied cleanly on a fresh sqlite database)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HiJrn6J6vhyFfC2qyY4uh
Gilles scheduled this pull request to auto merge when all checks succeed 2026-10-31 21:00:54 +00:00
Gilles merged commit 10fa6c1afd into master 2026-10-31 21:03:50 +00:00
Gilles deleted branch feat/issue-link-webhook-sync 2026-10-31 21:03:50 +00:00
Sign in to join this conversation.
No description provided.