feat: a public notes page per release, for clients

On the releases page, «Página pública» (owners and admins) opens a
secret link to a page with the release's notes, readable without an
account, also for private projects: the notes written, or the draft
from its finished tickets while there are none (without repeating the
title the page already shows). The link can be copied, renewed (the old
one stops working) or withdrawn. Stored in Mongo (`release_note_shares`).

API: shareReleaseNotes / unshareReleaseNotes / releaseNotesShare,
publicReleaseNotes(token); GET/PUT/DELETE /api/releases/{id}/public-notes,
GET /api/shared/release-notes/{token}. Web: /notes/:token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 20:57:40 +02:00
parent 3a46a236e4
commit 8ed16f133e
27 changed files with 2158 additions and 6 deletions

View File

@@ -1185,6 +1185,11 @@ type Mutation {
"""Turn the link on or change its blocks; renew for a new secret"""
shareDashboard(input: ShareDashboardInput!): DashboardShare!
"""
Turn a release's public notes page on; renew makes a new link (the old one stops working)
"""
shareReleaseNotes(releaseId: ID!, renew: Boolean! = false): ReleaseNotesShare!
"""Start a planned sprint (only one under way per project)"""
startSprint(sprintId: ID!): CommandResult!
@@ -1227,6 +1232,9 @@ type Mutation {
"""The link stops working"""
unshareDashboard(projectId: ID!): Boolean!
"""A release's public notes link stops working"""
unshareReleaseNotes(releaseId: ID!): Boolean!
"""Push off on this device"""
unsubscribePush(endpoint: String!): Boolean!
@@ -1637,6 +1645,23 @@ type ProposalsFunnel {
waiting: Int!
}
"""A release's notes, for anyone with its link"""
type PublicReleaseNotes {
description: String
"""Markdown"""
notes: String!
projectKey: String!
projectName: String!
releaseName: String!
releasedAt: String
status: ReleaseStatus!
targetDate: String
"""False: drawn from its finished tickets (none written yet)"""
written: Boolean!
}
"""A browser's Web Push subscription (PushSubscription.toJSON())"""
input PushSubscriptionInput {
auth: String!
@@ -1783,8 +1808,14 @@ type Query {
"""Projects anyone can read, also without signing in"""
publicProjects: [Project!]!
"""A release's notes, for anyone with its link"""
publicReleaseNotes(token: String!): PublicReleaseNotes
release(id: ID!): Release!
"""A release's public notes link, if any (owners and admins)"""
releaseNotesShare(releaseId: ID!): ReleaseNotesShare
"""My views of the project's board, then the shared ones"""
savedFilters(projectId: ID!): [SavedFilter!]!
@@ -1882,6 +1913,15 @@ type ReleaseForecast {
verdict: String!
}
"""A release's public notes link (owners and admins)"""
type ReleaseNotesShare {
createdAt: String!
releaseId: ID!
"""The secret of the link: /notes/{token}"""
token: String!
}
type ReleaseProgress {
byStatus: [CountByStatus!]!
finished: Int!