Smart Announcement Banner for Bitbucket
Bitbucket v0.1.0 · Verified live
Targeted, scheduled announcements for Bitbucket, published at workspace, project or repository level, with per-reader acknowledgement tracking.
Overview
Three scope levels are what makes this app different from its Jira and Confluence siblings, and the level is derived server-side from Bitbucket’s own context — never from the client. The workspace settings console publishes to everyone in the workspace; the project console reaches every repository in that project; the repository console reaches that repository only.
Readers get them in three places: a compact strip at the top of the repository source page, the full announcement centre under Repository → Apps, and a workspace-level centre under Workspace → Apps that reaches announcements outside any repository.
Smart Announcement Banner is one of three sibling apps — Jira, Confluence and Bitbucket. They are separate apps because Forge app storage is scoped to the installation: installing one app on both Jira and Confluence creates two independent installations with completely separate storage, so a single app could never deliver “publish this announcement to Jira and Confluence”. Splitting makes that platform reality the design instead of a footnote. Bitbucket was already separate for a different reason — a single Forge app cannot mix Bitbucket workspace scopes with Jira/Confluence site scopes.
Highlights
Five announcement types, with a preview that cannot lie
Critical, Warning, Information, Success and Tip, each with its own colour, icon and lozenge; priority 1–5 plus a pin flag; an optional call-to-action button. The live preview renders the real reader component fed real normalized data, so what an admin previews is what ships.
Scheduling in the admin’s timezone, stored as UTC
Optional start and end instants, with a derived Live / Scheduled / Expired / Paused status computed against a server timestamp so every surface agrees.
Three dismissal modes
Require acknowledgement, dismissible with an optional snooze that brings it back, or always visible.
Delegated administration that gains no extra reach
A delegated console lists, reports on and can mutate only its own announcements. Readers see the levels merged, each tagged with its origin.
Insights
Acknowledgement and dismissal counts, distinct readers reached, breakdown by type, most-engaged announcements, and a recent reader-activity feed. “Reset reads” re-shows an announcement to everyone who already cleared it.
Bounded, conflict-safe storage
Announcements, target generations, current reader engagement, 90-day activity and the personal-account registry are separate entities. Conditional revisions prevent concurrent-admin clobbering; engagement epochs make “Reset reads” immediate without losing permanent acknowledgements to an event cap.
Badges that name the project
Two projects can easily hold announcements with the same title, and a repository reader sees its own project’s notices merged with the workspace’s. A bare PROJECT badge is not enough, so the badge names it (PROJECT TES). For records that predate the key being captured, it is filled in at read time — whoever is looking at a project already knows its key.
Placement, and the constraints that come with it
Bitbucket has no true banner module. There is no equivalent of confluence:pageBanner that renders a strip above content on every page. The repository code-overview panel is the closest placement available, and it comes with two constraints worth knowing: it only appears on the repository source page — not Commits, Pull requests or Pipelines — and it is user-collapsible, with Bitbucket remembering the collapsed state. A reader who collapses it stops seeing announcements there until they expand it again. The Apps → Announcements page is the reliable fallback.
The panel runs in compact mode: when there is nothing outstanding it renders nothing at all rather than an empty state, so it never pushes a reader’s code down for no reason.
The workspace announcement centre is workspace-only, and says so. It reuses the same reader surface and component as the repository pages; the difference is entirely server-side. Project- and repository-scoped announcements are only merged when the request context carries a repository id to match them against, and a workspace page’s context carries none. The page states this rather than leaving a reader to wonder why a project notice they know about is missing.
Resolving a repository’s project. Project-scoped announcements can only reach a reader if the app knows which project their repository belongs to. Atlassian’s module docs list only repository.uuid on that surface’s context; in practice the live context also carries a top-level workspaceId, so the direct repository lookup is the path normally taken, with a UUID-query fallback for the documented case. Project membership is authorization data and repositories can move between projects, so every reader authorization decision performs a fresh lookup rather than using a stale cache.
User guide
Who this is forWhoever publishes announcements. Each console is already permission-gated by the product, so a delegated admin needs no extra grant — and gains no reach beyond their own container.
Every console manages only its own scope, and that is the point: a project or space lead can publish to their own people without a site admin in the loop. Each console states its reach under the heading, next to a scope lozenge.
| Surface | Where to find it |
|---|---|
| Workspace console | Workspace settings → Forge Apps → Smart Announcement Banner |
| Project console | Project settings → Forge Apps → Smart Announcement Banner |
| Repository console | Repository settings → Forge Apps → Smart Announcement Banner |
| Reader surfaces | The top of a repository’s source page, Repository → Apps → Announcements, and Workspace → Apps → Announcements |
-
Open the console for the level you want to publish at
You cannot choose the level in the composer — it comes from the page you are on. That is what stops a delegated admin publishing site-wide, and it is why the composer states the reach rather than offering it as a dropdown.
Workspace reaches everyone in every repository — outages and org-wide policy. Project reaches every repository in that project — conventions, project migrations. Repository reaches that repository only — a repo freeze, an ownership change.
-
Write the content
Click New announcement. Title (required, 120 characters) and Message (required, 2000 characters) are the two required fields; each new line in the message becomes its own paragraph for readers.
Pick a Type — Critical, Warning, Information, Success or Tip — by what the reader must do, not by how loud you want to be. Critical on a routine notice trains people to ignore Critical. Priority runs 1–5 and affects ordering.
The composer shows a live preview at the top that is the real reader component fed real validated data — what you see there is exactly what readers get.
-
Decide how readers clear it
Require acknowledgement — the reader clicks “Got it”. Recorded per person, reported in Insights, never returns. Dismissible — the reader closes it, and it returns after the snooze you set (
0hours makes the dismissal permanent for that reader). Always visible — no close control at all.Use Always visible sparingly: it cannot be cleared by the reader. Pin above other announcements puts this one first regardless of priority.
-
Choose who sees it
Everyone, only these people, or everyone except these people — matched on Atlassian account ids, comma- or newline-separated. Bitbucket has no group options. It exposes no group-membership endpoint this app can read, so a group rule could never be evaluated — and rather than offer a control that silently matches nobody, it is not offered.
“Everyone” means everyone this announcement reaches: on a delegated console that is the people in that container, not the whole site. Targeting narrows within the scope; it never widens it.
An announcement targeted at anyone other than “Everyone” is hidden from unauthenticated readers, including exclude-modes — the app cannot confirm an anonymous reader is not one of the excluded people.
-
Set the schedule, and read the line under the fields
Starts and Ends are optional, entered in your timezone and stored as UTC. Leave both empty to run until you pause it.
Use the calendar button rather than typing. The date field parses typed text in your browser’s locale, so
2026-08-20is not read as 20 August everywhere. The line under the fields always spells out the window the pickers actually resolved to — read it before saving, because a start date in the past still counts as Live and the mistake is otherwise invisible until the announcement goes out early. -
Publish, or stage it paused
The footer toggle reads Publish when saved / Save as paused. Saving as paused is always allowed, and is how you stage the next announcement while the current one is still running.
-
Watch Insights, and reset reads if the content changes
Acknowledgements (deduplicated per person), people engaged (distinct readers who acknowledged, dismissed or snoozed), and per-announcement breakdowns. A dismissible announcement produces no acknowledgements, so the table reports whichever number that mode actually generates rather than a permanently-zero column.
Reset reads starts a new engagement epoch without changing the content, re-showing the announcement to everyone who already cleared it. Use it when an announcement’s content changed materially.
Good to know
- Each console holds three announcements and shows one at a time. The three-stored limit counts everything the console can see — live, scheduled, paused and expired alike. Both limits are per console, not per site: the site console gets three, and so does every delegated console, so a lead filling their own budget can never stop another team publishing.
- “The same period” means the whole window, not just right now. Two announcements queued for the same week clash even though neither is live yet — otherwise the rule would be broken later by the calendar rather than by anything you did. An announcement with no end date covers everything after its start, so scheduling a successor means giving the current one an end date first.
- Two announcements cannot run at once in the same console, even with different audiences. If you need a notice for engineering and a different one for sales simultaneously, publish them from two different consoles. Readers see the levels merged.
- Managing what exists: Edit reopens the composer and keeps read receipts; Pause/Resume hides from readers without deleting, also keeping receipts; Duplicate creates a paused copy titled “… (copy)” in the same scope; Delete removes the announcement and its read receipts, and cannot be undone.
- Derived status: Live (active and inside its window), Scheduled (active, start not yet reached), Expired (active, past its end date), Paused (switched off — this beats the schedule, so a paused announcement never shows as expired).
- Call-to-action links need both halves — a label with no destination renders a dead button. Only
http(s)links and in-product paths starting with/are accepted;javascript:,data:and protocol-relative//hostURLs are rejected in the composer and again on the backend. - What readers see: a collapsible panel at the top of the repository’s source page (the landing page — not Commits, Pull requests or Pipelines), the full list under Repository → Apps → Announcements, and Workspace → Apps → Announcements, which is reachable without opening any repository. The two repository-level surfaces merge workspace, project and repository announcements; the workspace page shows workspace-scoped announcements only, because it has no repository to resolve the others against — and it says so on the page.
- Retention: acknowledgement and dismissal state is kept per reader for the lifetime of the announcement, so a busy announcement does not make older acknowledgements reappear. The separate recent-activity feed shows the 25 newest events and keeps activity for 90 days.
- Treat a repository’s own Announcements page as the reliable surface. The source-page panel is collapsible and Bitbucket remembers a reader’s collapsed state, so someone who collapses it stops seeing announcements there until they expand it again.
- Ordering is the exception here: on Bitbucket the more specific announcement always leads, rather than priority deciding, because its reader surfaces are full lists rather than a single slot.
- The panel renders nothing at all when there is nothing outstanding, rather than an empty state, so it never pushes a reader’s code down for no reason.
- Badges name the project (
PROJECT TES, not a barePROJECT), because two projects can easily hold announcements with the same title and a repository reader sees their project’s notices merged with the workspace’s.
If something looks wrong
The console refuses to publish a new announcement.
Either the console already holds three, or another announcement in it covers the same period. Both limits count paused and expired announcements too — delete something, or give the current announcement an end date before scheduling its successor.
An announcement went out earlier than expected.
A start date in the past still counts as Live. The line under the schedule fields spells out the window the pickers resolved to; typed dates are parsed in your browser’s locale, so use the calendar button.
A targeted announcement reaches nobody.
Audience matching fails closed. An unauthenticated reader matches no targeted announcement, and a failed group-membership lookup hides rather than shows — in both include- and exclude-group modes.
A project-level announcement is missing from the workspace Announcements page.
Expected. Project and repository announcements are only merged when the request carries a repository id to match them against, and a workspace page carries none. The page states this rather than leaving you to wonder.
A reader says they never see announcements on the source page.
They probably collapsed the panel — Bitbucket remembers that per reader. Point them at Repository → Apps → Announcements, which is not collapsible.
What it does not do
- Publish across products. Each app has its own storage and cannot see the others’ announcements.
- Let a delegated console reach beyond its own container, or let a client choose its own scope — the level is derived server-side from the product’s context.
- Run two announcements at once in the same console, whatever their audiences.
- Recover a deleted announcement or its read receipts.
Permissions & scopes
| Scope | Why it's needed |
|---|---|
storage:app | Announcements and engagement records |
read:repository:bitbucket | The fresh repository→project identity lookup used by reader authorization |
report:personal-data | Scheduled privacy reporting of stored account IDs, and closed-account erasure |
The UUID-query fallback for the repository→project lookup runs on read:repository:bitbucket, which the app already holds. The more obvious /2.0/workspaces route would also work, but forge lint flags it as requiring both read:user:bitbucket and read:workspace:bitbucket — more access than a fallback justifies.
External access
Data stored
| Record | Key | Contents | Retention |
|---|---|---|---|
announcement-v2 | custom entity | Type, title, message, priority, pin flag, CTA, schedule, dismissal mode, scope and attribution | Until deleted |
target-v2 | custom entity, one row per account, swapped by generation | The account IDs an announcement includes or excludes | Replaced wholesale on each edit |
engagement-v2 | custom entity, with epochs | Which reader acknowledged or dismissed which announcement | Permanent for acknowledgements; resets advance an epoch so readers never depend on an eventually-consistent delete finishing first |
activity-v2 | custom entity, idempotent receipts | The recent reader-activity feed | 90 days |
personal-account-v2 | custom entity | The account IDs the app holds | Reported by a daily scheduled trigger; erased on account closure |
Legacy aggregate KVS records migrate idempotently on first access. Ambiguous legacy audience or scope data is paused instead of being widened to everyone.
Security notes
Every resolver is gated on the module the platform rendered
- That value comes from
request.context, not the caller’s payload, and each admin module is already permission-gated by its product — so matching it is equivalent to requiring admin without an extra REST call per mutation. - Bitbucket sends no
moduleKeyat all, so the gate usescontext.extension.typeinstead. This was found by logging the live context: the original gate checkedmoduleKeyand rejected every legitimate request. - All gates fail closed on a missing value, and the mapping is one-to-one. Clients do not send a surface: admin modules cannot call reader engagement, reader modules cannot request admin analytics, and a delegated admin cannot claim global scope.
Scope is derived, then re-used as an authorization check
- The write scope comes from
request.context.extensionand throws rather than widening if the container id is missing. - Every mutation then asserts the target announcement belongs to that exact scope. Two layers: the module gate proves which admin page you are on, the scope check proves which announcements it owns.
- Matching is on the immutable container id; the human-readable key is carried for the reader badge only.
Fail-closed audience matching
- An unauthenticated reader matches no targeted announcement.
- A failed group lookup hides rather than shows — in both include-group and exclude-group modes.
- Targeting narrows within an announcement’s scope; it never widens it.
Call-to-action links are parsed and origin-checked
- An admin-supplied
hrefis the one field that reaches every other reader’s browser, so onlyhttp(s)and in-product paths are stored. javascript:,data:, protocol-relative URLs, backslashes and control characters are dropped.- Embedded credentials are rejected too, because
https://trusted.example@evil.example/reads as a link to the trusted host but navigates to the attacker’s. - Enforced on the backend and mirrored in the composer. An external CTA opens in a new tab; an in-product path stays in the current one.
Versioned, fail-closed migration
- Legacy blobs are copied with stable ids, explicit predecessor truth tables, checkpoints and verification.
- Ambiguous rows are paused and quarantined for admin repair. They never default to active, global or everyone.
- The source blobs remain rollback input for one release.
Reader cost is bounded
- The reader bootstrap filters before it reads engagement, so a site with many paused or out-of-window announcements does not pay a storage read per announcement on every page load.
Known limitations
- No group targeting. Audiences are everyone / only these people / everyone except these people. Bitbucket exposes no group-membership endpoint this app can read, so a group rule could never be evaluated. The previous version offered group targeting anyway and it matched nobody: an announcement an admin believed was live reached zero people, silently. Offering only what can actually be honoured is the fix.
- The overview panel appears only on the repository source page, and it is user-collapsible with the collapsed state remembered by Bitbucket. Apps → Announcements is the reliable fallback.
- The workspace announcement centre shows workspace-wide announcements only, because its request context carries no repository to match project- and repository-scoped announcements against. The page states this.
- A cross-app fix is three separate reviews, not one patch applied thrice.
Release notes
Version history for Smart Announcement Banner for Bitbucket. The most recent release is listed first.
-
v0.1.0 Current
First build, verified live on a real Bitbucket workspace.
What it does
- Three scope levels — workspace, project and repository — each with its own admin console, and each derived server-side from Bitbucket’s context.
- Three reader surfaces: the repository code-overview panel, the repository announcement centre, and a workspace announcement centre outside any repository.
- Five announcement types, priority and pinning, an optional call-to-action, scheduling, and three dismissal modes.
- Project-named badges, so two same-titled notices from different projects stay distinguishable.
- Five stable custom entity types with generation-swapped targets, engagement epochs and 90-day activity receipts, plus a daily privacy trigger.
Found only by running it live
- Bitbucket sends no
moduleKey. The admin gate originally checked it and rejected every legitimate request. The real context carriesextension.typeinstead, which is what the gate now uses. - Bitbucket REST paths 404 on a bare UUID. Scope ids are stored unwrapped because braces are not storage-key safe, but the repository endpoint needs them wrapped — so the repository→project lookup failed and project-scoped announcements reached nobody until the ids were re-wrapped for the request.
Removed
- Group targeting. Bitbucket exposes no group-membership endpoint this app can read, so the option had always matched nobody — silently.
Verification
- All three admin consoles load and are scope-isolated: the project and repository consoles each showed 0 announcements while a workspace-wide one existed.
- A workspace announcement and a project announcement both appeared on the repository reader page, project first, each tagged with its level.
- Acknowledging cleared only that announcement for that reader.
- The workspace nav module renders its heading, its workspace-only note, and the same empty state as the other reader surfaces.
Get it & contact
Vendor
AryonForge
Support
Security
App ID
ari:cloud:ecosystem::app/47121873-1e21-4c78-88d0-74118979fac5