From 64fcac4d938562335c6358f7e368494d944c8cf2 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Wed, 29 Jul 2026 10:29:11 +0200 Subject: [PATCH 1/4] Label vendored-sync drift issues with Area-dotnet test (MTP) Every entry in eng/vendored-files.json is 'dotnet test' <-> Microsoft.Testing.Platform shared source, so drift issues opened by the vendored-files workflow now also carry the MTP area label and land in the normal area triage queue. - Apply both labels on issue creation and backfill the area label on issues opened before this change. - Create the area label only when missing so the repo-owned description/color is not overwritten by the workflow. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 577f0b32-950f-4628-bd19-76ff5dbcabeb --- .github/scripts/check_vendored_files.py | 50 ++++++++++++++++++++-- .github/workflows/check-vendored-files.yml | 3 +- eng/vendored-files.md | 2 +- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/.github/scripts/check_vendored_files.py b/.github/scripts/check_vendored_files.py index fc05377f7cab..c0cb7fbb95b6 100644 --- a/.github/scripts/check_vendored_files.py +++ b/.github/scripts/check_vendored_files.py @@ -13,7 +13,8 @@ The check mode is idempotent: existing issues are matched by label `area-vendored-sync` plus a hidden HTML marker in the body of the form -``. +``. Issues are additionally +labelled `Area-dotnet test (MTP)` for area triage. See eng/vendored-files.md for the manifest schema and reconciliation workflow. """ @@ -38,6 +39,10 @@ REPO_ROOT = Path(__file__).resolve().parents[2] MANIFEST_PATH = REPO_ROOT / "eng" / "vendored-files.json" ISSUE_LABEL = "area-vendored-sync" +# Every entry in the manifest is 'dotnet test' <-> Microsoft.Testing.Platform shared +# source, so drift issues are also routed to the MTP area label for triage. +AREA_LABEL = "Area-dotnet test (MTP)" +ISSUE_LABELS = [ISSUE_LABEL, AREA_LABEL] ISSUE_REPO = os.environ.get("VENDORED_SYNC_REPO", "dotnet/sdk") MAX_DIFF_LINES = 300 SHA_RE = re.compile(r"^[0-9a-f]{40}$") @@ -384,7 +389,7 @@ def _list_open_sync_issues() -> list[dict[str, Any]]: "--label", ISSUE_LABEL, "--state", "open", "--limit", "200", - "--json", "number,title,body", + "--json", "number,title,body,labels", ]) if rc != 0: print(f"gh issue list failed: {err}", file=sys.stderr) @@ -405,7 +410,12 @@ def find_existing_issue(marker: str) -> dict[str, Any] | None: def ensure_label() -> None: - """Ensure the sync label exists. `gh label create --force` is idempotent.""" + """Ensure the labels applied to drift issues exist. + + The sync label is owned by this workflow, so it is force-created (idempotent + and self-healing). The area label is owned by the repo's regular triage + taxonomy, so it is only created when missing and never overwritten. + """ _gh([ "label", "create", ISSUE_LABEL, "--repo", ISSUE_REPO, @@ -413,6 +423,12 @@ def ensure_label() -> None: "--color", "fbca04", "--force", ]) + _gh([ + "label", "create", AREA_LABEL, + "--repo", ISSUE_REPO, + "--description", "Issues related to 'dotnet test' with Microsoft.Testing.Platform", + "--color", "d4c5f9", + ]) _STATUS_TITLES = { @@ -422,6 +438,18 @@ def ensure_label() -> None: } +def _label_flags() -> list[str]: + flags: list[str] = [] + for label in ISSUE_LABELS: + flags += ["--label", label] + return flags + + +def _missing_labels(issue: dict[str, Any]) -> list[str]: + present = {(item.get("name") or "") for item in (issue.get("labels") or [])} + return [label for label in ISSUE_LABELS if label not in present] + + def upsert_issue(result: DriftResult, dry_run: bool) -> None: marker = _marker(result.entry.id, result.source_index) body = _render_issue_body(result) @@ -431,6 +459,7 @@ def upsert_issue(result: DriftResult, dry_run: bool) -> None: if dry_run: print(f"\n--- would create/update issue ({result.entry.id}#{result.source_index}) ---") print(f"title: {title}") + print(f"labels: {', '.join(ISSUE_LABELS)}") print(body[:2000]) print("---") return @@ -441,7 +470,7 @@ def upsert_issue(result: DriftResult, dry_run: bool) -> None: "issue", "create", "--repo", ISSUE_REPO, "--title", title, - "--label", ISSUE_LABEL, + *_label_flags(), "--body-file", "-", ], input_data=body) if rc != 0: @@ -450,6 +479,19 @@ def upsert_issue(result: DriftResult, dry_run: bool) -> None: print(f"Created issue for {result.entry.id}#{result.source_index}: {out.strip()}") return + # Issues opened before a label was added to ISSUE_LABELS are backfilled here. + missing = _missing_labels(existing) + if missing: + rc, _, err = _gh([ + "issue", "edit", str(existing["number"]), + "--repo", ISSUE_REPO, + *[arg for label in missing for arg in ("--add-label", label)], + ]) + if rc != 0: + print(f"Failed to add labels to issue #{existing['number']}: {err}", file=sys.stderr) + else: + print(f"Added labels to issue #{existing['number']}: {', '.join(missing)}") + if (existing.get("body") or "").strip() == body.strip(): print(f"Issue #{existing['number']} already up to date for {result.entry.id}#{result.source_index}.") return diff --git a/.github/workflows/check-vendored-files.yml b/.github/workflows/check-vendored-files.yml index 7241aafd2088..cb60737f3c0f 100644 --- a/.github/workflows/check-vendored-files.yml +++ b/.github/workflows/check-vendored-files.yml @@ -4,7 +4,8 @@ name: Check vendored source files # sources, as listed in eng/vendored-files.json. See eng/vendored-files.md. # # - Scheduled runs and manual dispatch perform full drift detection and open -# (or update) tracking issues labelled `area-vendored-sync`. +# (or update) tracking issues labelled `area-vendored-sync` and +# `Area-dotnet test (MTP)`. # - Pull-request runs that touch the manifest, script, or workflow validate the # manifest structure only (no network, no issue mutation). diff --git a/eng/vendored-files.md b/eng/vendored-files.md index 5d05c3867564..9364d6ef89be 100644 --- a/eng/vendored-files.md +++ b/eng/vendored-files.md @@ -80,7 +80,7 @@ For every `(entry, source)` pair the workflow: 3. Otherwise fetches the baseline content (via the blobs API, robust to force-pushes) and the current content (via `raw.githubusercontent.com`), computes a unified diff, and opens/updates a tracking issue labelled - `area-vendored-sync` containing: + `area-vendored-sync` and `Area-dotnet test (MTP)` containing: - links to the upstream file history, baseline blob, current blob, and the whole-repo compare URL, - the upstream-only diff (truncated at 300 lines), From 3900b3cb67d4c33b1b372bd2812c19c118037e28 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Wed, 29 Jul 2026 16:25:06 +0200 Subject: [PATCH 2/4] Resolve vendored-sync area labels from the manifest The drift detector is upstream-agnostic (a source can point at any repo), so hard-coding the MTP area label would misroute a future entry vendored for a different area. Move the area labels into eng/vendored-files.json instead. - Add manifest-level 'default_area_labels' plus an optional per-entry 'area_labels' override; validate both. - Resolve labels per entry when creating issues, backfilling labels on existing issues, and ensuring labels exist. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 577f0b32-950f-4628-bd19-76ff5dbcabeb --- .github/scripts/check_vendored_files.py | 60 ++++++++++++---------- .github/workflows/check-vendored-files.yml | 4 +- eng/vendored-files.json | 2 + eng/vendored-files.md | 18 ++++++- 4 files changed, 54 insertions(+), 30 deletions(-) diff --git a/.github/scripts/check_vendored_files.py b/.github/scripts/check_vendored_files.py index c0cb7fbb95b6..1495c70a25d2 100644 --- a/.github/scripts/check_vendored_files.py +++ b/.github/scripts/check_vendored_files.py @@ -13,8 +13,9 @@ The check mode is idempotent: existing issues are matched by label `area-vendored-sync` plus a hidden HTML marker in the body of the form -``. Issues are additionally -labelled `Area-dotnet test (MTP)` for area triage. +``. Issues additionally carry +the area labels resolved for the entry (manifest `default_area_labels`, or the +entry's own `area_labels` override) so they land in the right triage queue. See eng/vendored-files.md for the manifest schema and reconciliation workflow. """ @@ -39,10 +40,6 @@ REPO_ROOT = Path(__file__).resolve().parents[2] MANIFEST_PATH = REPO_ROOT / "eng" / "vendored-files.json" ISSUE_LABEL = "area-vendored-sync" -# Every entry in the manifest is 'dotnet test' <-> Microsoft.Testing.Platform shared -# source, so drift issues are also routed to the MTP area label for triage. -AREA_LABEL = "Area-dotnet test (MTP)" -ISSUE_LABELS = [ISSUE_LABEL, AREA_LABEL] ISSUE_REPO = os.environ.get("VENDORED_SYNC_REPO", "dotnet/sdk") MAX_DIFF_LINES = 300 SHA_RE = re.compile(r"^[0-9a-f]{40}$") @@ -77,21 +74,29 @@ class Entry: local_path: str notes: str sources: list[Source] + area_labels: list[str] @classmethod - def from_dict(cls, d: dict[str, Any]) -> "Entry": + def from_dict(cls, d: dict[str, Any], default_area_labels: list[str]) -> "Entry": return cls( id=d["id"], local_path=d["local_path"], notes=d.get("notes", ""), sources=[Source.from_dict(s) for s in d["sources"]], + area_labels=list(d.get("area_labels", default_area_labels)), ) + @property + def issue_labels(self) -> list[str]: + """Labels applied to this entry's drift issues, sync label first.""" + return [ISSUE_LABEL, *self.area_labels] + def load_manifest() -> list[Entry]: with MANIFEST_PATH.open("r", encoding="utf-8") as f: data = json.load(f) - return [Entry.from_dict(e) for e in data["entries"]] + default_area_labels = data.get("default_area_labels", []) + return [Entry.from_dict(e, default_area_labels) for e in data["entries"]] # ---------- validation ---------- @@ -114,6 +119,11 @@ def validate(entries: list[Entry]) -> int: if not entry.sources: errors.append(f"{entry.id}: must declare at least one source.") + if not all(isinstance(label, str) and label.strip() for label in entry.area_labels): + errors.append(f"{entry.id}: area_labels must contain only non-empty strings.") + if ISSUE_LABEL in entry.area_labels: + errors.append(f"{entry.id}: area_labels must not repeat '{ISSUE_LABEL}'.") + for index, source in enumerate(entry.sources): tag = f"{entry.id}#sources[{index}]" if not re.match(r"^[\w.-]+/[\w.-]+$", source.repo): @@ -409,12 +419,13 @@ def find_existing_issue(marker: str) -> dict[str, Any] | None: return None -def ensure_label() -> None: +def ensure_label(entries: list[Entry]) -> None: """Ensure the labels applied to drift issues exist. The sync label is owned by this workflow, so it is force-created (idempotent - and self-healing). The area label is owned by the repo's regular triage - taxonomy, so it is only created when missing and never overwritten. + and self-healing). Area labels come from the manifest and are owned by the + repo's regular triage taxonomy, so they are only created when missing and + never overwritten. """ _gh([ "label", "create", ISSUE_LABEL, @@ -423,12 +434,8 @@ def ensure_label() -> None: "--color", "fbca04", "--force", ]) - _gh([ - "label", "create", AREA_LABEL, - "--repo", ISSUE_REPO, - "--description", "Issues related to 'dotnet test' with Microsoft.Testing.Platform", - "--color", "d4c5f9", - ]) + for label in sorted({label for entry in entries for label in entry.area_labels}): + _gh(["label", "create", label, "--repo", ISSUE_REPO]) _STATUS_TITLES = { @@ -438,16 +445,16 @@ def ensure_label() -> None: } -def _label_flags() -> list[str]: +def _label_flags(labels: list[str]) -> list[str]: flags: list[str] = [] - for label in ISSUE_LABELS: + for label in labels: flags += ["--label", label] return flags -def _missing_labels(issue: dict[str, Any]) -> list[str]: +def _missing_labels(issue: dict[str, Any], labels: list[str]) -> list[str]: present = {(item.get("name") or "") for item in (issue.get("labels") or [])} - return [label for label in ISSUE_LABELS if label not in present] + return [label for label in labels if label not in present] def upsert_issue(result: DriftResult, dry_run: bool) -> None: @@ -455,11 +462,12 @@ def upsert_issue(result: DriftResult, dry_run: bool) -> None: body = _render_issue_body(result) status_text = _STATUS_TITLES.get(result.status, result.status) title = f"[vendored-sync] {result.entry.id}: {status_text} (#{result.source_index})" + labels = result.entry.issue_labels if dry_run: print(f"\n--- would create/update issue ({result.entry.id}#{result.source_index}) ---") print(f"title: {title}") - print(f"labels: {', '.join(ISSUE_LABELS)}") + print(f"labels: {', '.join(labels)}") print(body[:2000]) print("---") return @@ -470,7 +478,7 @@ def upsert_issue(result: DriftResult, dry_run: bool) -> None: "issue", "create", "--repo", ISSUE_REPO, "--title", title, - *_label_flags(), + *_label_flags(labels), "--body-file", "-", ], input_data=body) if rc != 0: @@ -479,8 +487,8 @@ def upsert_issue(result: DriftResult, dry_run: bool) -> None: print(f"Created issue for {result.entry.id}#{result.source_index}: {out.strip()}") return - # Issues opened before a label was added to ISSUE_LABELS are backfilled here. - missing = _missing_labels(existing) + # Issues opened before the entry's label set changed are backfilled here. + missing = _missing_labels(existing, labels) if missing: rc, _, err = _gh([ "issue", "edit", str(existing["number"]), @@ -523,7 +531,7 @@ def cmd_check(args: argparse.Namespace) -> int: return 1 if not args.dry_run: - ensure_label() + ensure_label(entries) drift_count = 0 error_count = 0 diff --git a/.github/workflows/check-vendored-files.yml b/.github/workflows/check-vendored-files.yml index cb60737f3c0f..4fe3eefc53d8 100644 --- a/.github/workflows/check-vendored-files.yml +++ b/.github/workflows/check-vendored-files.yml @@ -4,8 +4,8 @@ name: Check vendored source files # sources, as listed in eng/vendored-files.json. See eng/vendored-files.md. # # - Scheduled runs and manual dispatch perform full drift detection and open -# (or update) tracking issues labelled `area-vendored-sync` and -# `Area-dotnet test (MTP)`. +# (or update) tracking issues labelled `area-vendored-sync` plus the area +# labels declared for the entry in eng/vendored-files.json. # - Pull-request runs that touch the manifest, script, or workflow validate the # manifest structure only (no network, no issue mutation). diff --git a/eng/vendored-files.json b/eng/vendored-files.json index 3ebda1fb5a49..3466bb457dfa 100644 --- a/eng/vendored-files.json +++ b/eng/vendored-files.json @@ -1,5 +1,7 @@ { "$comment": "Manifest of source files copied (vendored) from other repositories into dotnet/sdk. See eng/vendored-files.md. These files are the 'dotnet test' <-> Microsoft.Testing.Platform shared source (wire contract + terminal reporter); the source of truth is microsoft/testfx, which enumerates the same set via DotnetTestProtocolContract.props and TerminalReporterContract.props.", + "$comment_labels": "Area labels applied to drift issues in addition to 'area-vendored-sync'. Every entry below is 'dotnet test'/MTP source, so the default covers them all. A future entry vendored from a different upstream for a different area must set its own 'area_labels' so it is not misrouted to MTP triage.", + "default_area_labels": ["Area-dotnet test (MTP)"], "entries": [ { "id": "dotnet-test-wire-contract-fieldids", diff --git a/eng/vendored-files.md b/eng/vendored-files.md index 9364d6ef89be..f630846f8aa6 100644 --- a/eng/vendored-files.md +++ b/eng/vendored-files.md @@ -45,11 +45,17 @@ differ". ```jsonc { + // Area labels applied to every entry's drift issues, in addition to the + // `area-vendored-sync` label. Today all entries are `dotnet test`/MTP source. + "default_area_labels": ["Area-dotnet test (MTP)"], "entries": [ { "id": "stable-kebab-case-id", "local_path": "src/path/to/Local.cs", "notes": "free-form description of local adaptations", + // Optional. Overrides `default_area_labels` for this entry. Set it when + // the entry is vendored for a different area than the manifest default. + "area_labels": ["Area-SomethingElse"], "sources": [ { "repo": "owner/repo", @@ -65,6 +71,11 @@ differ". } ``` +The drift-detection mechanism itself is upstream-agnostic — a source may point at +any repo — so area routing is manifest data rather than something hard-coded in +the script. If you vendor a file for a different area, set `area_labels` on that +entry so its issues are not misrouted to the default area's triage queue. + A single local file may declare multiple upstream sources. For example the terminal reporter is one file in this repo (`src/Cli/dotnet/Commands/Test/MTP/Terminal/TerminalTestReporter.cs`) but many @@ -80,7 +91,8 @@ For every `(entry, source)` pair the workflow: 3. Otherwise fetches the baseline content (via the blobs API, robust to force-pushes) and the current content (via `raw.githubusercontent.com`), computes a unified diff, and opens/updates a tracking issue labelled - `area-vendored-sync` and `Area-dotnet test (MTP)` containing: + `area-vendored-sync` plus the entry's area labels (see `default_area_labels` / + `area_labels` above) containing: - links to the upstream file history, baseline blob, current blob, and the whole-repo compare URL, - the upstream-only diff (truncated at 300 lines), @@ -105,7 +117,9 @@ reconciliation PR is merged. (`gh api repos/{repo}/commits/{ref} --jq .sha`), - `baseline_blob_sha`: the upstream file's blob SHA at that ref (`gh api "repos/{repo}/contents/{path}?ref={ref}" --jq .sha`). -3. Run `python .github/scripts/check_vendored_files.py validate` locally to +3. If the file does not belong to the area in `default_area_labels`, set + `area_labels` on the entry so its drift issues reach the right triage queue. +4. Run `python .github/scripts/check_vendored_files.py validate` locally to confirm the structure is correct. ## Reconciling drift From 6a86b44fb3562718b66a26dc80b576fab46cab71 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Wed, 5 Aug 2026 08:42:53 +0200 Subject: [PATCH 3/4] Harden vendored-sync label validation Reject non-list area label declarations during manifest validation, and make label setup enumerate existing labels before creating only the missing ones. Surface gh failures and stop the check instead of continuing with incomplete label setup. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 577f0b32-950f-4628-bd19-76ff5dbcabeb --- .github/scripts/check_vendored_files.py | 55 ++++++++++++++++++++----- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/.github/scripts/check_vendored_files.py b/.github/scripts/check_vendored_files.py index 1495c70a25d2..ee195c20eaa5 100644 --- a/.github/scripts/check_vendored_files.py +++ b/.github/scripts/check_vendored_files.py @@ -74,21 +74,24 @@ class Entry: local_path: str notes: str sources: list[Source] - area_labels: list[str] + area_labels: object @classmethod - def from_dict(cls, d: dict[str, Any], default_area_labels: list[str]) -> "Entry": + def from_dict(cls, d: dict[str, Any], default_area_labels: object) -> "Entry": + area_labels = d.get("area_labels", default_area_labels) return cls( id=d["id"], local_path=d["local_path"], notes=d.get("notes", ""), sources=[Source.from_dict(s) for s in d["sources"]], - area_labels=list(d.get("area_labels", default_area_labels)), + area_labels=area_labels.copy() if isinstance(area_labels, list) else area_labels, ) @property def issue_labels(self) -> list[str]: """Labels applied to this entry's drift issues, sync label first.""" + if not isinstance(self.area_labels, list): + raise ValueError(f"{self.id}: area_labels must be a list.") return [ISSUE_LABEL, *self.area_labels] @@ -119,9 +122,11 @@ def validate(entries: list[Entry]) -> int: if not entry.sources: errors.append(f"{entry.id}: must declare at least one source.") - if not all(isinstance(label, str) and label.strip() for label in entry.area_labels): + if not isinstance(entry.area_labels, list): + errors.append(f"{entry.id}: area_labels must be a list.") + elif not all(isinstance(label, str) and label.strip() for label in entry.area_labels): errors.append(f"{entry.id}: area_labels must contain only non-empty strings.") - if ISSUE_LABEL in entry.area_labels: + elif ISSUE_LABEL in entry.area_labels: errors.append(f"{entry.id}: area_labels must not repeat '{ISSUE_LABEL}'.") for index, source in enumerate(entry.sources): @@ -419,7 +424,7 @@ def find_existing_issue(marker: str) -> dict[str, Any] | None: return None -def ensure_label(entries: list[Entry]) -> None: +def ensure_labels(entries: list[Entry]) -> bool: """Ensure the labels applied to drift issues exist. The sync label is owned by this workflow, so it is force-created (idempotent @@ -427,15 +432,43 @@ def ensure_label(entries: list[Entry]) -> None: repo's regular triage taxonomy, so they are only created when missing and never overwritten. """ - _gh([ + rc, _, err = _gh([ "label", "create", ISSUE_LABEL, "--repo", ISSUE_REPO, "--description", "Drift detected between a vendored source file and its upstream copy", "--color", "fbca04", "--force", ]) - for label in sorted({label for entry in entries for label in entry.area_labels}): - _gh(["label", "create", label, "--repo", ISSUE_REPO]) + if rc != 0: + print(f"Failed to ensure label '{ISSUE_LABEL}': {err}", file=sys.stderr) + return False + + rc, out, err = _gh([ + "label", "list", + "--repo", ISSUE_REPO, + "--limit", "1000", + "--json", "name", + ]) + if rc != 0: + print(f"Failed to list labels: {err}", file=sys.stderr) + return False + try: + existing_labels = {item["name"] for item in json.loads(out)} + except (json.JSONDecodeError, KeyError, TypeError): + print("Failed to parse labels returned by gh.", file=sys.stderr) + return False + + area_labels = { + label + for entry in entries + for label in entry.issue_labels[1:] + } + for label in sorted(area_labels - existing_labels): + rc, _, err = _gh(["label", "create", label, "--repo", ISSUE_REPO]) + if rc != 0: + print(f"Failed to create area label '{label}': {err}", file=sys.stderr) + return False + return True _STATUS_TITLES = { @@ -530,8 +563,8 @@ def cmd_check(args: argparse.Namespace) -> int: if validate(entries) != 0: return 1 - if not args.dry_run: - ensure_label(entries) + if not args.dry_run and not ensure_labels(entries): + return 1 drift_count = 0 error_count = 0 From 07b898651b9f8b6c49aa0b6ce319694e7be1f6be Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Thu, 6 Aug 2026 10:23:07 +0200 Subject: [PATCH 4/4] Reconcile vendored sync area labels Require manifest area labels to exist in the repository taxonomy and replace stale Area-* labels on existing sync issues while preserving unrelated labels. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 131255e3-e14c-4fc3-91f6-00ff64afb2a6 --- .github/scripts/check_vendored_files.py | 79 +++++++++++++++---------- eng/vendored-files.md | 9 ++- 2 files changed, 56 insertions(+), 32 deletions(-) diff --git a/.github/scripts/check_vendored_files.py b/.github/scripts/check_vendored_files.py index ee195c20eaa5..daddbddc2926 100644 --- a/.github/scripts/check_vendored_files.py +++ b/.github/scripts/check_vendored_files.py @@ -40,6 +40,7 @@ REPO_ROOT = Path(__file__).resolve().parents[2] MANIFEST_PATH = REPO_ROOT / "eng" / "vendored-files.json" ISSUE_LABEL = "area-vendored-sync" +AREA_LABEL_PREFIX = "Area-" ISSUE_REPO = os.environ.get("VENDORED_SYNC_REPO", "dotnet/sdk") MAX_DIFF_LINES = 300 SHA_RE = re.compile(r"^[0-9a-f]{40}$") @@ -424,24 +425,24 @@ def find_existing_issue(marker: str) -> dict[str, Any] | None: return None -def ensure_labels(entries: list[Entry]) -> bool: +def ensure_labels(entries: list[Entry], create_sync_label: bool = True) -> bool: """Ensure the labels applied to drift issues exist. - The sync label is owned by this workflow, so it is force-created (idempotent - and self-healing). Area labels come from the manifest and are owned by the - repo's regular triage taxonomy, so they are only created when missing and - never overwritten. + The sync label is owned by this workflow, so it is force-created when issue + mutation is enabled. Area labels come from the manifest and are owned by the + repo's regular triage taxonomy, so they must already exist. """ - rc, _, err = _gh([ - "label", "create", ISSUE_LABEL, - "--repo", ISSUE_REPO, - "--description", "Drift detected between a vendored source file and its upstream copy", - "--color", "fbca04", - "--force", - ]) - if rc != 0: - print(f"Failed to ensure label '{ISSUE_LABEL}': {err}", file=sys.stderr) - return False + if create_sync_label: + rc, _, err = _gh([ + "label", "create", ISSUE_LABEL, + "--repo", ISSUE_REPO, + "--description", "Drift detected between a vendored source file and its upstream copy", + "--color", "fbca04", + "--force", + ]) + if rc != 0: + print(f"Failed to ensure label '{ISSUE_LABEL}': {err}", file=sys.stderr) + return False rc, out, err = _gh([ "label", "list", @@ -463,11 +464,13 @@ def ensure_labels(entries: list[Entry]) -> bool: for entry in entries for label in entry.issue_labels[1:] } - for label in sorted(area_labels - existing_labels): - rc, _, err = _gh(["label", "create", label, "--repo", ISSUE_REPO]) - if rc != 0: - print(f"Failed to create area label '{label}': {err}", file=sys.stderr) - return False + missing_labels = sorted(area_labels - existing_labels) + if missing_labels: + print( + f"Missing area labels in {ISSUE_REPO}: {', '.join(missing_labels)}", + file=sys.stderr, + ) + return False return True @@ -485,9 +488,16 @@ def _label_flags(labels: list[str]) -> list[str]: return flags -def _missing_labels(issue: dict[str, Any], labels: list[str]) -> list[str]: +def _label_changes(issue: dict[str, Any], labels: list[str]) -> tuple[list[str], list[str]]: present = {(item.get("name") or "") for item in (issue.get("labels") or [])} - return [label for label in labels if label not in present] + desired = set(labels) + add = sorted(desired - present) + remove = sorted( + label + for label in present - desired + if label.startswith(AREA_LABEL_PREFIX) + ) + return add, remove def upsert_issue(result: DriftResult, dry_run: bool) -> None: @@ -520,21 +530,30 @@ def upsert_issue(result: DriftResult, dry_run: bool) -> None: print(f"Created issue for {result.entry.id}#{result.source_index}: {out.strip()}") return - # Issues opened before the entry's label set changed are backfilled here. - missing = _missing_labels(existing, labels) - if missing: + # Keep the workflow-managed area routing in sync while preserving other labels. + add_labels, remove_labels = _label_changes(existing, labels) + labels_up_to_date = not add_labels and not remove_labels + if not labels_up_to_date: rc, _, err = _gh([ "issue", "edit", str(existing["number"]), "--repo", ISSUE_REPO, - *[arg for label in missing for arg in ("--add-label", label)], + *[arg for label in add_labels for arg in ("--add-label", label)], + *[arg for label in remove_labels for arg in ("--remove-label", label)], ]) if rc != 0: - print(f"Failed to add labels to issue #{existing['number']}: {err}", file=sys.stderr) + print(f"Failed to reconcile labels on issue #{existing['number']}: {err}", file=sys.stderr) else: - print(f"Added labels to issue #{existing['number']}: {', '.join(missing)}") + changes: list[str] = [] + if add_labels: + changes.append(f"added {', '.join(add_labels)}") + if remove_labels: + changes.append(f"removed {', '.join(remove_labels)}") + print(f"Reconciled labels on issue #{existing['number']}: {'; '.join(changes)}") + labels_up_to_date = True if (existing.get("body") or "").strip() == body.strip(): - print(f"Issue #{existing['number']} already up to date for {result.entry.id}#{result.source_index}.") + if labels_up_to_date: + print(f"Issue #{existing['number']} already up to date for {result.entry.id}#{result.source_index}.") return rc, _, err = _gh([ @@ -563,7 +582,7 @@ def cmd_check(args: argparse.Namespace) -> int: if validate(entries) != 0: return 1 - if not args.dry_run and not ensure_labels(entries): + if not ensure_labels(entries, create_sync_label=not args.dry_run): return 1 drift_count = 0 diff --git a/eng/vendored-files.md b/eng/vendored-files.md index f630846f8aa6..5aa2bda981f0 100644 --- a/eng/vendored-files.md +++ b/eng/vendored-files.md @@ -74,7 +74,11 @@ differ". The drift-detection mechanism itself is upstream-agnostic — a source may point at any repo — so area routing is manifest data rather than something hard-coded in the script. If you vendor a file for a different area, set `area_labels` on that -entry so its issues are not misrouted to the default area's triage queue. +entry so its issues are not misrouted to the default area's triage queue. Every +declared area label must already exist in the repository's triage taxonomy; the +workflow only creates its own `area-vendored-sync` label. Existing sync issues +are reconciled to the entry's current area-label set while retaining unrelated +labels. A single local file may declare multiple upstream sources. For example the terminal reporter is one file in this repo @@ -118,7 +122,8 @@ reconciliation PR is merged. - `baseline_blob_sha`: the upstream file's blob SHA at that ref (`gh api "repos/{repo}/contents/{path}?ref={ref}" --jq .sha`). 3. If the file does not belong to the area in `default_area_labels`, set - `area_labels` on the entry so its drift issues reach the right triage queue. + `area_labels` on the entry to existing repository triage labels so its drift + issues reach the right triage queue. 4. Run `python .github/scripts/check_vendored_files.py validate` locally to confirm the structure is correct.