Skip to content

ci: surface stable preview URLs in PRs#2799

Open
vnv-varun wants to merge 1 commit intomainfrom
varun/preview-stable-urls
Open

ci: surface stable preview URLs in PRs#2799
vnv-varun wants to merge 1 commit 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: 77c9096

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 23, 2026 2:26pm
agents-docs Ready Ready Preview, Comment Mar 23, 2026 2:26pm
agents-manage-ui Ready Ready Preview, Comment Mar 23, 2026 2:26pm

Request Review

@pullfrog
Copy link
Contributor

pullfrog bot commented Mar 23, 2026

TL;DR — This PR adds message feedback persistence (thumbs up/down) across the full stack — new message_feedback runtime table, DAL functions, Run and Manage API endpoints, and playground UI wiring — alongside a CI improvement that posts stable preview URLs as PR comments after successful smoke checks.

Key changes

  • Message feedback schema & migration — Adds a message_feedback table to the runtime database with upsert-on-conflict semantics, foreign keys to conversations/messages with cascade deletes, and composite indexes.
  • Data access layer for feedback — New upsert, get, getByConversation, and delete DAL functions in agents-core exported from the barrel.
  • Run API: submit & delete feedbackPOST and DELETE on /run/v1/conversations/{conversationId}/messages/{messageId}/feedback with conversation/message existence checks and inheritedRunApiKeyAuth.
  • Manage API: list conversation feedbackGET on /manage/.../conversations/{conversationId}/feedback with requireProjectPermission('view') for admin-side feedback retrieval.
  • Playground UI wiringonFeedback callback added to the InkeepEmbeddedChat widget to persist feedback to the Run API.
  • CI: stable preview URL comments — New workflow step and shell script post an idempotent PR comment with stable preview aliases after Vercel deployments pass smoke checks.

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


Message feedback database schema

Before: No mechanism to persist user feedback on assistant messages.
After: A message_feedback table stores positive/negative ratings per message with optional structured reasons (JSON), enforced via a unique constraint on (tenant_id, project_id, message_id).

The table uses the standard projectScoped and timestamps mixins. Foreign keys cascade-delete when the parent conversation or message is removed. Two B-tree indexes cover the conversation-level and message-level lookup patterns. Drizzle ORM relations are added to conversations and messages for eager-loading.

runtime-schema.ts · 0024_supreme_wolf_cub.sql · _journal.json


Data access layer

Before: No DAL functions for feedback.
After: Four curried DAL functions — upsertMessageFeedback, getMessageFeedback, getConversationFeedback, deleteMessageFeedback — using projectScopedWhere for tenant isolation.

upsertMessageFeedback uses Drizzle's onConflictDoUpdate targeting the message-unique constraint so re-submitting feedback on the same message updates in place rather than inserting a duplicate. All functions are exported from the agents-core barrel.

message-feedback.ts · index.ts · message-feedback.test.ts (DAL)


Run API feedback endpoints

Before: No way to submit or remove feedback via the Run API.
After: POST /run/v1/conversations/{conversationId}/messages/{messageId}/feedback upserts feedback; DELETE on the same path removes it. Both verify conversation and message existence first.

The routes use inheritedRunApiKeyAuth() and extract tenantId/projectId from the execution context. The submit handler generates a new ID via generateId() and optionally attaches endUserId from execution metadata.

message-feedback.ts (route) · run/index.ts · message-feedback.test.ts (run)


Manage API conversation feedback endpoint

Before: No admin-side endpoint to retrieve feedback for a conversation.
After: GET /manage/.../conversations/{conversationId}/feedback returns all feedback entries scoped to a conversation, gated by requireProjectPermission('view').

conversationFeedback.ts · manage/routes/index.ts · conversationFeedback.test.ts


Playground UI feedback wiring

Before: The embedded chat widget had no onFeedback handler; thumbs up/down actions were purely client-side.
After: An onFeedback callback POSTs to the Run API with the conversation ID, message ID, feedback type, and reasons, using the temp API key and tenant/project headers.

chat-widget.tsx


CI: stable preview URL comments

Before: Preview URLs were only available in Vercel bot comments with auto-generated hashes.
After: A new workflow step runs comment-preview-urls.sh after successful smoke checks, posting (or updating) a PR comment with human-readable stable aliases for the API and UI, plus a collapsible section with raw Vercel deployment URLs.

The script uses an HTML comment marker (<!-- preview-environments:stable-urls -->) for idempotent upsert. The workflow adds issues: write permission.

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

Pullfrog  | View workflow run | Triggered by Pullfrogpullfrog.com𝕏

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.

COMMENT_MARKER="<!-- preview-environments:stable-urls -->"
COMMENTS_ENDPOINT="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments"
API_HEALTH_URL="${API_URL%/}/health"
COMMENT_BODY_FILE="$(mktemp)"
Copy link
Contributor

Choose a reason for hiding this comment

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

💭 Consider: Add cleanup trap for temp file

Issue: The temporary file created with mktemp is not explicitly cleaned up on exit.

Why: While GitHub Actions runners are ephemeral and the file will be deleted when the runner terminates, explicit cleanup is a best practice and improves local testability of the script.

Fix: Add a trap after this line:

trap 'rm -f "${COMMENT_BODY_FILE}"' EXIT

Refs:

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:

@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

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.

1 participant