feat(notifications): post contract state changes to a Teams channel #9

Merged
macoun merged 2 commits from feat/teams-channel-notifications into main 2026-11-06 12:10:46 +00:00
Owner

The tracker only notified people by e-mail. The Teams channel had a slot in the outbox but no implementation, waiting for a Power Automate URL.

What changes

  • Card text is built from RichTextBlock/TextRun, not TextBlock, so markdown in a contract title or comment (e.g. a disguised link) shows as typed.
  • New TeamsChannel posts an Adaptive Card to a Teams Workflows "send webhook alerts to a channel" flow. It runs behind the outbox like e-mail, so retries, dead-letter and DryRun behave the same. The signed URL is never logged or stored in errors.
  • Only state changes go to the channel: one card per change for the whole team, with a link to the contract. Assignments and questions are personal and stay in e-mail. Practice contracts post nothing.
  • Off by default (Teams:Enabled=false). Switch it on and off in Portainer without removing the URL. Messages already queued while it is off are dropped with a note, not dead-lettered.
  • RedirectAllTo now applies to e-mail only.
  • POST /api/v1/admin/notifications/test-teams checks the channel after a deploy.
  • ADR-027 and a Teams section in the runbook.

Deploy

  1. The stack is pasted in Portainer's web editor: paste the updated docker-compose.yml first.
  2. Portainer: set TEAMS_WEBHOOK_URL (secret).
  3. When ready, set TEAMS_ENABLED=true and update the stack.
  4. Call POST /api/v1/admin/notifications/test-teams and check the channel.

Do not set the webhook URL on dev. It would post to the real channel.

Tests

dotnet build with 0 warnings, dotnet test 253 passed.

The tracker only notified people by e-mail. The Teams channel had a slot in the outbox but no implementation, waiting for a Power Automate URL. ## What changes - Card text is built from `RichTextBlock`/`TextRun`, not `TextBlock`, so markdown in a contract title or comment (e.g. a disguised link) shows as typed. - New `TeamsChannel` posts an Adaptive Card to a Teams Workflows "send webhook alerts to a channel" flow. It runs behind the outbox like e-mail, so retries, dead-letter and DryRun behave the same. The signed URL is never logged or stored in errors. - Only **state changes** go to the channel: one card per change for the whole team, with a link to the contract. Assignments and questions are personal and stay in e-mail. Practice contracts post nothing. - **Off by default** (`Teams:Enabled=false`). Switch it on and off in Portainer without removing the URL. Messages already queued while it is off are dropped with a note, not dead-lettered. - `RedirectAllTo` now applies to e-mail only. - `POST /api/v1/admin/notifications/test-teams` checks the channel after a deploy. - ADR-027 and a Teams section in the runbook. ## Deploy 0. The stack is pasted in Portainer's web editor: paste the updated `docker-compose.yml` first. 1. Portainer: set `TEAMS_WEBHOOK_URL` (secret). 2. When ready, set `TEAMS_ENABLED=true` and update the stack. 3. Call `POST /api/v1/admin/notifications/test-teams` and check the channel. Do not set the webhook URL on dev. It would post to the real channel. ## Tests `dotnet build` with 0 warnings, `dotnet test` 253 passed.
feat(notifications): post contract state changes to a Teams channel
All checks were successful
Build and publish Docker image / migrations-check (pull_request) Successful in 55s
Build and publish Docker image / frontend (pull_request) Successful in 46s
Build and publish Docker image / test (pull_request) Successful in 1m53s
Build and publish Docker image / build-and-push (pull_request) Has been skipped
65a293b63f
The tracker only notified people by e-mail. The Teams channel had a slot in
the outbox but no implementation, waiting for a Power Automate URL.

- Add TeamsChannel, which posts an Adaptive Card to the URL of a Teams
  Workflows "send webhook alerts to a channel" flow. It runs behind the outbox
  like e-mail, so retries, dead-letter and DryRun work the same way. The
  signed URL is never logged or stored in errors.
- Post only state changes, one card per change for the whole team, with a
  link to the contract. Assignments and questions are personal and stay in
  e-mail. Practice contracts post nothing.
- Keep Teams off by default (Teams:Enabled=false) so it can be switched on and
  off in Portainer without removing the URL. Messages already queued when it
  is off are dropped with a note instead of dead-lettering.
- Apply RedirectAllTo to e-mail only; it must not replace the channel target.
- Add POST /api/v1/admin/notifications/test-teams to check the channel after
  a deploy.
- Record the decision in ADR-027 and add a Teams section to the runbook.
fix(notifications): render user text in Teams cards without markdown
All checks were successful
Build and publish Docker image / migrations-check (pull_request) Successful in 41s
Build and publish Docker image / frontend (pull_request) Successful in 43s
Build and publish Docker image / test (pull_request) Successful in 1m41s
Build and publish Docker image / build-and-push (pull_request) Has been skipped
135217dd0b
TextBlock renders markdown, so a comment like "[Open contract](https://...)"
showed up as a link inside a card that looks like it comes from the system.
Build the card from RichTextBlock and TextRun, which show text as typed.

Also list the Teams stack variables for production in the runbook and warn
that the stack is pasted in Portainer's web editor, so a changed
docker-compose.yml has to be pasted again before the update.
macoun merged commit fe33112beb into main 2026-11-06 12:10:46 +00:00
macoun deleted branch feat/teams-channel-notifications 2026-11-06 12:10:46 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
STATOTEST/contract-microservice!9
No description provided.