Skip to content

feat: add gh-pr-release-tagger task to label released PRs#171

Open
comfy-pr-bot wants to merge 2 commits intomainfrom
feature/gh-pr-release-tagger
Open

feat: add gh-pr-release-tagger task to label released PRs#171
comfy-pr-bot wants to merge 2 commits intomainfrom
feature/gh-pr-release-tagger

Conversation

@comfy-pr-bot
Copy link
Copy Markdown
Member

@comfy-pr-bot comfy-pr-bot commented Feb 19, 2026

Summary

  • Adds app/tasks/gh-pr-release-tagger/index.ts - new task that labels original PRs (merged to main) with released:core or released:cloud when their backports are confirmed deployed
  • Adds app/tasks/gh-pr-release-tagger/index.spec.ts - test suite covering extraction, state shape, comparison logic, and deduplication
  • Registers the new task in app/tasks/run-gh-tasks.ts for 5-minute scheduled execution

How it works

  1. Core: Reads Comfy-Org/desktop latest release → package.jsonconfig.frontend.version to find the deployed frontend version tag, then resolves its release branch
  2. Cloud: Reads Comfy-Org/cloud → frontend-version.json for the release branch + ArgoCD prod overlay for the deployed commit SHA
  3. Checks if the deployed ref has already been processed (PRReleaseTaggerState collection, unique on {target, deployedRef})
  4. Lists merged PRs on the release branch (capped at 200), compares each PR's merge commit against the deployed ref
  5. For backport PRs: extracts original PR number from body (Backport of #NNN), labels the original PR on main
  6. For direct PRs: labels them directly
  7. Ensures labels exist in repo before applying (creates with blue color if missing, handles 422 race)
  8. Tracks all labeled PRs in MongoDB PRReleaseTaggerState collection, preserving partial progress on failure

Test plan

  • extractOriginalPRNumber: standard backport, cloud backport, non-backport, edge cases
  • Label naming: correct labels for each target
  • State structure: valid shape, all taskStatus values, direct PRs (no backport)
  • Comparison logic: behind/identical = released, ahead/diverged = not released
  • Deduplication: skip previously labeled PRs

@comfy-pr-bot comfy-pr-bot requested a review from snomiao February 19, 2026 01:52
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Feb 19, 2026

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

Project Deployment Actions Updated (UTC)
comfy-pr Ready Ready Preview, Comment Apr 4, 2026 4:20am

Request Review

Adds a new GitHub task that monitors core/1.* and cloud/1.* branches
in ComfyUI_frontend and automatically adds 'released:core' or
'released:cloud' labels to PRs included in each release.

- Lists release branches matching the configured patterns
- Fetches releases and groups by branch, filtering by processSince
- Compares consecutive releases to find included commits
- Labels associated PRs with the appropriate branch prefix label
- Ensures labels exist in repo before applying (creates if missing)
- Tracks processed releases in MongoDB GithubPRReleaseTaggerTask collection
- Registers in run-gh-tasks.ts for 5-minute scheduled execution

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@snomiao snomiao force-pushed the feature/gh-pr-release-tagger branch from 73a21c8 to 2d272b1 Compare April 4, 2026 04:05
@snomiao snomiao marked this pull request as ready for review April 4, 2026 04:09
Copilot AI review requested due to automatic review settings April 4, 2026 04:10
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 scheduled GitHub automation task intended to label ComfyUI_frontend PRs as released to either “core” or “cloud”, based on deployed versions resolved from the Desktop and Cloud repos.

Changes:

  • Add gh-pr-release-tagger task to resolve deployed refs and apply released:{core|cloud} labels to qualifying PRs.
  • Register the new task in the global GitHub task runner so it runs with the other scheduled GitHub tasks.
  • Add a Bun test file for basic state/utility assertions, plus a lockfile update.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 8 comments.

File Description
bun.lock Lockfile update (dependency metadata/sha addition).
app/tasks/run-gh-tasks.ts Registers “GitHub PR Release Tagger Task” for scheduled execution.
app/tasks/gh-pr-release-tagger/index.ts Implements deployed-ref resolution and PR labeling + MongoDB state persistence.
app/tasks/gh-pr-release-tagger/index.spec.ts Adds basic tests (type/utility checks) for the new task module.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +23 to +41
const FRONTEND_REPO = { owner: "Comfy-Org", repo: "ComfyUI_frontend" };

export type PRReleaseTaggerState = {
target: "core" | "cloud";
deployedRef: string; // version tag or commit SHA
branch: string; // e.g. "core/1.41" or "cloud/1.41"
labeledOriginalPRs: Array<{
prNumber: number;
prUrl: string;
prTitle: string;
backportPrNumber: number | null;
labeledAt: Date;
}>;
taskStatus: "checking" | "completed" | "failed";
checkedAt: Date;
};

export const PRReleaseTaggerState = db.collection<PRReleaseTaggerState>("PRReleaseTaggerState");

Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

The PR description says this task tracks processed releases in a GithubPRReleaseTaggerTask collection (unique by releaseUrl) and labels PRs by iterating releases/compare ranges. The implementation instead stores state in PRReleaseTaggerState keyed by {target,deployedRef} and does not persist per-release processing. Please align the code with the described data model/approach, or update the PR description to match the actual behavior (including collection name + unique key).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Updated PR description to match the actual implementation: collection is PRReleaseTaggerState keyed by {target, deployedRef}. Fixed in 72aed00.

Comment on lines +71 to +72
const content = "content" in pkgContent.data ? pkgContent.data.content : "";
const pkg = JSON.parse(Buffer.from(content, "base64").toString("utf-8"));
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

getContent fallback sets content to "" when the response isn't a file (or is missing content), which will cause JSON.parse("") to throw a generic SyntaxError. Consider validating that pkgContent.data is a file with content present (and that encoding is base64) and throw a clearer error if not.

Suggested change
const content = "content" in pkgContent.data ? pkgContent.data.content : "";
const pkg = JSON.parse(Buffer.from(content, "base64").toString("utf-8"));
if (!("type" in pkgContent.data) || pkgContent.data.type !== "file") {
throw new Error(`Expected desktop package.json at ${tag} to be a file`);
}
if (!("content" in pkgContent.data) || !pkgContent.data.content) {
throw new Error(`Expected desktop package.json at ${tag} to include file content`);
}
if (!("encoding" in pkgContent.data) || pkgContent.data.encoding !== "base64") {
throw new Error(
`Expected desktop package.json at ${tag} to use base64 encoding, got ${String(pkgContent.data.encoding)}`,
);
}
const pkg = JSON.parse(Buffer.from(pkgContent.data.content, "base64").toString("utf-8"));

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Fixed in 72aed00 — now validates content field exists before parsing, throws a descriptive error if missing.

Comment on lines +91 to +100
const versionFile = await ghc.repos.getContent({
owner: "Comfy-Org",
repo: "cloud",
path: "frontend-version.json",
});
const versionContent = "content" in versionFile.data ? versionFile.data.content : "";
const versionConfig = JSON.parse(Buffer.from(versionContent, "base64").toString("utf-8"));
const branch: string = versionConfig.releaseBranch;
if (!branch) throw new Error("No releaseBranch in cloud frontend-version.json");

Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

Same issue as core: if frontend-version.json isn't returned as a file with content, JSON.parse will throw a generic SyntaxError. Add explicit validation for versionFile.data shape and provide a targeted error message (e.g., missing file / unexpected directory response).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Fixed in 72aed00 — added content validation for both cloud files (frontend-version.json and overlay values.yaml).

Comment on lines +127 to +145
async function ensureLabelExists(labelName: string) {
const { owner, repo } = FRONTEND_REPO;
try {
await gh.issues.getLabel({ owner, repo, name: labelName });
} catch (e: unknown) {
if ((e as { status?: number }).status === 404) {
const target = labelName.split(":")[1] || labelName;
await gh.issues.createLabel({
owner,
repo,
name: labelName,
color: "0075ca",
description: `PR has been released to ${target}`,
});
logger.info(`Created label '${labelName}'`);
} else {
throw e;
}
}
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

ensureLabelExists treats any non-404 error as fatal, but if multiple workers run this task concurrently a label creation race can produce a 422 "already_exists" from createLabel. Handle that case explicitly (treat it as success) to avoid failing the entire run due to a benign race.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Fixed in 72aed00ensureLabelExists now catches 422 (already_exists) from createLabel and treats it as success.

Comment on lines +179 to +189
const mergedPRs = await ghPageFlow(ghc.pulls.list, { per_page: 100 })({
...FRONTEND_REPO,
base: branch,
state: "closed",
sort: "updated",
direction: "desc",
})
.filter((pr) => pr.merged_at !== null)
.toArray();

logger.info(`${target}: found ${mergedPRs.length} merged PRs on ${branch}`);
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

ghPageFlow(...).toArray() will paginate and load all merged PRs for the release branch into memory every run. On large repos this can be very slow and will also drive a large number of downstream API calls. Consider limiting candidates (e.g., stop pagination once merged_at/updated_at is older than the earliest unprocessed deployedRef window, or persist a checkpoint so runs are incremental).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Fixed in 72aed00 — added .slice(0, 200) to cap at 200 PRs (2 pages), preventing unbounded pagination.

Comment on lines +201 to +218
for (const backportPR of mergedPRs) {
// Check if backport PR's merge commit is ancestor of deployed ref
if (!backportPR.merge_commit_sha) continue;

try {
const comparison = await ghc.repos.compareCommits({
...FRONTEND_REPO,
base: deployedRef,
head: backportPR.merge_commit_sha,
});
// If status is "behind" or "identical", the merge commit is included in deployed ref
if (comparison.data.status !== "behind" && comparison.data.status !== "identical") {
continue; // merge commit is ahead of deployed ref, not yet released
}
} catch {
// If comparison fails, skip this PR
continue;
}
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

The main loop does an O(N) per-PR compareCommits call (and then potentially more calls to fetch/label the original PR). Combined with scanning all merged PRs, this is very likely to hit GitHub rate limits and lengthen runs. A more scalable approach is to compute the released commit range once (e.g., compare previous release tag -> deployedRef) and only inspect commits/PRs in that range, or otherwise batch/limit comparisons.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Acknowledged — the per-PR comparison is the core logic to determine inclusion. The 200-PR cap from the pagination fix already bounds the total API calls. The compareCommits calls use the cached ghc client. A more optimized commit-range approach would require knowing the previous deployed ref, which we can add as a follow-up optimization.

Comment on lines +313 to +320
} catch (err: unknown) {
logger.error(`${target}: failed — ${(err as Error).message}`);
await save({
target,
deployedRef,
taskStatus: "failed",
checkedAt: new Date(),
});
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

On failure, the task overwrites state with taskStatus: "failed" but does not persist any labeledOriginalPRs that may have been successfully labeled earlier in this run (and it also resets nothing explicitly). If the run partially succeeds and then throws, the DB state won't reflect what was actually labeled. Consider updating state incrementally as labels are applied, or include the accumulated labeledOriginalPRs in the failure save/update.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Fixed in 72aed00 — moved labeledOriginalPRs declaration outside the try block and included it in the failure save, so partial progress is persisted.

Comment on lines +2 to +12
import type { PRReleaseTaggerState } from "./index";

describe("PRReleaseTaggerState", () => {
describe("extractOriginalPRNumber", () => {
// Re-implement locally for testing (pure function)
function extractOriginalPRNumber(body: string | null): number | null {
if (!body) return null;
const match = body.match(/Backport of #(\d+)/);
return match ? parseInt(match[1], 10) : null;
}

Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

These tests re-implement extractOriginalPRNumber locally instead of exercising the real implementation in index.ts. This can silently diverge (tests still pass while production logic changes). Prefer exporting the helper (or moving it to a small util module) and testing the actual function; similarly, consider adding tests that cover the real GitHub-labeling decision logic with mocked gh/ghc.

Suggested change
import type { PRReleaseTaggerState } from "./index";
describe("PRReleaseTaggerState", () => {
describe("extractOriginalPRNumber", () => {
// Re-implement locally for testing (pure function)
function extractOriginalPRNumber(body: string | null): number | null {
if (!body) return null;
const match = body.match(/Backport of #(\d+)/);
return match ? parseInt(match[1], 10) : null;
}
import { extractOriginalPRNumber, type PRReleaseTaggerState } from "./index";
describe("PRReleaseTaggerState", () => {
describe("extractOriginalPRNumber", () => {

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Fixed in 72aed00 — exported extractOriginalPRNumber from index.ts and updated tests to import and test the real function.

- Validate getContent responses instead of falling back to empty string
- Handle 422 race condition in ensureLabelExists for concurrent workers
- Limit PR pagination to 200 to reduce API calls
- Persist labeledOriginalPRs on failure for partial success tracking
- Export extractOriginalPRNumber and test the real implementation

Amp-Thread-ID: https://ampcode.com/threads/T-019d56a6-1a9b-72db-9d46-7bb5f89379bb
Co-authored-by: Amp <amp@ampcode.com>
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.

3 participants