From beb3f303490ccef82d4c3f8b68ec08c52b921f04 Mon Sep 17 00:00:00 2001
From: Jan Maarten <83665577+janmaarten-a11y@users.noreply.github.com>
Date: Sun, 28 Jun 2026 14:04:47 -0700
Subject: [PATCH 1/5] Timeline: add License Compliance Opened event story
(proof-of-pattern)
Phase 2 of github/primer#6663. Adds the Opened event group plus the
file scaffold for the License Compliance alert timeline, mirroring the
reviewed Secret Scanning stories template.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
...nse-compliance.features.stories.module.css | 71 ++++++
...ne.license-compliance.features.stories.tsx | 232 ++++++++++++++++++
2 files changed, 303 insertions(+)
create mode 100644 packages/react/src/Timeline/Timeline.license-compliance.features.stories.module.css
create mode 100644 packages/react/src/Timeline/Timeline.license-compliance.features.stories.tsx
diff --git a/packages/react/src/Timeline/Timeline.license-compliance.features.stories.module.css b/packages/react/src/Timeline/Timeline.license-compliance.features.stories.module.css
new file mode 100644
index 00000000000..5952016652f
--- /dev/null
+++ b/packages/react/src/Timeline/Timeline.license-compliance.features.stories.module.css
@@ -0,0 +1,71 @@
+.RealisticTimeline {
+ /* GitHub renders the alert timeline at most 1012px wide in product surfaces. */
+ max-width: 1012px;
+}
+
+/* Story-only scaffolding: each variant is wrapped in its own with a
+ small caption heading ABOVE the event, so the event row itself (badge +
+ inline actor + body) renders exactly as it would in product — the caption
+ never sits inside Timeline.Body. Variants stay scannable like a Figma
+ component set. Not part of the faithful event. */
+.Variant {
+ margin-bottom: var(--base-size-24);
+}
+
+.VariantLabel {
+ margin: 0 0 var(--base-size-4);
+ font-size: var(--text-body-size-small);
+ font-weight: var(--base-text-weight-semibold);
+ color: var(--fgColor-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+}
+
+/* Inline user-actor avatar in the body. Live `TimelineEventWithActor`
+ (`events/shared.tsx`) renders a 20px CIRCLE `GitHubAvatar` immediately before
+ the login — note this surface uses 20px, NOT the 16px of the secret-scanning
+ timeline. */
+.InlineAvatar {
+ vertical-align: middle;
+ margin-right: var(--base-size-4);
+}
+
+/* Bold actor name for the non-link actor shapes — live `shared.tsx` renders the
+ login as a bold `` when there is no `actor.url`
+ (and for the bot shape, before the secondary "bot" Label). */
+.ActorName {
+ font-weight: var(--base-text-weight-semibold);
+ color: var(--fgColor-default);
+}
+
+/* Secondary "bot" Label spacing — live `shared.tsx` renders the bot Label with
+ a leading `ml-1`. */
+.BotLabel {
+ margin-left: var(--base-size-4);
+}
+
+/* Linked actor — live `shared.tsx` `actorLink`: an inline-flex avatar + login
+ rendered as a `` whenever `actor.url` is present, forced to the default
+ foreground color and SEMIBOLD weight (overriding the link accent color). The
+ bold weight is what keeps it distinguishable from the surrounding muted text
+ WITHOUT relying on color, so it satisfies the axe `link-in-text-block`
+ high-contrast rule. */
+.ActorLink {
+ display: inline-flex;
+ align-items: center;
+ font-weight: var(--base-text-weight-semibold);
+ color: var(--fgColor-default) !important;
+}
+
+/* Muted action verb (e.g. "opened this alert"). Live `shared.tsx` renders it as
+ ``. */
+.ActionText {
+ color: var(--fgColor-muted);
+}
+
+/* Muted relative timestamp — live `shared.tsx` renders a plain `RelativeTime`
+ with no link wrapper (muted text only), matching the secret-scanning and
+ Dependabot timelines. */
+.Timestamp {
+ color: var(--fgColor-muted);
+}
diff --git a/packages/react/src/Timeline/Timeline.license-compliance.features.stories.tsx b/packages/react/src/Timeline/Timeline.license-compliance.features.stories.tsx
new file mode 100644
index 00000000000..0babc61dbe1
--- /dev/null
+++ b/packages/react/src/Timeline/Timeline.license-compliance.features.stories.tsx
@@ -0,0 +1,232 @@
+import type {Meta} from '@storybook/react-vite'
+import type {ComponentProps} from '../utils/types'
+import {FeatureFlags} from '../FeatureFlags'
+import Timeline from './Timeline'
+import {ShieldIcon} from '@primer/octicons-react'
+import Avatar from '../Avatar'
+import Label from '../Label'
+import Link from '../Link'
+import Octicon from '../Octicon'
+import RelativeTime from '../RelativeTime'
+import classes from './Timeline.license-compliance.features.stories.module.css'
+
+/**
+ * License Compliance alert Timeline event examples (Phase 2 of github/primer#6663).
+ *
+ * These stories recreate GitHub's live license-compliance-alert timeline events
+ * using the Primer `Timeline` compositional slots. They mirror the established
+ * Issues (`Timeline.issues.features.stories.tsx`), Dependabot
+ * (`Timeline.dependabot.features.stories.tsx`) and Secret Scanning
+ * (`Timeline.secret-scanning.features.stories.tsx`) stories and are sourced from
+ * the `timeline-audit` inventory
+ * (`license-compliance-timeline-events-for-figma.md`), verified against the live
+ * React implementation.
+ *
+ * SOURCE OF TRUTH — License Compliance is FULLY React (NOT ERB), like Secret
+ * Scanning. The alert show page is a React SPA in `github/github-ui`, package
+ * `packages/license-compliance-alerts/`. The timeline is rendered by
+ * `components/timeline/AlertTimeline.tsx`, which maps each event through
+ * `components/timeline/TimelineEventItem.tsx`, whose `switch (event.type)` is
+ * the authoritative dispatch. The actual badge variant + octicon + copy for each
+ * event live in the per-event components under `components/timeline/events/`,
+ * which compose Primer React `Timeline` + `Timeline.Badge variant=` via two
+ * shared wrappers in `events/shared.tsx`:
+ * - `TimelineEventWithActor` — renders the event `actor` INLINE (a 20px circle
+ * `GitHubAvatar` + login), then the muted action verb + `RelativeTime`.
+ * - `TimelineEventWithoutActor` — no actor; muted action verb + `RelativeTime`
+ * only (used by the synthetic `appeared_in_branch` event).
+ *
+ * The authoritative `switch (event.type)` dispatches NINE event types
+ * (`types/alerts.ts` `AlertEventType`): `opened`, `appeared_in_branch`,
+ * `review_requested`, `review_denied`, `review_approved`, `review_expired`,
+ * `exception_added`, `licenses_added`, `closed`. `opened` and
+ * `appeared_in_branch` are SYNTHETIC events created by Rails. The `default` case
+ * renders `null` (unknown types render nothing).
+ *
+ * NO SYSTEM "GitHub" ACTOR ON THIS SURFACE: unlike the secret-scanning timeline
+ * (which has a `MarkGithubIcon` + "GitHub" system actor for its Created event),
+ * EVERY license-compliance actor event flows through `TimelineEventWithActor`,
+ * which renders a user/bot actor — never a `MarkGithubIcon` system label. So
+ * this file intentionally does NOT define a `GitHubActor` helper.
+ *
+ * SCOPE: These are Storybook-only examples by design. They are intentionally
+ * NOT wired into components-json / the primer.style docs page (do NOT add this
+ * file to `Timeline.docs.json` or `build.ts`). Individual timeline events are
+ * not consumer-facing components — the primer.style Timeline page reflects the
+ * base `Timeline` component's own stories, and any docs-site representation is a
+ * Phase 3 consideration via base-component story changes, out of scope here.
+ *
+ * FUTURE FILTERING (taxonomy still open — github/primer#6663): category
+ * `data-*` attributes (e.g. `data-event-category="created"`) will attach to each
+ * `Timeline.Item` below so stories can be filtered/grouped by event family. We
+ * intentionally do NOT add them yet to avoid baking in a taxonomy.
+ *
+ * PROOF-OF-PATTERN SCOPE (this file, for now): only the `opened` group is built
+ * below, to validate the template before scaling to the remaining eight groups.
+ * Helpers to add as those groups land: an `EventComment` sub-row (live
+ * `shared.tsx` renders ` +
+ * {comment}` for review / exception /
+ * licenses-added comments — note it uses `NoteIcon`, not the secret-scanning
+ * `CommentIcon`), a `PullRequestLink` sub-row, and a `Timeline.Actions`
+ * "Review request" button on the latest `review_requested` event (live
+ * `AlertTimeline.tsx` only passes `onReviewRequest` to the LAST
+ * review_requested event).
+ *
+ * BADGE COLORS (live per-event components): success (green) `ShieldIcon` —
+ * `opened`; done (purple) `ShieldCheckIcon` — `closed`. Per the audit, every
+ * other group renders a gray DEFAULT badge (`badgeVariant` undefined →
+ * `` with a muted icon, no solid fill). We render those bare,
+ * matching the secret-scanning / Dependabot convention (NOT the Issues
+ * `--timelineBadge-bgColor` hook).
+ *
+ * ACCESSIBILITY NOTE: the only in-text `` on this surface is the actor
+ * link (rendered when `actor.url` is present). Live `shared.tsx` styles it
+ * SEMIBOLD + default foreground color (`actorLink`); the bold weight keeps it
+ * distinguishable from the surrounding muted text without relying on color, so
+ * it satisfies the axe `link-in-text-block` high-contrast rule. Any future
+ * in-text link added here must likewise be `inline` (underline) or bold.
+ */
+
+const MONALISA_AVATAR = 'https://avatars.githubusercontent.com/u/583231?v=4'
+const DEPENDABOT_AVATAR = 'https://avatars.githubusercontent.com/in/29110?v=4'
+
+/**
+ * User actor — live `TimelineEventWithActor` (`events/shared.tsx`). Renders a
+ * 20px CIRCLE `GitHubAvatar` followed by the login. Three shapes, all reproduced
+ * here so every future group can reuse this helper:
+ * - `url` present → a `` wrapping avatar + login, forced semibold +
+ * default color (`actorLink`); bold weight satisfies `link-in-text-block`.
+ * - no `url` → avatar + bold login text (``).
+ * - `bot` (live: `login.endsWith('[bot]')`) → avatar + bold display login (the
+ * `[bot]` suffix stripped) + a secondary "bot" `Label`.
+ */
+const UserActor = ({
+ login = 'monalisa',
+ src = MONALISA_AVATAR,
+ url,
+ bot = false,
+}: {
+ login?: string
+ src?: string
+ url?: string
+ bot?: boolean
+}) => {
+ const avatar =
+ if (bot) {
+ return (
+
+ {avatar}
+ {login}
+
+
+ )
+ }
+ if (url) {
+ return (
+
+ {avatar}
+ {login}
+
+ )
+ }
+ return (
+
+ {avatar}
+ {login}
+
+ )
+}
+
+// Muted relative timestamp. Live `shared.tsx` renders a plain `RelativeTime`
+// with no link wrapper — muted text only (matching the secret-scanning and
+// Dependabot timelines, unlike the Issues `Ago` deep-link).
+const Time = ({date}: {date: string}) => (
+
+
+
+)
+
+export default {
+ title: 'Components/Timeline/Events/License Compliance',
+ component: Timeline,
+ subcomponents: {
+ 'Timeline.Item': Timeline.Item,
+ 'Timeline.Avatar': Timeline.Avatar,
+ 'Timeline.Badge': Timeline.Badge,
+ 'Timeline.Body': Timeline.Body,
+ 'Timeline.Break': Timeline.Break,
+ 'Timeline.Actions': Timeline.Actions,
+ },
+ decorators: [
+ // File-scoped: render every story in the future-state list semantics
+ // (``/`
`). The `primer_react_timeline_list_semantics` flag is merged
+ // on main; this opts these stories into the DOM the timeline will ship.
+ Story => (
+
+
+
+ ),
+ ],
+} as Meta>
+
+/**
+ * The Opened event group — `AlertEventType.Opened` (audit § 1).
+ *
+ * Source: `case AlertEventType.Opened` in `TimelineEventItem.tsx`, rendered by
+ * `events/OpenedEvent.tsx`:
+ * `}
+ * actionText="opened this alert" badgeVariant="success" />`
+ * Badge: `ShieldIcon` on `success` (green). Copy is a fixed `"opened this
+ * alert"` + the relative time. This is a SYNTHETIC event created by Rails, but
+ * it still carries an `actor` (enriched by the Rails controller).
+ *
+ * ACTOR (audit UNCERTAIN resolved): the Opened event is NOT attributed to a
+ * GitHub-system actor. `OpenedEvent` uses `TimelineEventWithActor`, so the actor
+ * is whatever user/bot Rails enriched onto the event — rendered as a 20px avatar
+ * + login (a `` when `actor.url` is present, otherwise bold text; bots get
+ * a "bot" Label). Both shapes are shown below.
+ *
+ * The live `OpenedEvent` has exactly one rendering shape (no source / from-PR /
+ * from-push branches); the two variants here differ ONLY by actor type to
+ * document the resolved actor question.
+ */
+export const EventOpened = () => (
+
+ {/* Opened by a user — linked actor (20px avatar + semibold login), ShieldIcon
+ on success (green). */}
+
+
Opened by a user
+
+
+
+
+
+
+ {' '}
+ opened this alert
+
+
+
+
+
+ {/* Opened by a bot — automated dependency detection. Live `shared.tsx`
+ strips the `[bot]` suffix from the login and appends a secondary "bot"
+ Label. Same success badge + copy. */}
+
+
Opened by a bot
+
+
+
+
+
+
+ {' '}
+ opened this alert
+
+
+
+
+
+)
From 07ebfbda04c6d80936511befdee783e69a2e87fa Mon Sep 17 00:00:00 2001
From: Jan Maarten <83665577+janmaarten-a11y@users.noreply.github.com>
Date: Sun, 28 Jun 2026 14:14:14 -0700
Subject: [PATCH 2/5] Timeline: add remaining License Compliance event stories
Adds the eight remaining event groups (appeared_in_branch, review
requested/approved/denied/expired, exception_added, licenses_added,
closed) to complete the License Compliance alert timeline stories,
verified against the live per-event components and the Rails synthetic
event builder. Documents the dormant appeared_in_branch PR sub-row and
the actor-less review_expired event.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
...nse-compliance.features.stories.module.css | 36 ++
...ne.license-compliance.features.stories.tsx | 506 +++++++++++++++++-
2 files changed, 527 insertions(+), 15 deletions(-)
diff --git a/packages/react/src/Timeline/Timeline.license-compliance.features.stories.module.css b/packages/react/src/Timeline/Timeline.license-compliance.features.stories.module.css
index 5952016652f..ddcdcda8410 100644
--- a/packages/react/src/Timeline/Timeline.license-compliance.features.stories.module.css
+++ b/packages/react/src/Timeline/Timeline.license-compliance.features.stories.module.css
@@ -69,3 +69,39 @@
.Timestamp {
color: var(--fgColor-muted);
}
+
+/* Comment sub-row — live `EventComment` (`events/shared.tsx`) renders a
+ `Stack` (mt-1) with a 16px muted `NoteIcon` + an `f6` muted comment span. */
+.CommentRow {
+ display: flex;
+ align-items: center;
+ margin-top: var(--base-size-4);
+ font-size: var(--text-body-size-small);
+ color: var(--fgColor-muted);
+}
+
+.CommentRowIcon {
+ margin-right: var(--base-size-8);
+ color: var(--fgColor-muted);
+}
+
+/* PR-link sub-row — live `PullRequestLink` (`events/shared.tsx`) renders a 16px
+ success-green `GitPullRequestIcon` + an `f6` ``. */
+.PrRow {
+ display: inline-flex;
+ align-items: center;
+ margin-top: var(--base-size-4);
+ font-size: var(--text-body-size-small);
+}
+
+.PrIcon {
+ margin-right: var(--base-size-8);
+ color: var(--fgColor-success);
+}
+
+/* Sub-row link forced to the default foreground color, matching live
+ `defaultColorLink`. Rendered with the `inline` prop (underline) at the call
+ site so it stays distinguishable for the axe `link-in-text-block` rule. */
+.SubRowLink {
+ color: var(--fgColor-default) !important;
+}
diff --git a/packages/react/src/Timeline/Timeline.license-compliance.features.stories.tsx b/packages/react/src/Timeline/Timeline.license-compliance.features.stories.tsx
index 0babc61dbe1..42a85b43bc7 100644
--- a/packages/react/src/Timeline/Timeline.license-compliance.features.stories.tsx
+++ b/packages/react/src/Timeline/Timeline.license-compliance.features.stories.tsx
@@ -2,8 +2,22 @@ import type {Meta} from '@storybook/react-vite'
import type {ComponentProps} from '../utils/types'
import {FeatureFlags} from '../FeatureFlags'
import Timeline from './Timeline'
-import {ShieldIcon} from '@primer/octicons-react'
+import {
+ CheckIcon,
+ CircleSlashIcon,
+ CommentIcon,
+ GitBranchIcon,
+ GitPullRequestIcon,
+ LawIcon,
+ NoteIcon,
+ ShieldCheckIcon,
+ ShieldIcon,
+ XIcon,
+} from '@primer/octicons-react'
+import type React from 'react'
import Avatar from '../Avatar'
+import BranchName from '../BranchName'
+import {Button} from '../Button'
import Label from '../Label'
import Link from '../Link'
import Octicon from '../Octicon'
@@ -61,22 +75,28 @@ import classes from './Timeline.license-compliance.features.stories.module.css'
* `Timeline.Item` below so stories can be filtered/grouped by event family. We
* intentionally do NOT add them yet to avoid baking in a taxonomy.
*
- * PROOF-OF-PATTERN SCOPE (this file, for now): only the `opened` group is built
- * below, to validate the template before scaling to the remaining eight groups.
- * Helpers to add as those groups land: an `EventComment` sub-row (live
- * `shared.tsx` renders ` +
- * {comment}` for review / exception /
- * licenses-added comments — note it uses `NoteIcon`, not the secret-scanning
- * `CommentIcon`), a `PullRequestLink` sub-row, and a `Timeline.Actions`
- * "Review request" button on the latest `review_requested` event (live
- * `AlertTimeline.tsx` only passes `onReviewRequest` to the LAST
- * review_requested event).
+ * PROOF-OF-PATTERN HISTORY: this file landed first as an `opened`-only
+ * scaffold to validate the template, then grew to the full nine-group set. All
+ * groups are verified against the live per-event components in
+ * `events/` and, for the two synthetic events, against the Rails controller
+ * `app/controllers/repos/license_compliance_alerts_controller.rb` (the synthetic
+ * `opened` / `appeared_in_branch` builder).
+ *
+ * DORMANT CODE (verified, intentionally NOT built): `AppearedInBranchEvent`
+ * renders a `PullRequestLink` sub-row whenever its event body carries
+ * `pull_request_number` / `pull_request_title`, but the Rails
+ * `create_synthetic_branch_event` only ever writes `{branch_name}` — it never
+ * populates those PR fields (PR enrichment is applied ONLY to
+ * `review_requested`). So that sub-row is dead in production; we build only the
+ * branch-name variant. Likewise `review_expired` uses `TimelineEventWithActor`
+ * but expiry is automatic and Rails attaches no actor, so it renders actor-less
+ * (its copy "Request to close expired" is a standalone capitalized sentence).
*
* BADGE COLORS (live per-event components): success (green) `ShieldIcon` —
- * `opened`; done (purple) `ShieldCheckIcon` — `closed`. Per the audit, every
- * other group renders a gray DEFAULT badge (`badgeVariant` undefined →
- * `` with a muted icon, no solid fill). We render those bare,
- * matching the secret-scanning / Dependabot convention (NOT the Issues
+ * `opened`; done (purple) `ShieldCheckIcon` — `closed`. Every other group
+ * renders a gray DEFAULT badge (`badgeVariant` undefined → ``
+ * with a muted icon, no solid fill). We render those bare, matching the
+ * secret-scanning / Dependabot convention (NOT the Issues
* `--timelineBadge-bgColor` hook).
*
* ACCESSIBILITY NOTE: the only in-text `` on this surface is the actor
@@ -89,6 +109,7 @@ import classes from './Timeline.license-compliance.features.stories.module.css'
const MONALISA_AVATAR = 'https://avatars.githubusercontent.com/u/583231?v=4'
const DEPENDABOT_AVATAR = 'https://avatars.githubusercontent.com/in/29110?v=4'
+const HUBOT_AVATAR = 'https://avatars.githubusercontent.com/hubot'
/**
* User actor — live `TimelineEventWithActor` (`events/shared.tsx`). Renders a
@@ -148,6 +169,50 @@ const Time = ({date}: {date: string}) => (
)
+/**
+ * Optional comment sub-row — live `EventComment` (`events/shared.tsx`) renders a
+ * `` containing a
+ * 16px muted `NoteIcon` + an `f6` muted comment span. Shared by the review
+ * request / approve / deny events and the closed event. (Note this surface uses
+ * `NoteIcon`, NOT the secret-scanning `CommentIcon`.)
+ */
+const EventComment = ({children}: {children: React.ReactNode}) => (
+
+
+ {children}
+
+)
+
+/**
+ * PR link sub-row — live `PullRequestLink` (`events/shared.tsx`): a 16px
+ * success-green `GitPullRequestIcon` + an `f6` `` reading `{title}
+ * #{number}`. Only the `review_requested` event renders this (the Rails
+ * controller enriches that event with `pull_request_number` / `_title` when the
+ * alert has an associated PR). The link is `inline` (underlined) here to satisfy
+ * the axe `link-in-text-block` rule — live uses a color-only `defaultColorLink`
+ * (an a11y debt we correct in the story).
+ */
+const PullRequestLink = ({number, title}: {number: number; title: string}) => (
+
+
+
+ {title} #{number}
+
+
+)
+
+/**
+ * Inline "license policy" link — live `ExceptionAddedEvent` / `LicensesAddedEvent`
+ * embed a `` reading "license policy" inside the action sentence. Rendered
+ * `inline` (underlined) here to satisfy the axe `link-in-text-block` rule (live
+ * uses a color-only `defaultColorLink`).
+ */
+const PolicyLink = ({href = '../../settings/security_analysis'}: {href?: string}) => (
+
+ license policy
+
+)
+
export default {
title: 'Components/Timeline/Events/License Compliance',
component: Timeline,
@@ -230,3 +295,414 @@ export const EventOpened = () => (
)
+
+/**
+ * The Appeared-in-branch event group — `AlertEventType.AppearedInBranch`
+ * (audit § 2).
+ *
+ * Source: `case AlertEventType.AppearedInBranch` → `events/AppearedInBranchEvent.tsx`,
+ * which uses `TimelineEventWithoutActor` (NO actor): `GitBranchIcon size={16}`
+ * on the default (gray) badge, copy "Appeared in branch", followed by a
+ * `BranchName` pill linking to the branch tree. This is a SYNTHETIC event.
+ *
+ * The component ALSO renders a `PullRequestLink` sub-row when its body has
+ * `pull_request_number` / `pull_request_title` — but per the Rails
+ * `create_synthetic_branch_event`, those fields are NEVER written for this event
+ * (only `{branch_name}`), so that sub-row is DORMANT and intentionally not built
+ * here. Single variant: the branch-name pill.
+ */
+export const EventAppearedInBranch = () => (
+
+ {/* Appeared in branch — actor-less, GitBranchIcon on the default (gray)
+ badge, with a BranchName pill. PR sub-row is dormant (see group doc). */}
+
+
Appeared in branch
+
+
+
+
+
+
+ Appeared in branch{' '}
+ feature-branch{' '}
+
+
+
+
+
+
+)
+
+/**
+ * The Review-requested event group — `AlertEventType.ReviewRequested`
+ * (audit § 3).
+ *
+ * Source: `events/ReviewRequestedEvent.tsx` (`TimelineEventWithActor`):
+ * `CommentIcon size={16}` on the default (gray) badge. Copy is "requested to
+ * close" or, when the event body carries a `closure_reason`, "requested to close
+ * as {reason}". An optional `EventComment` sub-row renders the requester's
+ * comment. When the alert has an associated PR, the Rails controller enriches
+ * this event with `pull_request_number` / `_title`, so a `PullRequestLink`
+ * sub-row appears AND — for the latest review_requested event only — a primary
+ * "Review request" button. Live nests that button beside the PR link in a
+ * space-between Stack; we surface it via the `Timeline.Actions` right-controls
+ * slot (the established template convention).
+ */
+export const EventReviewRequested = () => (
+
+ {/* Requested to close — no reason, no PR, no comment */}
+
+
Requested to close
+
+
+
+
+
+
+ {' '}
+ requested to close
+
+
+
+
+
+ {/* Requested to close as {reason}, with a requester comment sub-row */}
+
+
Requested to close as a specific reason (with comment)
+
+
+
+
+
+
+ {' '}
+ requested to close as used in tests{' '}
+
+ This dependency is only pulled in by our test harness.
+
+
+
+
+
+ {/* Requested to close with an associated PR — PR link sub-row + the primary
+ "Review request" button (latest request only) in Timeline.Actions. */}
+
+
Requested to close with a pull request (latest — shows Review request)
+
+
+
+
+
+
+ {' '}
+ denied closure request
+ This package is distributed to customers, so the license still applies.
+
+
+
+
+
+)
+
+/**
+ * The Review-expired event group — `AlertEventType.ReviewExpired` (audit § 6).
+ *
+ * Source: `events/ReviewExpiredEvent.tsx`. Although it uses
+ * `TimelineEventWithActor`, expiry is automatic and Rails attaches no actor, so
+ * it renders ACTOR-LESS (verified: the `review_expired` test fixture has no
+ * actor, and the copy "Request to close expired" is a standalone capitalized
+ * sentence). `CircleSlashIcon size={16}` on the default (gray) badge. No comment
+ * sub-row. Single variant.
+ */
+export const EventReviewExpired = () => (
+
+ {/* Request to close expired — actor-less (automatic expiry) */}
+
+
Request to close expired
+
+
+
+
+
+
+ Request to close expired
+
+
+
+
+
+)
+
+/**
+ * The Exception-added event group — `AlertEventType.ExceptionAdded`
+ * (audit § 7).
+ *
+ * Source: `events/ExceptionAddedEvent.tsx` (`TimelineEventWithActor`):
+ * `LawIcon size={16}` on the default (gray) badge. When the body has
+ * `package_manager` + `package_name`, the copy is "added {pm}/{pkg}" + (when a
+ * policy path is known) " to {license policy link}" + (when a repo name is
+ * known) " for {repo}". Otherwise it falls back to "created exception".
+ */
+export const EventExceptionAdded = () => (
+
+ {/* Full shape — package + policy link + repo name */}
+
+
Added a package exception to the license policy
+
+
+
+
+
+
+ {' '}
+
+ added npm/left-pad to for monalisa/octo-app
+ {' '}
+
+
+
+
+
+
+ {/* Fallback shape — body missing package info */}
+
+
Created exception (fallback)
+
+
+
+
+
+
+ {' '}
+ created exception
+
+
+
+
+
+)
+
+/**
+ * The Licenses-added event group — `AlertEventType.LicensesAdded` (audit § 8).
+ *
+ * Source: `events/LicensesAddedEvent.tsx` (`TimelineEventWithActor`):
+ * `LawIcon size={16}` on the default (gray) badge. When the body has a
+ * `licenses` array, the copy is "added {licenses joined by ', '}" + (when a
+ * policy path is known) " to {license policy link}" + (when a repo name is
+ * known) " for {repo}". Otherwise it falls back to "added to approved licenses".
+ */
+export const EventLicensesAdded = () => (
+
+ {/* Full shape — licenses list + policy link + repo name */}
+
+
+)
+
+/**
+ * The Closed event group — `AlertEventType.Closed` (audit § 9).
+ *
+ * Source: `events/ClosedEvent.tsx` (`TimelineEventWithActor`): `ShieldCheckIcon
+ * size={16}` on the `done` (PURPLE) badge — the only colored gray-exempt event
+ * besides Opened. The copy comes from `getClosedActionText(closureReason,
+ * resolution)`, which renders exactly one of:
+ * - "closed as {closureReason}" — when a free-text `closureReason` is present
+ * (e.g. "used in tests", carried over from the review flow);
+ * - "closed as outdated" — resolution `Outdated`;
+ * - "closed as amendment" — resolution `ExceptionAdded` or `LicensesAdded`;
+ * - "closed this alert" — the default.
+ * An optional `EventComment` sub-row renders a closing comment.
+ */
+export const EventClosed = () => (
+
+ {/* Closed as {free-text reason}, with a closing comment */}
+
+
Closed as a specific reason (with comment)
+
+
+
+
+
+
+ {' '}
+ closed as used in tests
+ Confirmed this dependency only ships in the test bundle.
+
+
+
+
+
+ {/* Closed as outdated — resolution Outdated */}
+
+
+)
From a287d416bedb416f981f0eb2455c0579dad30368 Mon Sep 17 00:00:00 2001
From: Jan Maarten <83665577+janmaarten-a11y@users.noreply.github.com>
Date: Sun, 28 Jun 2026 15:08:28 -0700
Subject: [PATCH 3/5] Timeline: fix License Compliance actor row alignment
Render the inline actor as plain inline elements (a vertical-align:middle
Avatar followed by an inline Link/span) instead of an inline-flex wrapper,
matching the aligned Issues and Dependabot story rows. The inline-flex box
baseline-aligned oddly within the surrounding inline text, throwing the
badge, actor, and summary off a single line.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
...nse-compliance.features.stories.module.css | 26 +++++++--------
...ne.license-compliance.features.stories.tsx | 33 +++++++++++--------
2 files changed, 32 insertions(+), 27 deletions(-)
diff --git a/packages/react/src/Timeline/Timeline.license-compliance.features.stories.module.css b/packages/react/src/Timeline/Timeline.license-compliance.features.stories.module.css
index ddcdcda8410..956c480a53c 100644
--- a/packages/react/src/Timeline/Timeline.license-compliance.features.stories.module.css
+++ b/packages/react/src/Timeline/Timeline.license-compliance.features.stories.module.css
@@ -33,9 +33,22 @@
/* Bold actor name for the non-link actor shapes — live `shared.tsx` renders the
login as a bold `` when there is no `actor.url`
(and for the bot shape, before the secondary "bot" Label). */
+/* Actor name — shared by the linked actor (``), the bold non-link actor,
+ and the bot login. Rendered PLAIN INLINE (no flex wrapper) so it sits on the
+ same vertically-centered line as the avatar, badge and summary, matching the
+ Issues / Dependabot rows. Semibold + `fgColor-default`: the bold weight is the
+ non-color differentiator that satisfies the axe `link-in-text-block` rule, so
+ the linked variant stays a11y-safe without an underline. */
.ActorName {
font-weight: var(--base-text-weight-semibold);
color: var(--fgColor-default);
+ margin-right: var(--base-size-4);
+}
+
+/* Accent on hover for the linked actor (no-op on the plain-span shapes),
+ mirroring the Dependabot `LinkWithBoldStyle` hover. */
+.ActorName:hover {
+ color: var(--fgColor-accent);
}
/* Secondary "bot" Label spacing — live `shared.tsx` renders the bot Label with
@@ -44,19 +57,6 @@
margin-left: var(--base-size-4);
}
-/* Linked actor — live `shared.tsx` `actorLink`: an inline-flex avatar + login
- rendered as a `` whenever `actor.url` is present, forced to the default
- foreground color and SEMIBOLD weight (overriding the link accent color). The
- bold weight is what keeps it distinguishable from the surrounding muted text
- WITHOUT relying on color, so it satisfies the axe `link-in-text-block`
- high-contrast rule. */
-.ActorLink {
- display: inline-flex;
- align-items: center;
- font-weight: var(--base-text-weight-semibold);
- color: var(--fgColor-default) !important;
-}
-
/* Muted action verb (e.g. "opened this alert"). Live `shared.tsx` renders it as
``. */
.ActionText {
diff --git a/packages/react/src/Timeline/Timeline.license-compliance.features.stories.tsx b/packages/react/src/Timeline/Timeline.license-compliance.features.stories.tsx
index 42a85b43bc7..92909b5bd99 100644
--- a/packages/react/src/Timeline/Timeline.license-compliance.features.stories.tsx
+++ b/packages/react/src/Timeline/Timeline.license-compliance.features.stories.tsx
@@ -113,13 +113,16 @@ const HUBOT_AVATAR = 'https://avatars.githubusercontent.com/hubot'
/**
* User actor — live `TimelineEventWithActor` (`events/shared.tsx`). Renders a
- * 20px CIRCLE `GitHubAvatar` followed by the login. Three shapes, all reproduced
- * here so every future group can reuse this helper:
- * - `url` present → a `` wrapping avatar + login, forced semibold +
- * default color (`actorLink`); bold weight satisfies `link-in-text-block`.
- * - no `url` → avatar + bold login text (``).
- * - `bot` (live: `login.endsWith('[bot]')`) → avatar + bold display login (the
- * `[bot]` suffix stripped) + a secondary "bot" `Label`.
+ * 20px CIRCLE `GitHubAvatar` followed by the login. Rendered as PLAIN INLINE
+ * elements (avatar with `vertical-align: middle` immediately followed by an
+ * inline ``/span) — NO `inline-flex` wrapper — so the avatar, login, badge
+ * and trailing summary all sit on one cleanly vertically-centered line, exactly
+ * like the working Issues (#8070) / Dependabot (#8071) rows. Three shapes:
+ * - `url` present → inline `` login, semibold + `fgColor-default` (the bold
+ * weight is the non-color differentiator that satisfies `link-in-text-block`).
+ * - no `url` → bold login text.
+ * - `bot` (live: `login.endsWith('[bot]')`) → bold display login (the `[bot]`
+ * suffix stripped) + a secondary "bot" `Label`.
*/
const UserActor = ({
login = 'monalisa',
@@ -135,28 +138,30 @@ const UserActor = ({
const avatar =
if (bot) {
return (
-
+ <>
{avatar}
{login}
-
+ >
)
}
if (url) {
return (
-
+ <>
{avatar}
- {login}
-
+
+ {login}
+
+ >
)
}
return (
-
+ <>
{avatar}
{login}
-
+ >
)
}
From 59a65dc83e60cccbc238fd6600d07879f6809878 Mon Sep 17 00:00:00 2001
From: Jan Maarten <83665577+janmaarten-a11y@users.noreply.github.com>
Date: Sun, 28 Jun 2026 15:12:29 -0700
Subject: [PATCH 4/5] Timeline: address Copilot review on License Compliance
stories
- UserActor derives the bot shape from the login (strips a trailing
[bot] suffix and renders bots unlinked), matching live shared.tsx,
instead of a separate bot prop.
- Wrap each group's examples in an Examples container that prevents
default navigation on the story-only demo links, so clicking a
placeholder href doesn't navigate out of Storybook (mirrors the base
Timeline WithActions story).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
...ne.license-compliance.features.stories.tsx | 96 +++++++++++--------
1 file changed, 56 insertions(+), 40 deletions(-)
diff --git a/packages/react/src/Timeline/Timeline.license-compliance.features.stories.tsx b/packages/react/src/Timeline/Timeline.license-compliance.features.stories.tsx
index 92909b5bd99..8e14acb9407 100644
--- a/packages/react/src/Timeline/Timeline.license-compliance.features.stories.tsx
+++ b/packages/react/src/Timeline/Timeline.license-compliance.features.stories.tsx
@@ -117,30 +117,28 @@ const HUBOT_AVATAR = 'https://avatars.githubusercontent.com/hubot'
* elements (avatar with `vertical-align: middle` immediately followed by an
* inline ``/span) — NO `inline-flex` wrapper — so the avatar, login, badge
* and trailing summary all sit on one cleanly vertically-centered line, exactly
- * like the working Issues (#8070) / Dependabot (#8071) rows. Three shapes:
- * - `url` present → inline `` login, semibold + `fgColor-default` (the bold
- * weight is the non-color differentiator that satisfies `link-in-text-block`).
- * - no `url` → bold login text.
- * - `bot` (live: `login.endsWith('[bot]')`) → bold display login (the `[bot]`
- * suffix stripped) + a secondary "bot" `Label`.
+ * like the working Issues (#8070) / Dependabot (#8071) rows. The shape is derived
+ * from the login, matching live `shared.tsx`:
+ * - login ends with `[bot]` → the suffix is stripped and the (UNLINKED) display
+ * login renders bold + a secondary "bot" `Label` (live ignores `url` for bots).
+ * - otherwise `url` present → inline `` login, semibold + `fgColor-default`
+ * (the bold weight is the non-color differentiator that satisfies
+ * `link-in-text-block`).
+ * - otherwise → bold login text.
*/
-const UserActor = ({
- login = 'monalisa',
- src = MONALISA_AVATAR,
- url,
- bot = false,
-}: {
- login?: string
- src?: string
- url?: string
- bot?: boolean
-}) => {
+const UserActor = ({login = 'monalisa', src = MONALISA_AVATAR, url}: {login?: string; src?: string; url?: string}) => {
+ // Derive the bot shape from the login itself, exactly like live `shared.tsx`
+ // (`isBot = login.endsWith('[bot]')`, with the suffix stripped for display).
+ const isBot = login.endsWith('[bot]')
+ const displayLogin = isBot ? login.replace(/\[bot\]$/i, '') : login
const avatar =
- if (bot) {
+ if (isBot) {
+ // Live renders bots UNLINKED (the bot branch ignores `actor.url`): avatar +
+ // bold display login + a secondary "bot" Label.
return (
<>
{avatar}
- {login}
+ {displayLogin}
@@ -152,7 +150,7 @@ const UserActor = ({
<>
{avatar}
- {login}
+ {displayLogin}
>
)
@@ -160,7 +158,7 @@ const UserActor = ({
return (
<>
{avatar}
- {login}
+ {displayLogin}
>
)
}
@@ -218,6 +216,24 @@ const PolicyLink = ({href = '../../settings/security_analysis'}: {href?: string}
)
+/**
+ * Story-only wrapper around each group's examples. Besides constraining the
+ * width (`.RealisticTimeline`), it swallows clicks on the demo links (actor
+ * profile, PR, license-policy) so navigating one of these placeholder `href`s
+ * doesn't kick the viewer out of the Storybook UI — the same guard the base
+ * `Timeline.features.stories.tsx` `WithActions` story uses.
+ */
+const Examples = ({children}: {children: React.ReactNode}) => (
+
{
+ if ((e.target as HTMLElement).closest('a')) e.preventDefault()
+ }}
+ >
+ {children}
+
+
{/* Opened by a user — linked actor (20px avatar + semibold login), ShieldIcon
on success (green). */}
@@ -292,13 +308,13 @@ export const EventOpened = () => (
- {' '}
+ {' '}
opened this alert
-
+
{/* Appeared in branch — actor-less, GitBranchIcon on the default (gray)
badge, with a BranchName pill. PR sub-row is dormant (see group doc). */}
@@ -335,7 +351,7 @@ export const EventAppearedInBranch = () => (
-
+
)
From e08fd784c8bbafe3826397a9e4cf5e11e8ff5f88 Mon Sep 17 00:00:00 2001
From: Jan Maarten <83665577+janmaarten-a11y@users.noreply.github.com>
Date: Sun, 28 Jun 2026 16:20:10 -0700
Subject: [PATCH 5/5] Timeline: refine License Compliance actor + closure
fidelity
- All system events use the github-license-compliance[bot] actor: the
Opened event is now a single bot variant, and Review-expired shows the
bot actor (Rails sets it for system user_id<=0; the no-actor mock was
misleading). Add LICENSE_BOT_AVATAR.
- Closed covers the full live closure-reason set (amendment, private
package, inaccurate license, policy edited, fixed) plus outdated and
the default.
- Note that the review_requested closure_reason format comes from the Go
OLC service and needs a prod smoke-test.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
...ne.license-compliance.features.stories.tsx | 153 +++++++++++-------
1 file changed, 93 insertions(+), 60 deletions(-)
diff --git a/packages/react/src/Timeline/Timeline.license-compliance.features.stories.tsx b/packages/react/src/Timeline/Timeline.license-compliance.features.stories.tsx
index 8e14acb9407..9c62c87ffa3 100644
--- a/packages/react/src/Timeline/Timeline.license-compliance.features.stories.tsx
+++ b/packages/react/src/Timeline/Timeline.license-compliance.features.stories.tsx
@@ -108,8 +108,10 @@ import classes from './Timeline.license-compliance.features.stories.module.css'
*/
const MONALISA_AVATAR = 'https://avatars.githubusercontent.com/u/583231?v=4'
-const DEPENDABOT_AVATAR = 'https://avatars.githubusercontent.com/in/29110?v=4'
const HUBOT_AVATAR = 'https://avatars.githubusercontent.com/hubot'
+// The license-compliance system bot — Rails attributes every system event
+// (Opened, Review-expired) to `github-license-compliance[bot]` when user_id<=0.
+const LICENSE_BOT_AVATAR = 'https://avatars.githubusercontent.com/u/9919?s=40&v=4'
/**
* User actor — live `TimelineEventWithActor` (`events/shared.tsx`). Renders a
@@ -268,52 +270,30 @@ export default {
* alert"` + the relative time. This is a SYNTHETIC event created by Rails, but
* it still carries an `actor` (enriched by the Rails controller).
*
- * ACTOR (audit UNCERTAIN resolved): the Opened event is NOT attributed to a
- * GitHub-system actor. `OpenedEvent` uses `TimelineEventWithActor`, so the actor
- * is whatever user/bot Rails enriched onto the event — rendered as a 20px avatar
- * + login (a `` when `actor.url` is present, otherwise bold text; bots get
- * a "bot" Label). Both shapes are shown below.
- *
- * The live `OpenedEvent` has exactly one rendering shape (no source / from-PR /
- * from-push branches); the two variants here differ ONLY by actor type to
- * document the resolved actor question.
+ * ACTOR (security UNCERTAIN deep-dive resolved): the synthetic `opened` event is
+ * ALWAYS attributed to the license-compliance system bot,
+ * `github-license-compliance[bot]` (Rails `create_synthetic_opened_event`). Per
+ * live `shared.tsx`, a login ending in `[bot]` renders avatar + bold display
+ * login ("github-license-compliance") + a secondary "bot" Label — NOT a Link.
+ * There is no human "opened" actor, so a single bot variant is shown.
*/
export const EventOpened = () => (
- {/* Opened by a user — linked actor (20px avatar + semibold login), ShieldIcon
- on success (green). */}
+ {/* Opened — license-compliance system bot, ShieldIcon on success (green) */}
-
Opened by a user
+
Opened
- {' '}
+ {' '}
opened this alert
-
- {/* Opened by a bot — automated dependency detection. Live `shared.tsx`
- strips the `[bot]` suffix from the login and appends a secondary "bot"
- Label. Same success badge + copy. */}
-
-
Opened by a bot
-
-
-
-
-
-
- {' '}
- opened this alert
-
-
-
-
)
@@ -368,6 +348,11 @@ export const EventAppearedInBranch = () => (
* "Review request" button. Live nests that button beside the PR link in a
* space-between Stack; we surface it via the `Timeline.Actions` right-controls
* slot (the established template convention).
+ *
+ * NOTE: the `closure_reason` text shown after "requested to close as …" is
+ * produced by the Go OLC service, not the local Rails/React code — its exact
+ * format (e.g. "amendment"-style label vs raw value) is unverified here. Confirm
+ * via a prod smoke-test; the example label below is illustrative.
*/
export const EventReviewRequested = () => (
@@ -522,16 +507,15 @@ export const EventReviewDenied = () => (
/**
* The Review-expired event group — `AlertEventType.ReviewExpired` (audit § 6).
*
- * Source: `events/ReviewExpiredEvent.tsx`. Although it uses
- * `TimelineEventWithActor`, expiry is automatic and Rails attaches no actor, so
- * it renders ACTOR-LESS (verified: the `review_expired` test fixture has no
- * actor, and the copy "Request to close expired" is a standalone capitalized
- * sentence). `CircleSlashIcon size={16}` on the default (gray) badge. No comment
- * sub-row. Single variant.
+ * Source: `events/ReviewExpiredEvent.tsx` (`TimelineEventWithActor`):
+ * `CircleSlashIcon size={16}` on the default (gray) badge, copy "Request to close
+ * expired". Although a misleading test mock omits the actor, Rails sets the
+ * license-compliance system bot for these system events (`user_id<=0`), so it
+ * renders WITH the bot actor. No comment sub-row. Single variant.
*/
export const EventReviewExpired = () => (
- {/* Request to close expired — actor-less (automatic expiry) */}
+ {/* Request to close expired — license-compliance system bot, automatic expiry */}
Request to close expired
@@ -540,6 +524,7 @@ export const EventReviewExpired = () => (
+ {' '}
Request to close expired
@@ -651,60 +636,108 @@ export const EventLicensesAdded = () => (
* Source: `events/ClosedEvent.tsx` (`TimelineEventWithActor`): `ShieldCheckIcon
* size={16}` on the `done` (PURPLE) badge — the only colored gray-exempt event
* besides Opened. The copy comes from `getClosedActionText(closureReason,
- * resolution)`, which renders exactly one of:
- * - "closed as {closureReason}" — when a free-text `closureReason` is present
- * (e.g. "used in tests", carried over from the review flow);
- * - "closed as outdated" — resolution `Outdated`;
- * - "closed as amendment" — resolution `ExceptionAdded` or `LicensesAdded`;
- * - "closed this alert" — the default.
- * An optional `EventComment` sub-row renders a closing comment.
+ * resolution)`. When a free-text `closureReason` is present it renders "closed as
+ * {reason}"; the live label set is `CLOSURE_REASON_TO_LABEL`
+ * (`license_compliance_dismissal_options.rb`): amendment, private package,
+ * inaccurate license, policy edited, fixed. The two resolution-driven fallbacks
+ * are "closed as outdated" (resolution `Outdated`) and "closed this alert"
+ * (default). An optional `EventComment` sub-row renders a closing comment.
*/
export const EventClosed = () => (
- {/* Closed as {free-text reason}, with a closing comment */}
+ {/* Closed as amendment — with a closing comment */}
-
Closed as a specific reason (with comment)
+
Closed as amendment (with comment)
-
+ {' '}
- closed as used in tests
- Confirmed this dependency only ships in the test bundle.
+ closed as amendment
+ Added a policy exception covering this package.
- {/* Closed as outdated — resolution Outdated */}
+ {/* Closed as private package */}
-
Closed as outdated
+
Closed as private package
-
+ {' '}
- closed as outdated
+ closed as private package
- {/* Closed as amendment — resolution ExceptionAdded / LicensesAdded */}
+ {/* Closed as inaccurate license */}
-