Sync issue links via webhook, with a polling fallback, and detect a spec code typed in an issue #27

Closed
opened 2026-10-31 19:55:28 +00:00 by Gilles · 0 comments
Owner

Second slice of the Forgejo issue <-> spec link feature (see thierryade/backend#25/#26 for the first, and the plan discussed in chat).

Scope (PR 2 of 3):

  • ForgejoRepoWebhook model (one per repo, events=["issues"]), registered lazily by apps.specs.forgejo.ensure_webhook -- triggered when a Project.default_repo is set/changed, or when a link (manual/create-issue) targets a repo with none yet. Deregistered (remove_webhook_if_unused) once the last SpecVersionIssueLink pointing at a repo is removed.
  • POST /api/webhooks/forgejo/issues/<repo_full_name>/: verifies the delivery's HMAC signature (X-Gitea-Signature/X-Forgejo-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 in the title/body -- creates a detected_in_body link for a match not already linked, drops one whose code disappeared from a later edit (never a manual/issue_button link).
  • apps.specs.tasks.poll_stale_issue_links (Celery beat, every 15 min): re-fetches every (repo, issue) pair whose last sync is stale or missing, applying the same B1/B2 rules -- the polling fallback for a missed/never-registered webhook.
  • apps.specs.tasks.sync_confirmation_comment: posts (or edits in place, never reposts) a "🔗 Spec liée" comment on the issue, pointing back at the spec in Thierryade -- queued whenever a link is created (any of the three paths) or its cached_state changes.
  • New THIERRYADE_BASE_URL setting (builds the webhook callback URL and the comment's spec link) and CELERY_BEAT_SCHEDULE (no django-celery-beat: incompatible with this project's Django pin, so this uses Celery's own file-based scheduler -- a new thierryade-beat service, deploy/docker-compose.yml). Also wires FORGEJO_API_TOKEN_FILE into deploy/docker-compose.yml/README, missed in PR 1.

No extract-PDF-on-the-issue-comment yet -- that's PR 3.

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 test touching a creation/update path for a real outbound Forgejo call -- caught and mocked one that would otherwise have hit the live git.stationmyr.net instance (ensure_webhook/sync_confirmation_comment weren't mocked in a couple of PR 1 tests once this PR wired them into the same code paths)

Unverified against the real Forgejo instance: the webhook registration payload shape (type: "forgejo", config/events/secret) and the signature header name (X-Gitea-Signature vs X-Forgejo-Signature) are per Gitea/Forgejo's documented convention, not confirmed end-to-end against git.stationmyr.net yet -- worth a real smoke test once this is deployed, before relying on it.

Second slice of the Forgejo issue <-> spec link feature (see thierryade/backend#25/#26 for the first, and the plan discussed in chat). Scope (PR 2 of 3): - `ForgejoRepoWebhook` model (one per repo, `events=["issues"]`), registered lazily by `apps.specs.forgejo.ensure_webhook` -- triggered when a `Project.default_repo` is set/changed, or when a link (manual/create-issue) targets a repo with none yet. Deregistered (`remove_webhook_if_unused`) once the last `SpecVersionIssueLink` pointing at a repo is removed. - `POST /api/webhooks/forgejo/issues/<repo_full_name>/`: verifies the delivery's HMAC signature (`X-Gitea-Signature`/`X-Forgejo-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 in the title/body -- creates a `detected_in_body` link for a match not already linked, drops one whose code disappeared from a later edit (never a manual/`issue_button` link). - `apps.specs.tasks.poll_stale_issue_links` (Celery beat, every 15 min): re-fetches every (repo, issue) pair whose last sync is stale or missing, applying the same B1/B2 rules -- the polling fallback for a missed/never-registered webhook. - `apps.specs.tasks.sync_confirmation_comment`: posts (or edits in place, never reposts) a "🔗 Spec liée" comment on the issue, pointing back at the spec in Thierryade -- queued whenever a link is created (any of the three paths) or its `cached_state` changes. - New `THIERRYADE_BASE_URL` setting (builds the webhook callback URL and the comment's spec link) and `CELERY_BEAT_SCHEDULE` (no `django-celery-beat`: incompatible with this project's Django pin, so this uses Celery's own file-based scheduler -- a new `thierryade-beat` service, `deploy/docker-compose.yml`). Also wires `FORGEJO_API_TOKEN_FILE` into `deploy/docker-compose.yml`/README, missed in PR 1. No extract-PDF-on-the-issue-comment yet -- that's PR 3. ## 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 test touching a creation/update path for a real outbound Forgejo call -- caught and mocked one that would otherwise have hit the live `git.stationmyr.net` instance (`ensure_webhook`/`sync_confirmation_comment` weren't mocked in a couple of PR 1 tests once this PR wired them into the same code paths) **Unverified against the real Forgejo instance**: the webhook registration payload shape (`type: "forgejo"`, `config`/`events`/`secret`) and the signature header name (`X-Gitea-Signature` vs `X-Forgejo-Signature`) are per Gitea/Forgejo's documented convention, not confirmed end-to-end against `git.stationmyr.net` yet -- worth a real smoke test once this is deployed, before relying on it.
Sign in to join this conversation.
No description provided.