Skip to content

feat(apollo-vertex): Roadmap Status page with live Jira data - #962

Open
hfrancis31 wants to merge 1 commit into
mainfrom
feat/vertex-roadmap-status
Open

feat(apollo-vertex): Roadmap Status page with live Jira data#962
hfrancis31 wants to merge 1 commit into
mainfrom
feat/vertex-roadmap-status

Conversation

@hfrancis31

Copy link
Copy Markdown
Collaborator

Summary

  • Adds a /design-system-status page surfaced as "Roadmap status" in the sidebar, positioned directly under Introduction
  • Reads live from the VS Horizontal UX Jira board (label = "horizontal-ux") and displays three sections: Recently Delivered, Coming Soon, and Backlog
  • Coming Soon cards are sorted with In Review first, In Progress after
  • Delivered section is curated: only tickets with the ds-delivered Jira label appear
  • Epic name shown on cards when the ticket has a parent Epic
  • Link resolution for delivered tickets: explicit Vertex: <url> in description (supports both plain text and Jira smartlinks) → convention slug lookup → Jira URL fallback

Environment variables required

Add to .env.local locally and to the Vercel project for production:

JIRA_BASE_URL=https://uipath.atlassian.net
JIRA_EMAIL=<your-email>
JIRA_API_TOKEN=<your-api-token>

How to add tickets to the Delivered section

Add the ds-delivered label to the Jira ticket. No code change needed.

How to set the Vertex link on a delivered card

Add Vertex: https://... as a line in the Jira ticket description.

Test plan

  • Visit /design-system-status and confirm three sections render with live data
  • Confirm "In Review" cards appear at the top of Coming Soon
  • Confirm the Delivered section shows only the ds-delivered-labelled ticket(s)
  • Confirm epic names appear on cards that have a parent Epic
  • Confirm clicking a delivered card with a Vertex: URL navigates to the Vertex page (not Jira)
  • Confirm page loads gracefully without env vars (shows setup error state)

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings July 23, 2026 20:47
@hfrancis31
hfrancis31 requested a review from a team as a code owner July 23, 2026 20:47
@github-actions github-actions Bot added app:apollo-vertex size:XL 500-999 changed lines. labels Jul 23, 2026
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Dependency License Review

  • 1950 package(s) scanned
  • ✅ No license issues found
  • ⚠️ 2 package(s) excluded (see details below)
License distribution
License Packages
MIT 1720
ISC 89
Apache-2.0 55
BSD-3-Clause 27
BSD-2-Clause 23
BlueOak-1.0.0 8
MPL-2.0 4
MIT-0 3
CC0-1.0 3
MIT OR Apache-2.0 2
(MIT OR Apache-2.0) 2
Unlicense 2
LGPL-3.0-or-later 1
Python-2.0 1
CC-BY-4.0 1
(MPL-2.0 OR Apache-2.0) 1
Unknown 1
Artistic-2.0 1
(WTFPL OR MIT) 1
(BSD-2-Clause OR MIT OR Apache-2.0) 1
CC-BY-3.0 1
0BSD 1
(MIT OR CC0-1.0) 1
MIT AND ISC 1
Excluded packages
Package Version License Reason
@img/sharp-libvips-linux-x64 1.3.2 LGPL-3.0-or-later LGPL pre-built binary, not linked
khroma 2.1.0 Unknown MIT per GitHub repo, missing license field in package.json

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds a new Apollo Vertex documentation page that surfaces “Roadmap status” using live data from the VS Horizontal UX Jira board, including basic card processing (sectioning, badges, epic display) and delivered-link resolution rules.

Changes:

  • Added Jira integration utilities to query issues and extract text from Jira ADF descriptions.
  • Added issue processing + delivered-link resolution (explicit Vertex: link, convention-based slug lookup, Jira fallback).
  • Added a new /design-system-status page (and sidebar entry) rendering a three-section status board, plus an /api/jira endpoint.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
apps/apollo-vertex/lib/jira.ts Fetches Jira issues via JQL with pagination; includes ADF text extraction helper.
apps/apollo-vertex/lib/jira-resolve.ts Transforms raw Jira issues into board sections/cards and resolves delivered links.
apps/apollo-vertex/app/design-system-status/page.mdx Adds the new “Roadmap status” MDX page that mounts the status board.
apps/apollo-vertex/app/design-system-status/_components/status-board.tsx Implements the status board UI, error state, and server-side data fetch.
apps/apollo-vertex/app/api/jira/route.ts Adds a JSON API endpoint that returns the processed board data.
apps/apollo-vertex/app/_meta.ts Adds “Roadmap status” to the sidebar under Introduction.

Comment thread apps/apollo-vertex/lib/jira-resolve.ts Outdated
Comment thread apps/apollo-vertex/app/design-system-status/_components/status-board.tsx Outdated
Comment thread apps/apollo-vertex/app/design-system-status/_components/status-board.tsx Outdated
Comment thread apps/apollo-vertex/app/design-system-status/page.mdx Outdated
Comment thread apps/apollo-vertex/app/api/jira/route.ts
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

📊 Coverage + size by package

Per-package bundle size on this PR (no JS/TS source changes detected under packages/* or web-packages/*).

Package Coverage New-line coverage Packed (gzip) Unpacked vs main
@uipath/apollo-core
@uipath/apollo-react
@uipath/apollo-wind
@uipath/ap-chat

"Coverage" is each package's own coverage.include scope (e.g. apollo-core instruments only scripts/). "Packed"/"Unpacked" come from npm pack --dry-run and only cover built packages — "—" means not measured this run (package not affected / not built). "vs main" is the packed (gzipped) delta against the last successful main build (the package-sizes artifact from the Release workflow); "—" there means no main baseline was available this run. The baseline is main's latest build, not this PR's exact merge-base, so it includes any drift since the branch diverged. Packages with no vitest config are omitted.

@0xr3ngar 0xr3ngar left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

left some comments, address them with Claude, if you need my help lmk.

Also CI checks are failing, please fix before re-requesting review.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

should this even be in the apollo-vertex? shouldn't we just mock jira tickets in the UI and explicitly tell the consumers to connect it to the actual JIRA API. I don't think this needs to be here

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I discussed with @ruudandriessen that this is needed so that we can provide visibility to all eng teams on what is coming and the status. We are wanting more visibility across teams.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

instead of hand rolling our own jira processing can't we just use a sdk npm package ? like

https://github.com/jira-node/node-jira-client

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for the suggestion. I looked into it. The main reasons I'm keeping the raw fetch approach here:

  • Single endpoint: we only call POST /rest/api/3/search/jql. Adding a full SDK to wrap one request adds dependency weight without meaningful simplification.
  • v3 cursor pagination: we use nextPageToken, which is specific to the REST v3 API. Most Jira JS SDKs still target v2 (startAt/maxResults) and would require workarounds.
  • Intentionally narrow types: JiraIssue only types the fields we actually fetch. SDK-generated types cover the full response shape, which adds noise for no gain here.

Happy to revisit if the integration scope grows, but for a single read-only JQL query this felt like the right trade-off.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lets split this file up in smaller functions that each handle 1 thing on their own.

Comment on lines +117 to +123
function toSection(statusName: string): Section {
const s = statusName.toLowerCase();
if (s === "closed" || s === "done") return "delivered";
if (s === "in progress" || s === "in review" || s === "review")
return "coming-soon";
return "backlog";
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would use a record

const SECTION_BY_STATUS = {
  closed: "delivered",
  done: "delivered",
  "in progress": "coming-soon",
  "in review": "coming-soon",
  review: "coming-soon",
} satisfies Record<string, Section>;

function toSection(statusName: string): Section {
  return SECTION_BY_STATUS[statusName.trim().toLowerCase()] ?? "backlog";
}

Comment thread apps/apollo-vertex/lib/jira-resolve.ts Outdated
Comment on lines +125 to +129
function toBadge(labels: string[]): BadgeLabel {
if (labels.includes("ai-legal-required")) return "required";
if (labels.includes("ai-legal-best-practice")) return "best-practice";
return null;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this would be cleaner if it's

const BADGE_BY_LABEL = {

  "ai-legal-required": "required",

  "ai-legal-best-practice": "best-practice",

} as const satisfies Record<string, Exclude<BadgeLabel, null>>;



const BADGE_LABEL_PRIORITY = [

  "ai-legal-required",

  "ai-legal-best-practice",

] as const;



function toBadge(labels: readonly string[]): BadgeLabel {

  const label = BADGE_LABEL_PRIORITY.find((candidate) =>

    labels.includes(candidate),

  );



  return label ? BADGE_BY_LABEL[label] : null;

}

Comment on lines +131 to +161
function resolveDeliveredLink(
issue: JiraIssue,
jiraUrl: string,
): { url: string; source: LinkSource } {
const descText = extractAdfText(issue.fields.description);

// Priority 1: explicit "Vertex: https://..." or "Vertex URL: https://..." in description
const vertexMatch = descText.match(
/Vertex(?:\s+URL)?:\s*(https?:\/\/[^\s\n)]+)/i,
);
if (vertexMatch) {
return { url: vertexMatch[1].trim(), source: "explicit" };
}

// Priority 2: "Component: <name>" convention — slugify and look up known paths
const componentMatch = descText.match(/^Component:\s*(.+)$/im);
if (componentMatch) {
const rawSlug = componentMatch[1].match(/^([^\s(,]+)/)?.[1];
if (rawSlug) {
const slug = rawSlug
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "");
const path = SLUG_PATH_MAP.get(slug);
if (path) return { url: path, source: "convention" };
}
}

// Priority 3: fall back to Jira ticket
return { url: jiraUrl, source: "jira" };
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

all of this regex processing starts making me question if we should really process the description at all

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The best version, is to avoid parsing the description entirely. Jira should ideally expose these as structured custom fields

issue.fields.vertexUrl
issue.fields.componentName

Then the function shrinks to

function resolveDeliveredLink(
  issue: JiraIssue,
  jiraUrl: string,
): { url: string; source: LinkSource } {
  if (issue.fields.vertexUrl) {
    return {
      url: issue.fields.vertexUrl,
      source: "explicit",
    };
  }

  const path = issue.fields.componentName
    ? PATH_BY_COMPONENT[issue.fields.componentName]
    : undefined;

  return path
    ? { url: path, source: "convention" }
    : { url: jiraUrl, source: "jira" };
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

For now, keeping the description-based Vertex: convention as a pragmatic workaround to get the board live and visible. Since we don't have this available right now, I'm working with an admin to create one. Once it's available, I'll update lib/jira.ts to fetch the field directly and simplify the parsing in jira-resolve.ts.

Comment on lines +163 to +225
export function processIssues(
issues: JiraIssue[],
jiraBaseUrl: string,
): BoardData {
const delivered: ProcessedCard[] = [];
const comingSoon: ProcessedCard[] = [];
const backlog: ProcessedCard[] = [];
const linkStats = { explicit: 0, convention: 0, jiraFallback: 0 };

for (const issue of issues) {
const statusName = issue.fields.status.name;
const section = toSection(statusName);
const jiraUrl = `${jiraBaseUrl}/browse/${issue.key}`;

let link: string;
let linkSource: LinkSource;

if (section === "delivered") {
const resolved = resolveDeliveredLink(issue, jiraUrl);
link = resolved.url;
linkSource = resolved.source;
if (linkSource === "explicit") linkStats.explicit++;
else if (linkSource === "convention") linkStats.convention++;
else linkStats.jiraFallback++;
} else {
link = jiraUrl;
linkSource = "jira";
}

const parentIsEpic =
issue.fields.parent?.fields.issuetype.name === "Epic" ||
issue.fields.parent?.fields.issuetype.hierarchyLevel === 1;

const card: ProcessedCard = {
key: issue.key,
summary: issue.fields.summary,
status: statusName,
section,
badge: toBadge(issue.fields.labels),
link,
linkSource,
jiraUrl,
updated: issue.fields.updated,
epicName: parentIsEpic
? (issue.fields.parent?.fields.summary ?? null)
: null,
epicKey: parentIsEpic ? (issue.fields.parent?.key ?? null) : null,
};

if (section === "delivered") {
delivered.push(card);
} else if (section === "coming-soon") {
// Review/In Review sorts before In Progress — closer to landing
const sl = statusName.toLowerCase();
if (sl === "in review" || sl === "review") comingSoon.unshift(card);
else comingSoon.push(card);
} else {
backlog.push(card);
}
}

return { delivered, comingSoon, backlog, linkStats };
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this does way to many things in one place, this let link mutability is a smell. Tell your claude to extract this into seperate small functions so processIssues becomes an orchestrator

@0xr3ngar
0xr3ngar self-requested a review July 28, 2026 10:11

@0xr3ngar 0xr3ngar left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I accidentally approved oops

Copilot AI review requested due to automatic review settings August 5, 2026 14:33
@hfrancis31
hfrancis31 force-pushed the feat/vertex-roadmap-status branch from 35290ac to 60a5584 Compare August 5, 2026 14:33
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Apollo Coded App preview deployments are running.

Project Status Preview Updated (PT)
apollo-design Deploying... Logs Aug 06, 2026, 09:34:30 AM
apollo-docs Deploying... Logs Aug 06, 2026, 09:34:30 AM
apollo-landing Deploying... Logs Aug 06, 2026, 09:34:30 AM
apollo-vertex Deploying... Logs Aug 06, 2026, 09:34:30 AM

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (6)

apps/apollo-vertex/app/api/jira/route.ts:8

  • No code in apps/apollo-vertex appears to call /api/jira (searching for /api/jira returns no matches), and the Roadmap Status page fetches Jira directly via fetchJiraIssues(). If this endpoint isn’t needed for external consumers, removing it would reduce public attack surface and maintenance burden; otherwise, consider wiring the page to use it so there’s a single data path to maintain.
// Intentionally public — this endpoint surfaces cross-team design system status.
// No auth guard is by design; confirmed with @ruudandriessen.
export async function GET(_req: NextRequest) {
  try {

apps/apollo-vertex/lib/jira-resolve.ts:166

  • The “Component:” convention only uses the first whitespace-delimited token (e.g., "Alert Dialog" becomes "alert"), which prevents slug resolution for multi-word components like alert-dialog and button-group. Consider slugifying the full component name (optionally trimming any trailing "(…)" or ", …" qualifiers) before looking up the path.
  const componentMatch = descText.match(/^Component:\s*(.+)$/im);
  if (componentMatch) {
    const rawSlug = componentMatch[1].match(/^([^\s(,]+)/)?.[1];
    if (rawSlug) {
      const slug = rawSlug

apps/apollo-vertex/lib/jira.ts:56

  • JIRA_BASE_URL may be configured with a trailing slash (e.g., https://…/), which would generate a double-slash in the request URL (//rest/api/...). Normalizing the base URL avoids subtle request/redirect issues.
  const base = process.env.JIRA_BASE_URL;

apps/apollo-vertex/lib/jira.ts:63

  • The missing-config error message tells people to add vars to .env.local, but the UI error state points to apps/apollo-vertex/.env.local. Aligning the message avoids confusion when setting this up locally.
    throw new Error(
      "Missing Jira configuration. Add JIRA_BASE_URL, JIRA_EMAIL, and JIRA_API_TOKEN to .env.local",
    );

apps/apollo-vertex/lib/jira.ts:92

  • Throwing the full Jira error response body can leak unnecessary details to downstream callers (and can be very large if Jira returns HTML). Prefer using the status + statusText (and optionally logging the full body server-side) instead of embedding the entire body in the thrown message.
    if (!res.ok) {
      const text = await res.text();
      throw new Error(`Jira API error ${res.status}: ${text}`);
    }

apps/apollo-vertex/app/api/jira/route.ts:19

  • This route is marked intentionally public and calls Jira using server-side credentials. Two concerns: (1) Cache-Control: no-store means every request triggers a Jira API call, which is vulnerable to accidental/intentional traffic spikes and Jira rate limits; (2) returning the raw exception message can leak internal details. Consider adding CDN caching and only returning a safe error message (still preserving the missing-config hint if you want).
      headers: { "Cache-Control": "no-store" },
    });
  } catch (e) {
    const message = e instanceof Error ? e.message : String(e);
    return NextResponse.json({ error: message }, { status: 500 });

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Storybook visual diff

⏭️ Skipped: the apollo-design preview deployment did not succeed, so no comparison ran. Logs

Updated (PT): Aug 05, 2026, 08:05:34 AM

Copilot AI review requested due to automatic review settings August 5, 2026 14:52
@hfrancis31
hfrancis31 force-pushed the feat/vertex-roadmap-status branch from 60a5584 to e3f25bf Compare August 5, 2026 14:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (3)

apps/apollo-vertex/lib/jira.ts:94

  • The error thrown on non-2xx Jira responses includes the full response body (await res.text()). This can leak unexpected internal details to the UI/API consumer and can also create extremely large error messages if Jira returns HTML or verbose JSON. Consider only including the status code in production, optionally appending the body only in non-production for debugging.
    if (!res.ok) {
      // eslint-disable-next-line no-await-in-loop -- cursor pagination is inherently sequential
      const text = await res.text();
      throw new Error(`Jira API error ${res.status}: ${text}`);
    }

apps/apollo-vertex/app/api/jira/route.ts:19

  • This public API route returns the raw caught error message to clients. In production that can inadvertently expose internal details (for example, upstream HTML/JSON error bodies) and makes the endpoint easier to probe. Consider returning a generic message in production (while still preserving the "Missing Jira configuration" setup hint).
  } catch (e) {
    const message = e instanceof Error ? e.message : String(e);
    return NextResponse.json({ error: message }, { status: 500 });
  }

apps/apollo-vertex/lib/jira-resolve.ts:215

  • sortComingSoon only distinguishes review vs non-review and otherwise returns 0, which relies on the JS engine's sort stability to preserve the incoming (JQL) ordering. Adding an explicit tie-breaker (for example updated descending) avoids potentially non-deterministic ordering and keeps the "most recently updated" items at the top within each group.
function sortComingSoon(cards: ProcessedCard[]): ProcessedCard[] {
  return [...cards].sort((a, b) => {
    const aReview = isReviewStatus(a.status);
    const bReview = isReviewStatus(b.status);
    if (aReview !== bReview) return aReview ? -1 : 1;
    return 0;
  });
}

Copilot AI review requested due to automatic review settings August 5, 2026 15:02
@hfrancis31
hfrancis31 force-pushed the feat/vertex-roadmap-status branch from e3f25bf to 655b054 Compare August 5, 2026 15:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Suppressed comments (4)

apps/apollo-vertex/app/api/jira/route.ts:15

  • This route is public and triggers live Jira fetches, but there are no in-repo references to /api/jira (it isn’t used by the new StatusBoard either). If it’s not intended for external consumers, consider removing it to reduce public surface area and Jira load, or switch the UI to consume this endpoint so it has a single fetch path.
// Intentionally public — this endpoint surfaces cross-team design system status.
// No auth guard is by design; confirmed with @ruudandriessen.
export async function GET(_req: NextRequest) {
  try {
    const issues = await fetchJiraIssues();
    const jiraBaseUrl =
      process.env.JIRA_BASE_URL ?? "https://uipath.atlassian.net";
    const data = processIssues(issues, jiraBaseUrl);
    return NextResponse.json(data, {
      headers: { "Cache-Control": "no-store" },
    });

apps/apollo-vertex/lib/jira.ts:64

  • JIRA_BASE_URL is used as-is. If it’s configured with a trailing slash (common in env files), requests will go to ...//rest/api/.... Also, the thrown setup error points to .env.local while the UI error state points to apps/apollo-vertex/.env.local, which is inconsistent for users.
  const base = process.env.JIRA_BASE_URL;
  const email = process.env.JIRA_EMAIL;
  const token = process.env.JIRA_API_TOKEN;

  if (!base || !email || !token) {
    throw new Error(
      "Missing Jira configuration. Add JIRA_BASE_URL, JIRA_EMAIL, and JIRA_API_TOKEN to .env.local",
    );

apps/apollo-vertex/lib/jira-resolve.ts:175

  • Component: convention parsing only keeps the first token before whitespace, so values like Component: Alert Dialog or Component: Button Group will slugify to alert/button and fail to resolve to alert-dialog/button-group. This breaks the intended convention lookup for multi-word component names.
  const componentMatch = descText.match(/^Component:\s*(.+)$/im);
  if (componentMatch) {
    const rawSlug = componentMatch[1].match(/^([^\s(,]+)/)?.[1];
    if (rawSlug) {
      const slug = rawSlug
        .toLowerCase()
        .replace(/[^a-z0-9]+/g, "-")
        .replace(/(^-|-$)/g, "");
      const path = SLUG_PATH_MAP.get(slug);
      if (path) return { url: path, source: "convention" };
    }

apps/apollo-vertex/lib/jira.ts:88

  • cache: "no-store" forces every page view (and every call to the public API route) to hit Jira directly, which can become a reliability/rate-limit issue. Consider using a short server cache via Next’s fetch revalidation (for example 60s) so the page is still near-real-time but resilient to spikes.
        Accept: "application/json",
      },
      body: JSON.stringify(body),
      cache: "no-store",
    });

Comment on lines +90 to +94
if (!res.ok) {
// eslint-disable-next-line no-await-in-loop -- cursor pagination is inherently sequential
const text = await res.text();
throw new Error(`Jira API error ${res.status}: ${text}`);
}
Copilot AI review requested due to automatic review settings August 6, 2026 16:04
@hfrancis31
hfrancis31 force-pushed the feat/vertex-roadmap-status branch from 655b054 to 9e115a3 Compare August 6, 2026 16:04

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (5)

apps/apollo-vertex/app/api/jira/route.ts:19

  • This route returns the raw caught error message to unauthenticated callers. Even with improved upstream errors, it’s better to avoid exposing internal failure details on a public endpoint. Also, Cache-Control: no-store means every request will hit Jira, increasing the risk of rate limiting/abuse for a public URL.
    return NextResponse.json(data, {
      headers: { "Cache-Control": "no-store" },
    });
  } catch (e) {
    const message = e instanceof Error ? e.message : String(e);
    return NextResponse.json({ error: message }, { status: 500 });
  }

apps/apollo-vertex/lib/jira.ts:94

  • Throwing Jira API error ...: ${text} includes the full upstream response body. That error string is later returned to clients (API route) and rendered in the UI (StatusBoard), which can leak internal details and produce very large error payloads. Prefer extracting a short, structured message (or truncating) instead of embedding the full body.
    if (!res.ok) {
      // eslint-disable-next-line no-await-in-loop -- cursor pagination is inherently sequential
      const text = await res.text();
      throw new Error(`Jira API error ${res.status}: ${text}`);
    }

apps/apollo-vertex/lib/jira-resolve.ts:161

  • The Vertex: URL regex will include trailing punctuation (e.g. a period at end of sentence) because the character class allows . and ,. That can lead to broken external links for delivered cards.
  const vertexMatch = descText.match(
    /Vertex(?:\s+URL)?:\s*(https?:\/\/[^\s\n)]+)/i,
  );
  if (vertexMatch) {
    return { url: vertexMatch[1].trim(), source: "explicit" };
  }

apps/apollo-vertex/lib/jira-resolve.ts:175

  • Component: convention slug parsing only takes the first whitespace-delimited token (Button from Button Group), which prevents resolving multi-word slugs like button-group and will incorrectly fall back to Jira for those delivered tickets. Slugify the full component value instead of truncating it.
  const componentMatch = descText.match(/^Component:\s*(.+)$/im);
  if (componentMatch) {
    const rawSlug = componentMatch[1].match(/^([^\s(,]+)/)?.[1];
    if (rawSlug) {
      const slug = rawSlug

apps/apollo-vertex/app/design-system-status/_components/status-board.tsx:178

  • The non-config error state renders the raw error string from Jira/network failures. Since this page is publicly accessible, that can expose internal details (and can be very long if it includes upstream response bodies). Consider showing a generic message to users and keeping the detailed error server-side (logs) or only in development.
      ) : (
        <p className="text-sm text-muted-foreground">{message}</p>
      )}

Copilot AI review requested due to automatic review settings August 6, 2026 16:15
@hfrancis31
hfrancis31 force-pushed the feat/vertex-roadmap-status branch from 9e115a3 to 7c12a8e Compare August 6, 2026 16:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Suppressed comments (5)

apps/apollo-vertex/lib/jira-resolve.ts:224

  • The comparator returns 0 for most pairs, which relies on the engine’s sort stability to preserve the existing Jira ordering. Stability is not guaranteed across all JS engines/spec interpretations, so the relative order of non-review items can change unexpectedly. Add a deterministic tie-breaker (e.g., compare updated descending, then key) or decorate with original indices.
function sortComingSoon(cards: ProcessedCard[]): ProcessedCard[] {
  return cards.toSorted((a, b) => {
    const aReview = isReviewStatus(a.status);
    const bReview = isReviewStatus(b.status);
    if (aReview !== bReview) return aReview ? -1 : 1;
    return 0;
  });
}

apps/apollo-vertex/lib/jira.ts:95

  • Logging the full Jira error response body can leak sensitive information into server logs (Jira sometimes echoes request context or includes content). Consider truncating/redacting the body and/or only logging verbose bodies in non-production environments; at minimum log status + a request identifier rather than the full payload.
    if (!res.ok) {
      // eslint-disable-next-line no-await-in-loop -- cursor pagination is inherently sequential
      const body = await res.text();
      console.error(`Jira API error body (${res.status}):`, body);
      throw new Error(`Jira API error: ${res.status} ${res.statusText}`);
    }

apps/apollo-vertex/app/design-system-status/_components/status-board.tsx:143

  • The UI branches on a substring match of an error message, which tightly couples rendering logic to exact punctuation/wording from the throw site. Prefer throwing a typed error (custom Error subclass) or including a structured error code so the UI can reliably detect missing configuration without parsing strings.
function SetupError({ message }: { message: string }) {
  const isMissingConfig = message.includes("Missing Jira configuration");

apps/apollo-vertex/lib/jira.ts:64

  • The thrown error directs users to add variables to .env.local, while the UI error state in StatusBoard instructs apps/apollo-vertex/.env.local. Align these messages (same path and wording) to avoid confusing setup instructions.
  if (!base || !email || !token) {
    throw new Error(
      "Missing Jira configuration. Add JIRA_BASE_URL, JIRA_EMAIL, and JIRA_API_TOKEN to .env.local",
    );
  }

apps/apollo-vertex/lib/jira-resolve.ts:5

  • This slug list is large and manually inlined, which is likely to drift from the actual site routes over time (despite the comment saying it’s derived from _meta.ts). To reduce drift, consider generating this map from the source-of-truth route metadata at build time (or exporting/importing a shared constant), or add a validation step/test that asserts the list matches the real _meta.ts entries.
// All known routable slugs per section, derived from the site's _meta.ts files
const SLUGS_BY_SECTION: Record<string, string[]> = {
  components: [

Comment on lines +13 to +15
return NextResponse.json(data, {
headers: { "Cache-Control": "no-store" },
});
Adds a /design-system-status page (nav: "Roadmap status", positioned
directly under Introduction) that reads live from the VS Horizontal UX
Jira board and displays three sections: Recently Delivered, Coming Soon,
and Backlog.

- lib/jira.ts: Jira Cloud REST API v3 client with cursor pagination and
  ADF text extraction (handles plain text and smartlink inlineCard nodes)
- lib/jira-resolve.ts: maps raw issues to ProcessedCard objects, resolves
  delivered links via explicit Vertex URL > convention slug > Jira fallback,
  extracts epic from parent field
- app/api/jira/route.ts: GET handler returning board data as JSON
- app/design-system-status/page.mdx + _components/status-board.tsx:
  async server component rendering the three-column card board
- app/_meta.ts: adds "Roadmap status" nav entry below Introduction

Credentials required: JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN in
.env.local (local) or Vercel environment variables (production).
Board filter: label = "horizontal-ux"; delivered filter: label = "ds-delivered".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 6, 2026 17:07
@hfrancis31
hfrancis31 force-pushed the feat/vertex-roadmap-status branch from 7c12a8e to 22ad156 Compare August 6, 2026 17:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Suppressed comments (3)

apps/apollo-vertex/lib/jira-resolve.ts:224

  • Array.prototype.toSorted is not available in older Node runtimes (notably Node 18). If this app targets or may run on Node 18 (common for many Next deployments), this will crash at runtime. Consider using cards.slice().sort(...) (or a small helper) for broader compatibility.
function sortComingSoon(cards: ProcessedCard[]): ProcessedCard[] {
  return cards.toSorted((a, b) => {
    const aReview = isReviewStatus(a.status);
    const bReview = isReviewStatus(b.status);
    if (aReview !== bReview) return aReview ? -1 : 1;
    return 0;
  });
}

apps/apollo-vertex/app/api/jira/route.ts:21

  • This endpoint is intentionally public, but returning raw exception messages can leak internal configuration details (e.g., env var names/paths) and upstream error information. Prefer logging the detailed error server-side and returning a generic client-facing message (optionally with a stable error code) to avoid information disclosure.
  } catch (e) {
    const message = e instanceof Error ? e.message : String(e);
    return NextResponse.json({ error: message }, { status: 500 });
  }

apps/apollo-vertex/app/design-system-status/_components/status-board.tsx:143

  • This UI branches on a substring match of the error message, which is brittle (any wording change breaks the detection). A more robust approach is to throw a dedicated error type or attach a stable code (e.g., JIRA_CONFIG_MISSING) and branch on that instead of parsing message.
function SetupError({ message }: { message: string }) {
  const isMissingConfig = message.includes("Missing Jira configuration");

Comment on lines +191 to +193
const parentIsEpic =
issue.fields.parent?.fields.issuetype.name === "Epic" ||
issue.fields.parent?.fields.issuetype.hierarchyLevel === 1;
@petervachon
petervachon requested a review from 0xr3ngar August 6, 2026 18:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app:apollo-vertex size:XL 500-999 changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants