Skip to content

ci: surface stable preview URLs in PRs#2799

Open
vnv-varun wants to merge 2 commits intomainfrom
varun/preview-stable-urls
Open

ci: surface stable preview URLs in PRs#2799
vnv-varun wants to merge 2 commits intomainfrom
varun/preview-stable-urls

Conversation

@vnv-varun
Copy link
Contributor

@vnv-varun vnv-varun commented Mar 23, 2026

Summary

Surface the stable pr-<n>-*.preview.inkeep.com preview URLs directly in the PR conversation so reviewers can find the right links without digging through Vercel bot comments.

Changes

  • add a Publish stable preview URLs step after successful preview smoke checks
  • add .github/scripts/preview/comment-preview-urls.sh to create or update a single sticky GitHub Actions comment
  • surface the stable UI alias, API alias, and API health URL in that comment
  • include the raw Vercel deployment URLs in a collapsed details block for debugging

Why

The preview workflow already computes and aliases stable preview URLs, but they were easy to miss because the default Vercel comment is more prominent and uses deployment-specific hashes. This change makes the stable aliases the reviewer-facing entry point without exposing Railway infrastructure URLs.

Test Plan

  • bash -n .github/scripts/preview/*.sh
  • Parse .github/workflows/preview-environments.yml with yaml.safe_load
  • git diff --check
  • Verify the preview workflow posts the sticky PR comment with the stable preview URLs on this PR
  • Verify the preview workflow still passes end to end on this PR

Notes

This intentionally surfaces the stable Vercel alias URLs, not Railway service endpoints.

@changeset-bot
Copy link

changeset-bot bot commented Mar 23, 2026

⚠️ No Changeset found

Latest commit: d1a65b4

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link

vercel bot commented Mar 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agents-api Ready Ready Preview, Comment Mar 26, 2026 6:58am
agents-docs Ready Ready Preview, Comment Mar 26, 2026 6:58am
agents-manage-ui Ready Ready Preview, Comment Mar 26, 2026 6:58am

Request Review

@pullfrog
Copy link
Contributor

pullfrog bot commented Mar 23, 2026

TL;DR — Adds an automated PR comment with stable, human-readable preview URLs after Vercel deployments pass smoke checks, so reviewers don't have to hunt through Vercel bot comments for the right links.

Key changes

  • Add comment-preview-urls.sh script — New shell script that posts (or updates) an idempotent PR comment containing stable API and UI preview aliases, with an optional collapsible section showing raw Vercel deployment URLs.
  • Wire the script into the preview workflow — New step in preview-environments.yml runs the script after successful smoke checks, with issues: write permission added for comment creation.

Summary | 2 files | 2 commits | base: mainvarun/preview-stable-urls

Before: Preview URLs were only available in auto-generated Vercel bot comments with opaque hashes — easy to miss on busy PRs.
After: A dedicated "Preview URLs" comment is posted by github-actions[bot] after smoke checks pass, listing stable aliases for the UI, API, and API health endpoint. The comment is upserted via an HTML marker (<!-- preview-environments:stable-urls -->) so it stays current across pushes.

The script paginates through existing PR comments to find a prior marker match, then either PATCHes or POSTs accordingly. A temp file with a cleanup trap handles the comment body construction. When raw Vercel deployment URLs are available, they're included in a collapsible <details> block.

How does the idempotent upsert work? The comment body starts with an HTML comment marker. On each run the script fetches all PR comments (paginated, 100 per page), filters for those authored by github-actions[bot] containing the marker, and takes the last match. If found, it PATCHes that comment; otherwise it POSTs a new one.

comment-preview-urls.sh · preview-environments.yml

Pullfrog  | View workflow run | Triggered by Pullfrog | Using Claude Opus𝕏

Copy link
Contributor

@pullfrog pullfrog bot left a comment

Choose a reason for hiding this comment

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

Urgency: medium — This PR bundles two unrelated features (CI preview URL comments + message feedback persistence). The feedback feature is well-structured overall but has one data-integrity issue that should be addressed before merge.

PR scope mismatch: The title says "ci: surface stable preview URLs in PRs" but ~85% of the diff (15 of 18 files, ~5900 of ~6000 added lines) implements message feedback persistence — a new message_feedback table, DAL functions, API routes on both manage and run domains, integration tests, and a playground UI hook. Consider splitting this into two PRs or at minimum updating the title and description to reflect the actual content.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow runpullfrog.com𝕏

Comment on lines +89 to +103
const conversation = await getConversation(runDbClient)({
scopes: { tenantId, projectId },
conversationId,
});
if (!conversation) {
throw createApiError({ code: 'not_found', message: 'Conversation not found' });
}

const message = await getMessageById(runDbClient)({
scopes: { tenantId, projectId },
messageId,
});
if (!message) {
throw createApiError({ code: 'not_found', message: 'Message not found' });
}
Copy link
Contributor

Choose a reason for hiding this comment

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

getMessageById filters only by (scopes, messageId) — it does not verify the message belongs to conversationId. If a caller provides a valid conversationId and a messageId from a different conversation in the same tenant/project, the feedback row is saved with the mismatched conversationId.

Either:

  1. After fetching the message, assert message.conversationId === conversationId, or
  2. Use a query that includes conversationId in the WHERE clause.

The FK to conversations won't catch this because the conversationId in the insert payload is the one from the URL path, not from the message row.

import { createProtectedRoute, inheritedRunApiKeyAuth } from '@inkeep/agents-core/middleware';
import runDbClient from '../../../data/db/runDbClient';

type AppVariables = {
Copy link
Contributor

Choose a reason for hiding this comment

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

The AppVariables type is redeclared here, but it's already exported from ../../types. Use the shared type to avoid drift.

Suggested change
type AppVariables = {
import type { AppVariables } from '../../../types';
Suggested change
type AppVariables = {
import type { AppVariables } from '../../../types';

ALTER TABLE "message_feedback" ADD CONSTRAINT "message_feedback_conversation_fk" FOREIGN KEY ("tenant_id","project_id","conversation_id") REFERENCES "public"."conversations"("tenant_id","project_id","id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "message_feedback" ADD CONSTRAINT "message_feedback_message_fk" FOREIGN KEY ("tenant_id","project_id","message_id") REFERENCES "public"."messages"("tenant_id","project_id","id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "message_feedback_conversation_idx" ON "message_feedback" USING btree ("tenant_id","project_id","conversation_id");--> statement-breakpoint
CREATE INDEX "message_feedback_message_idx" ON "message_feedback" USING btree ("tenant_id","project_id","message_id"); No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing trailing newline — git diff flagged \ No newline at end of file. Minor, but worth fixing for clean diffs.

messageId,
type: body.type,
reasons: body.reasons ?? null,
userId: executionContext.metadata?.endUserId ?? null,
Copy link
Contributor

Choose a reason for hiding this comment

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

The userId field on the feedback row is populated from executionContext.metadata?.endUserId, but the unique constraint is (tenantId, projectId, messageId) — not scoped by user. This means if two different end-users submit feedback on the same message, the second overwrites the first. If that's the intended design (one canonical feedback per message), that's fine, but worth a conscious acknowledgment since the userId column exists and suggests per-user tracking was considered.

async onFeedback(feedback) {
try {
const response = await fetch(
`${PUBLIC_INKEEP_AGENTS_API_URL}/run/v1/conversations/${conversationId}/messages/${feedback.messageId}/feedback`,
Copy link
Contributor

Choose a reason for hiding this comment

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

The conversationId here comes from the component prop (the playground session ID). If the widget SDK's feedback.messageId ever refers to a message in a different conversation context (e.g. after a conversation reset without re-mounting), this URL would be wrong. Consider adding a defensive check or using the conversation ID from the message context if available.

Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(0) Total Issues | Risk: Low

This is a clean, low-risk CI enhancement that surfaces stable preview URLs (pr-<n>-*.preview.inkeep.com) as a sticky PR comment after successful smoke checks.

✅ What's Good

Well-structured implementation:

  • Uses established patterns from existing preview scripts (common.sh, require_env_vars)
  • Proper error handling with set -euo pipefail
  • Safe JSON encoding with jq -Rs for arbitrary content
  • Connection and max-time timeouts on curl calls
  • Idempotent upsert via HTML comment marker (<!-- preview-environments:stable-urls -->)

Correct workflow integration:

  • issues: write permission is appropriate (PR comments use the Issues API)
  • Step condition correctly gates on steps.smoke.outcome == 'success'
  • All required env vars are passed from workflow outputs

Security:

  • Uses ${{ github.token }} which is repo-scoped
  • URL variables come from controlled workflow outputs, not user input

💭 Consider (2)

Minor hygiene improvements noted as inline comments:

  • 💭 Consider: .github/scripts/preview/comment-preview-urls.sh:19 — Add cleanup trap for temp file
  • 💭 Consider: .github/scripts/preview/comment-preview-urls.sh:54 — Pagination limited to 100 comments

Neither affects correctness or security for typical PRs.

⚠️ Note on Prior Review

The pullfrog bot review mentions "message feedback persistence" features that are not present in the current PR diff. The current PR contains only CI/preview URL changes (2 files, 96 additions). The prior review appears to be from a different version of the branch or a different PR entirely — those findings are not applicable to this changeset.


✅ APPROVE

Summary: This is a well-implemented CI enhancement that follows existing patterns and improves developer experience by making stable preview URLs more discoverable. No blocking issues — ship it! 🚀

Note: Unable to submit formal approval due to GitHub App permissions. This review recommends approval.

Reviewers (2)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
pr-review-devops 5 0 0 0 2 0 0
pr-review-standards 0 0 0 0 0 0 0
Total 5 0 0 0 2 0 0

Note: 3 of the 5 devops findings were positive observations (INFO level), not issues.

curl --connect-timeout 10 --max-time 30 -fsS \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "Accept: application/vnd.github+json" \
"${COMMENTS_ENDPOINT}?per_page=100"
Copy link
Contributor

Choose a reason for hiding this comment

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

💭 Consider: Pagination limited to 100 comments

Issue: The GitHub API comments endpoint is paginated and the query fetches only per_page=100. If a PR has more than 100 comments, the existing preview URLs comment might not be found, leading to duplicate comments instead of an upsert.

Why: While uncommon, PRs with extensive discussion or automated bot activity could exceed this limit. The jq query correctly selects the last matching comment, but if the marker comment isn't in the first 100, it won't be found.

Fix: This is a known edge case that's acceptable for most PRs. If you want complete coverage, you'd need to iterate through pages. Alternatively, document this as a known limitation since busy PRs with 100+ comments are rare.

Refs:

Copy link
Collaborator

Choose a reason for hiding this comment

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

It is possible for our PRs to have over 100 comments. Especially with bot reviews

@github-actions github-actions bot deleted a comment from claude bot Mar 23, 2026
@github-actions
Copy link
Contributor

Preview URLs

Use these stable preview aliases for testing this PR:

These point to the same Vercel preview deployment as the bot comment, but they stay stable and easier to find.

Raw Vercel deployment URLs

@itoqa
Copy link

itoqa bot commented Mar 23, 2026

Ito Test Report ✅

18 test cases ran. 18 passed.

All 18 test cases passed with zero failures, confirming the preview URL publishing flow is functioning as designed end-to-end: sticky marker comments are created/updated idempotently, stable UI/API/API-health links are consistently published (including /health as expected 204), raw deployment URLs stay tucked in collapsed details, and behavior remains stable across reruns, rapid back/forward navigation, concurrent rerun/comment churn, and mobile viewing. Key safeguards also held in negative/adversarial scenarios (invalid PR input, smoke-fail and closed-event gating, forged marker protection, malformed URL sanitization, and safe 403 handling without partial writes), while one notable monitored risk was validated that markers pushed beyond the newest 100 comments can be missed by first-page queries, creating potential duplicate-comment risk if pagination is not accounted for.

✅ Passed (18)
Category Summary Screenshot
Adversarial Forged user marker comment remained unchanged after publish/update flow execution. ADV-1
Adversarial Malformed payload URLs remained encoded as safe http/https links with no executable script content observed. ADV-2
Adversarial Restricted issues:write simulation produced a controlled 403 path with no partial sticky-comment mutation. ADV-3
Adversarial Spam comments plus rapid reruns preserved stable URL discoverability and parseable marker content. ADV-4
Edge Newest bot marker was updated while older and forged marker comments stayed unchanged. EDGE-1
Edge Marker comments were absent from the first-page comment window while present in the full seeded set, confirming the measured pagination-risk scenario. EDGE-2
Edge Verified mobile check found no product defect; sticky preview comments are published to the PR timeline surface rather than the localhost app UI. EDGE-3
Edge Re-run with concurrent reruns and spam churn converged to one valid marker comment without unstable final state. EDGE-4
Logic Invalid PR input exits early and does not mutate the sticky comment state. LOGIC-1
Logic Smoke-failure path keeps publish guarded and leaves comment state unchanged. LOGIC-2
Logic Closed-event flow excludes smoke/publish behavior and does not update stable URL comments. LOGIC-3
Logic The latest sticky marker comment remained reliably discoverable through 3 rapid back/forward cycles with repeated details toggling. LOGIC-4
Happy-path Sticky marker comment behavior is correctly implemented and idempotent in the publish script. ROUTE-1
Happy-path Rerun simulation preserved a single sticky marker comment and kept update-in-place behavior stable across repeated back/forward navigation. ROUTE-2
Happy-path Raw deployment URLs are rendered in a collapsed details block while stable links stay visible. ROUTE-3
Happy-path UI alias reachability criteria intentionally accept auth-gated redirect/login responses. ROUTE-4
Happy-path API health behavior is correct because /health is defined as HTTP 204 with no body. ROUTE-5
Happy-path Workflow-computed stable URLs and published sticky comment links remain consistent. ROUTE-6

Commit: 77c9096

View Full Run


Tell us how we did: Give Ito Feedback

@amikofalvy
Copy link
Collaborator

@claude read the comments on this PR and address them.

@inkeep-internal-ci
Copy link
Contributor

inkeep-internal-ci bot commented Mar 26, 2026

Claude finished @amikofalvy's task in 1m 18s —— View job


Addressing PR comments

  • Read changed files for full context
  • Add cleanup trap for temp file (comment-preview-urls.sh:19)
  • Fix pagination to handle >100 comments (comment-preview-urls.sh:54)
  • Commit and push changes

Changes made

Pushed d1a65b4 addressing two review comments:

  1. Temp file cleanup trap — Added trap 'rm -f "${COMMENT_BODY_FILE}"' EXIT after mktemp (line 20) so the temp file is cleaned up on exit.

  2. Paginated comment search — Replaced the single per_page=100 fetch with a pagination loop that iterates through all pages. This prevents duplicate comments on PRs with 100+ comments (confirmed as a real scenario by @amikofalvy). The loop keeps the last matching marker comment ID across all pages, then breaks when a page returns fewer than 100 results.
    | Branch

- Add EXIT trap to clean up mktemp file
- Paginate through all PR comments when searching for the existing
  marker comment, fixing duplicate-comment risk on PRs with 100+ comments

Co-authored-by: Andrew Mikofalvy <amikofalvy@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@itoqa
Copy link

itoqa bot commented Mar 26, 2026

Ito Test Report ✅

17 test cases ran. 17 passed.

The unified run passed all 17 of 17 test cases with zero failures, confirming the sticky preview URL system is functioning as designed end to end. Key validations showed publish/update is correctly gated to smoke-check success (and skipped on smoke failure), sticky comments update idempotently to a single authoritative bot marker across reruns/races/navigation/pagination while ignoring non-bot spoof markers, links are reachable and safely normalized (/health), HTTPS-only and expected-domain constrained with no secret leakage, raw deployment URLs stay collapsed by default and mobile interactions remain usable, and workflow logic enforces correct PR targeting, write-permission requirements, and cross-PR isolation.

✅ Passed (17)
Category Summary Screenshot
Adversarial Local mocked concurrent reruns converged to one authoritative sticky marker comment (no duplicates) with latest successful attempt content. ADV-1
Adversarial Sticky comment text and details content did not expose internal infrastructure or secret-like values. ADV-2
Adversarial Mutation path requires workflow token and issues: write, matching read-only users being unable to mutate workflow/comment state. N/A
Adversarial Rendered links used https and matched the expected preview domain pattern only. ADV-4
Adversarial Comment publishing is scoped by PR_NUMBER and PR-specific URLs/concurrency, supporting cross-PR isolation. N/A
Edge Pagination over more than 100 comments selected the page-2 bot marker and updated it in place without creating a duplicate. EDGE-1
Edge A non-bot spoof marker was ignored while the bot-authored marker was selected for patch. EDGE-2
Edge Source verification shows workflow_dispatch validates and uses pr_number for PR-targeted comment publishing. N/A
Edge At 390x844 viewport, the stable UI, API, and API health links were present and operable, and the Raw Vercel deployment URLs details control remained interactable without blocking core usage. EDGE-4
Edge Navigation interruptions did not corrupt final sticky state; after rerun completion the authoritative marker comment remained singular and valid. EDGE-5
Logic API health link was normalized to a single /health suffix and the endpoint was reachable. LOGIC-1
Logic Across single rerun and rapid rerun race simulations, the <!-- preview-environments:stable-urls --> marker remained in raw markdown and stayed discoverable with one authoritative bot comment. LOGIC-2
Logic Smoke failure path kept publish step skipped, so sticky-comment mutation did not run on failure. LOGIC-4
Happy-path Stable preview alias comment flow is implemented with smoke-success gating and sticky marker update logic. ROUTE-1
Happy-path Stable UI/API/API health links opened successfully from the sticky comment and used expected preview hosts. ROUTE-2
Happy-path Local fixture-mode reruns updated existing marker comment id 424242 in place during navigation churn and rapid rerun stress; marker count remained 1. ROUTE-3
Happy-path The sticky-comment fixture preserved primary stable alias links, showed the raw deployment URLs in a collapsed details block by default, expanded correctly, and exposed valid HTTPS deployment links. Mobile (390x844) interactions remained functional for link tapping and details toggle. ROUTE-4

Commit: d1a65b4

View Full Run


Tell us how we did: Give Ito Feedback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants