Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/deploy-helpers/src/preview/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ export interface DeploymentResource {
limits?: CfUserLimits;
placement?: CfPlacement;
cache?: CacheOptions;
annotations?: {
"workers/message"?: string;
"workers/pull_request_number"?: string;
"workers/pull_request_url"?: string;
"workers/repository_url"?: string;
"workers/tag"?: string;
};
env?: EnvBindings;
created_on: string;
}
Expand All @@ -102,6 +109,9 @@ export type CreatePreviewDeploymentRequestParams = {
compatibility_flags?: string[];
annotations?: {
"workers/message"?: string;
"workers/pull_request_number"?: string;
"workers/pull_request_url"?: string;
"workers/repository_url"?: string;
"workers/tag"?: string;
};
migrations?: CfWorkerInit["migrations"];
Expand Down
162 changes: 151 additions & 11 deletions packages/deploy-helpers/src/preview/preview.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import path from "node:path";
import {
APIError,
configFileName,
getBindingTypeFriendlyName,
UserError,
Expand Down Expand Up @@ -27,9 +28,12 @@ import {
getBranchName,
getHeadCommitMessage,
getHeadCommitRef,
getPullRequestMetadata,
getRepositoryUrl,
resolveWorkerName,
shouldUseCIMetadataFallback,
} from "./shared";
import type { PullRequestMetadata } from "./shared";
import type { WorkerBuildResult } from "../shared/types";
import type {
Binding,
Expand Down Expand Up @@ -81,6 +85,10 @@ type MergedVersionLevel = {
value: Config["cache"];
fromConfig: boolean;
};
pull_request?: {
value: string;
fromConfig: false;
};
assets?: {
value: {
directory?: string;
Expand Down Expand Up @@ -240,6 +248,8 @@ async function assemblePreviewDeploymentSettings(
options: {
message?: string;
tag?: string;
repositoryUrl?: string;
pullRequest?: PullRequestMetadata;
assetsOptions?: PreviewAssetsOptions;
}
): Promise<CreatePreviewDeploymentRequestParams> {
Expand Down Expand Up @@ -276,9 +286,16 @@ async function assemblePreviewDeploymentSettings(
if (config.compatibility_flags && config.compatibility_flags.length > 0) {
request.compatibility_flags = config.compatibility_flags;
}
if (options.message || options.tag) {
const repositoryUrl = options.repositoryUrl;
const pullRequest = options.pullRequest;
if (options.message || options.tag || repositoryUrl || pullRequest) {
request.annotations = {
...(options.message && { "workers/message": options.message }),
...(pullRequest?.number && {
"workers/pull_request_number": pullRequest.number,
}),
...(pullRequest?.url && { "workers/pull_request_url": pullRequest.url }),
...(repositoryUrl && { "workers/repository_url": repositoryUrl }),
...(options.tag && { "workers/tag": options.tag }),
};
}
Expand Down Expand Up @@ -374,7 +391,9 @@ function buildMergedScriptLevel(

function buildMergedVersionLevel(
config: Config,
deployment: DeploymentResource
deployment: DeploymentResource,
repositoryUrl?: string,
pullRequest?: PullRequestMetadata
): MergedVersionLevel {
const previews = config.previews as PreviewsConfig | undefined;
const configBindingNames = new Set(
Expand Down Expand Up @@ -429,6 +448,17 @@ function buildMergedVersionLevel(
fromConfig: previews?.cache !== undefined || config.cache !== undefined,
};
}
const deploymentPullRequestUrl =
deployment.annotations?.["workers/pull_request_url"] ?? pullRequest?.url;
const deploymentPullRequestNumber =
deployment.annotations?.["workers/pull_request_number"] ??
pullRequest?.number;
if (deploymentPullRequestUrl || deploymentPullRequestNumber) {
result.pull_request = {
value: deploymentPullRequestUrl ?? `#${deploymentPullRequestNumber}`,
fromConfig: false,
};
}
if (config.assets) {
result.assets = {
value: {
Expand All @@ -448,6 +478,81 @@ function buildMergedVersionLevel(
return result;
}

function hasPreviewMetadataAnnotations(
request: CreatePreviewDeploymentRequestParams
): boolean {
return !!(
request.annotations?.["workers/pull_request_number"] ||
request.annotations?.["workers/pull_request_url"] ||
request.annotations?.["workers/repository_url"]
);
}

function omitPreviewMetadataAnnotations(
request: CreatePreviewDeploymentRequestParams
): CreatePreviewDeploymentRequestParams {
const annotations = {
...(request.annotations?.["workers/message"] && {
"workers/message": request.annotations["workers/message"],
}),
...(request.annotations?.["workers/tag"] && {
"workers/tag": request.annotations["workers/tag"],
}),
};

return {
...request,
annotations:
Object.keys(annotations).length > 0 ? annotations : undefined,
};
}

function isPreviewMetadataAnnotationsUnsupportedError(error: unknown): boolean {
if (typeof error !== "object" || error === null) {
return false;
}

const code = "code" in error ? error.code : undefined;
if (code !== undefined && code !== 10021) {
return false;
}

const messages = [
error instanceof Error ? error.message : undefined,
error instanceof APIError ? error.text : undefined,
...getErrorNoteTexts(error),
]
.filter(Boolean)
.join("\n");

return (
messages.includes("annotations not allowed") &&
(messages.includes("workers/pull_request") ||
messages.includes("workers/repository_url"))
);
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

function getErrorNoteTexts(error: unknown): string[] {
if (typeof error !== "object" || error === null || !("notes" in error)) {
return [];
}

const notes = error.notes;
if (!Array.isArray(notes)) {
return [];
}

return notes.flatMap((note) => [
typeof note === "object" &&
note !== null &&
"text" in note &&
typeof note.text === "string"
? note.text
: undefined,
...getErrorNoteTexts(note),
]).filter((text): text is string => text !== undefined);
}

function formatPreviewResource(
previewResource: PreviewResource,
scriptLevel: MergedScriptLevel,
Expand Down Expand Up @@ -565,6 +670,13 @@ function formatDeploymentResource(
versionLevel.cache.fromConfig,
]);
}
if (versionLevel.pull_request !== undefined) {
settingsRows.push([
"pull_request",
versionLevel.pull_request.value,
versionLevel.pull_request.fromConfig,
]);
}
if (settingsRows.length > 0) {
lines.push("");
lines.push(...formatAlignedRows(settingsRows));
Expand Down Expand Up @@ -682,6 +794,8 @@ export async function preview(
!args.message && shouldUseCIMetadataFallback()
? getHeadCommitMessage()
: undefined;
const repositoryUrl = getRepositoryUrl();
const pullRequest = getPullRequestMetadata();
Comment on lines +797 to +798

@devin-ai-integration devin-ai-integration Bot Aug 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Local previews now send repository details even outside CI, unlike other automatically detected metadata

The repository address is attached to every preview deployment (getRepositoryUrl() at packages/deploy-helpers/src/preview/preview.ts:766) even when not running in CI, because it falls back to reading the local git remote, unlike the commit-based metadata which is only used in CI.
Impact: Developers running previews from their own machine unexpectedly upload their repository address with each deployment.

Inconsistency with the existing CI-metadata gating

Commit tag/message fallbacks are guarded by shouldUseCIMetadataFallback() (packages/deploy-helpers/src/preview/preview.ts:760-765), which requires CI=1/CI=true. getRepositoryUrl() (packages/deploy-helpers/src/preview/shared.ts:92-120) first checks CI env vars, but when none are present it shells out to git config --get remote.origin.url and returns the normalized remote, so local runs always populate workers/repository_url. The PR description states the metadata is added "when it can be detected from CI environment variables", so the unconditional git-remote fallback goes beyond the stated behavior. Consider gating the git fallback (or the whole call) behind shouldUseCIMetadataFallback().

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


let existingPreview: PreviewResource | null = null;
try {
Expand Down Expand Up @@ -732,25 +846,51 @@ export async function preview(
{
message: args.message ?? fallbackMessage,
tag: args.tag ?? fallbackTag,
repositoryUrl,
pullRequest,
assetsOptions,
}
);
const deployment = await createPreviewDeployment(
config,
accountId,
workerName,
previewResource.id,
deploymentRequest,
{ ignoreDefaults }
);
let deployment: DeploymentResource;
try {
deployment = await createPreviewDeployment(
config,
accountId,
workerName,
previewResource.id,
deploymentRequest,
{ ignoreDefaults }
);
} catch (error) {
if (
hasPreviewMetadataAnnotations(deploymentRequest) &&
isPreviewMetadataAnnotationsUnsupportedError(error)
) {
deployment = await createPreviewDeployment(
config,
accountId,
workerName,
previewResource.id,
omitPreviewMetadataAnnotations(deploymentRequest),
{ ignoreDefaults }
);
} else {
throw error;
}
}

if (args.json) {
logger.log(
JSON.stringify({ preview: previewResource, deployment }, null, 2)
);
} else {
const scriptLevel = buildMergedScriptLevel(config, previewResource);
const versionLevel = buildMergedVersionLevel(config, deployment);
const versionLevel = buildMergedVersionLevel(
config,
deployment,
repositoryUrl,
pullRequest
);
const configName = configFileName(config.configPath);
logger.log(
formatPreviewResource(
Expand Down
Loading
Loading