Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
34 changes: 34 additions & 0 deletions .github/workflows/forge-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: forge-ci
on:
workflow_dispatch:
pull_request:
paths:
- 'forge/**'
- '.github/workflows/forge-ci.yml'
push:
branches:
- master
paths:
- 'forge/**'
- '.github/workflows/forge-ci.yml'

defaults:
run:
working-directory: forge

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: forge/package.json
Comment thread
nang2049 marked this conversation as resolved.
- run: npm install --omit=optional --no-audit --no-fund
- run: npm audit --omit=optional --audit-level=moderate
- run: npm run typecheck
- run: npm run validate-manifest
- run: npm run build
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,10 @@ bin
# webapp test cases
webapp/build/test-results.xml
webapp/junit.xml

# Forge bridge
forge/dist/
forge/scripts-dist/
forge/node_modules/
forge/.forge/
forge/package-lock.json
60 changes: 0 additions & 60 deletions assets/templates/atlassian-connect.json

This file was deleted.

107 changes: 107 additions & 0 deletions forge/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Mattermost Confluence Forge bridge

This Forge app is the GA replacement for the Atlassian Connect webhook
descriptor that Atlassian closed for new installs on March 31, 2026.

## Shape

This is a **pull** bridge:

- The Forge app subscribes to 8 Confluence events via `trigger` modules
(page `created`/`updated`/`trashed`/`restored`/`deleted`, comment
`created`/`updated`/`deleted`) and enqueues each event payload into Forge
storage under `evt:<cloudId>:<ts>:<rand>`.
- The Mattermost plugin periodically POSTs to the `drain` web trigger to
read queued events and ack them. Requests are HMAC-SHA256 signed using a
shared secret the admin sets via the one-shot `register` web trigger.
- No `permissions.external.fetch` is declared. The Forge app never makes
outbound calls. This keeps the install consent screen clean and removes
the per-customer `manifest.yml` editing the previous push design required.

Trade-off: Atlassian's Forge `trigger` module already has up to 3 minutes
of delivery delay, so the additional ~30s polling latency we add on the
plugin side is small in context.

## Install (operator workflow)

We `forge deploy` this app once into the Mattermost Atlassian developer
account, then share a private install link. Customers do NOT run
`forge deploy` themselves.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine we'll be adding a workflow with secret env vars that can potentially handle the forge deployments to a specific version for the production deployments? I'm wondering whether we want to have something made in the delivery platform repo that can be shared between Confluence and Jira but we can figure this out as a follow-up

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thats also a good point, we need to keep in mind before merging and when we have the Forge app. Question is who might own this?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could look into that (likely with some help from someone in SRE), we might need to discuss the current game plan for how we'll deploy and maintain this in the next team meeting beforehand


1. `npm install` in this directory.
2. `forge login` and `forge register` (one-time, generates the app ID —
paste it into `manifest.yml` under `app.id`).
3. `forge deploy --environment production`.
4. `forge install --site https://<test-tenant>.atlassian.net` to verify on
a test tenant. For end-customer distribution, generate a private
distribution link from the Atlassian developer console.

## Wire-up (customer workflow)

1. Confluence admin clicks the install link → app installs on their site.
2. Confluence admin runs `forge webtrigger` (or reads the install logs)
to get the `register` and `drain` URLs. We'll wrap this in a UI Kit
admin page in a follow-up.
3. In Mattermost System Console under Plugins > Confluence:
- Paste the `drain` URL into "Forge Drain URL".
- Copy the auto-generated "Forge Bridge Shared Secret".
4. POST the secret to the `register` URL once:

```bash
curl -X POST -H 'Content-Type: application/json' \
-d '{"secret":"<paste shared secret>"}' \
'<register-web-trigger-url>'
```

`register` is one-shot — it refuses subsequent calls so a leaked URL
can't be used to repoint the bridge. To re-register (e.g. rotate the
secret), clear `mm.registered` from Forge storage first.

The Mattermost plugin then polls `drain` on a ticker, verifies each
event, posts to subscribed channels, and acks drained keys so Forge can
delete them.

## Develop

This directory is its own Node project, independent of the plugin's
`server/` and `webapp/` builds. CI lives in `.github/workflows/forge-ci.yml`
Comment thread
coderabbitai[bot] marked this conversation as resolved.
and only fires when `forge/**` changes.

```bash
npm install --omit=optional # CI install path
npm install # developer install path, pulls @forge/cli

npm run typecheck
npm run validate-manifest
npm run build
npm run ci # all of the above
npm run deploy # forge deploy
```

## Why these 8 events

Direct one-to-one mapping with what the legacy Atlassian Connect descriptor
used to subscribe to (page + comment lifecycle), re-validated against
[the Forge Confluence events list](https://developer.atlassian.com/platform/forge/events-reference/confluence/).
Forge collapses Connect's `removed` onto `deleted`. See
`server/forge_event_mapping.go` on the plugin side for the explicit mapping.

## Known limits

- Forge `trigger` delivery is best-effort (up to ~3 min delay, occasional
drops). Connect webhooks had the same property, so we are not
regressing. If drops show up in production we will add a plugin-side
reconciliation poll over `/wiki/api/v2/pages?sort=-modified-date`.
- Forge storage is wiped 28 days after uninstall — the queue is buffer,
not durable state. The plugin is the system of record.
- Forge web trigger limit: 1000 req/min per app/env/context. At a 30s
poll cadence, that's 2 req/min per tenant → headroom for ~500
installations per environment before throttling.

## Not in scope here

- Plugin-side Cloud 3LO OAuth (lives in `server/instance_cloud.go` and
the Cloud branches of `server/user.go` / `server/flow.go`).
- Plugin-side polling loop (lives in `server/forge_poller.go`).
- Migration of existing Connect installs (we leave those running until
Atlassian's Q4 2026 EOS).
71 changes: 71 additions & 0 deletions forge/manifest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
modules:
trigger:
- key: page-created
function: enqueueFn
events:
- avi:confluence:created:page
- key: page-updated
function: enqueueFn
events:
- avi:confluence:updated:page
- key: page-trashed
function: enqueueFn
events:
- avi:confluence:trashed:page
- key: page-restored
function: enqueueFn
events:
- avi:confluence:restored:page
- key: page-deleted
function: enqueueFn
events:
- avi:confluence:deleted:page
- key: comment-created
function: enqueueFn
events:
- avi:confluence:created:comment
- key: comment-updated
function: enqueueFn
events:
- avi:confluence:updated:comment
- key: comment-deleted
function: enqueueFn
events:
- avi:confluence:deleted:comment
- key: app-installed
function: onInstalledFn
events:
- avi:forge:installed:app

webtrigger:
- key: drain
function: drainFn
urlFormat: v2
response:
type: dynamic
- key: register
function: registerFn
urlFormat: v2
response:
type: dynamic

function:
- key: enqueueFn
handler: index.enqueue
- key: drainFn
handler: index.drain
- key: registerFn
handler: index.register
- key: onInstalledFn
handler: index.onInstalled

app:
runtime:
name: nodejs22.x
id: ari:cloud:ecosystem::app/e78f4d26-6f49-4a3e-a591-18fe466fc5b0

permissions:
scopes:
- storage:app
- read:confluence-content.summary
- write:confluence-content
26 changes: 26 additions & 0 deletions forge/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "mattermost-confluence-forge",
"version": "0.1.0",
"private": true,
"description": "Forge bridge that forwards Confluence Cloud events to the Mattermost Confluence plugin. Replaces the legacy Atlassian Connect descriptor path that Atlassian deprecated in 2026.",
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit",
"validate-manifest": "tsc -p tsconfig.scripts.json && node scripts-dist/validate-manifest.js",
"ci": "npm run typecheck && npm run validate-manifest && npm run build",
"deploy": "forge deploy",
"install:dev": "forge install --upgrade"
},
"dependencies": {
"@forge/api": "^4.0.0"
},
"devDependencies": {
"@types/js-yaml": "^4.0.9",
"@types/node": "^20.0.0",
"js-yaml": "^4.1.1",
"typescript": "^5.5.0"
},
"optionalDependencies": {
"@forge/cli": "^11.0.0"
}
}
3 changes: 3 additions & 0 deletions forge/scripts/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../tsconfig.scripts.json"
}
Loading
Loading