diff --git a/.github/workflows/api-review-pending.yml b/.github/workflows/api-review-pending.yml new file mode 100644 index 000000000000..7b6a6d4ed652 --- /dev/null +++ b/.github/workflows/api-review-pending.yml @@ -0,0 +1,65 @@ +name: API Review Pending Work Item Sync + +on: + pull_request_target: + # Merged pull requests are delivered as the closed action with pull_request.merged == true. + types: [opened, closed, reopened] + workflow_call: + inputs: + event-path: + description: Path to a GitHub pull_request event payload. + required: false + type: string + metadata-marker: + description: Hidden PR body metadata marker. + required: false + type: string + default: api-md-review-sync + pending-reviews-field: + description: ADO work item field containing pending API review PR URLs. + required: false + type: string + default: Custom.PendingAPIReviews + secrets: + AZURE_DEVOPS_EXT_PAT: + required: true + +permissions: + contents: read + pull-requests: read + +jobs: + sync-pending-review: + name: Sync Pending API Review Work Item Field + if: >- + ${{ + github.event_name == 'workflow_call' || + contains(github.event.pull_request.body || '', 'api-md-review-sync') || + startsWith(github.event.pull_request.title || '', '[API Review]') || + startsWith(github.event.pull_request.head.ref || '', 'apireview/review') + }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + sparse-checkout: | + eng/common + + - name: Install azsdk-cli + shell: pwsh + run: | + $installDir = Join-Path $env:RUNNER_TEMP 'azsdk-cli' + ./eng/common/mcp/azure-sdk-mcp.ps1 -InstallDirectory $installDir + $azsdkCli = Join-Path $installDir "azsdk$(if ($IsWindows) { '.exe' } else { '' })" + "AZSDK_CLI=$azsdkCli" | Out-File -FilePath $env:GITHUB_ENV -Append + Write-Host "Installed azsdk-cli at $azsdkCli" + + - name: Sync pending API review URL + shell: bash + env: + AZURE_DEVOPS_EXT_PAT: ${{ secrets.AZURE_DEVOPS_EXT_PAT }} + API_REVIEW_EVENT_PATH: ${{ inputs['event-path'] }} + API_REVIEW_METADATA_MARKER: ${{ inputs['metadata-marker'] || 'api-md-review-sync' }} + PENDING_API_REVIEWS_FIELD: ${{ inputs['pending-reviews-field'] || 'Custom.PendingAPIReviews' }} + run: node eng/common/scripts/sync-api-review-pending.js diff --git a/eng/common/scripts/sync-api-review-pending.js b/eng/common/scripts/sync-api-review-pending.js new file mode 100644 index 000000000000..eb9225bc8f1b --- /dev/null +++ b/eng/common/scripts/sync-api-review-pending.js @@ -0,0 +1,203 @@ +#!/usr/bin/env node + +const { execFileSync } = require("child_process"); +const fs = require("fs"); + +const DEFAULT_METADATA_MARKER = "api-md-review-sync"; +const DEFAULT_PENDING_REVIEWS_FIELD = "Custom.PendingAPIReviews"; +const METADATA_WARNING = "DO NOT MODIFY THESE CONTENTS!"; + +function log(message) { + console.log(message); +} + +function warn(message) { + console.warn(message); +} + +function runAzsdkCli(args) { + const cli = process.env.AZSDK_CLI || "azsdk-cli"; + log(`$ ${[cli, ...args].join(" ")}`); + return execFileSync(cli, args, { + encoding: "utf-8", + shell: process.platform === "win32" && /\.(cmd|bat)$/i.test(cli), + stdio: ["ignore", "pipe", "pipe"], + }); +} + +function parseJson(text, description) { + try { + return JSON.parse(text.replace(/^\uFEFF/, "")); + } catch (error) { + throw new Error(`Failed to parse ${description}: ${error.message}`); + } +} + +function parseEvent() { + const eventPath = process.env.API_REVIEW_EVENT_PATH || process.env.GITHUB_EVENT_PATH; + if (!eventPath) { + throw new Error("GITHUB_EVENT_PATH is required when API_REVIEW_EVENT_PATH is not set."); + } + + return parseJson(fs.readFileSync(eventPath, "utf-8"), eventPath); +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function parseSyncMetadata(body, marker = DEFAULT_METADATA_MARKER) { + const pattern = new RegExp( + ``, + "m" + ); + const match = pattern.exec(body || ""); + if (!match) { + return null; + } + + const jsonText = match[1] + .split(/\r?\n/) + .filter((line) => line.trim() !== METADATA_WARNING) + .join("\n") + .trim(); + + const metadata = parseJson(jsonText, `${marker} metadata block`); + if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) { + throw new Error(`${marker} metadata block must contain a JSON object.`); + } + return metadata; +} + +function normalizeUrlList(value) { + if (!value) { + return []; + } + + const seen = new Set(); + const urls = []; + for (const line of String(value).split(/\r?\n/)) { + const url = line.trim(); + if (!url || seen.has(url)) { + continue; + } + seen.add(url); + urls.push(url); + } + return urls; +} + +function getPackageWorkItem(metadata) { + const rawWorkItemId = metadata.packageWorkItemId; + if (rawWorkItemId === undefined || rawWorkItemId === null || rawWorkItemId === "" || rawWorkItemId === "ERROR") { + return null; + } + + const workItemId = Number(rawWorkItemId); + if (!Number.isInteger(workItemId)) { + throw new Error(`Invalid packageWorkItemId in API review metadata: ${rawWorkItemId}`); + } + + return parseJson( + runAzsdkCli(["package", "get-work-item", "--work-item-id", String(workItemId), "-o", "json"]), + `package work item ${workItemId}` + ); +} + +function workItemId(workItem) { + const id = Number(workItem?.id); + if (!Number.isInteger(id)) { + throw new Error("Package work item response did not contain an integer id."); + } + return id; +} + +function pendingReviewUrls(workItem, fieldName) { + return normalizeUrlList(workItem?.fields?.[fieldName]); +} + +function updatePendingReviews(workItem, fieldName, urls) { + runAzsdkCli([ + "package", + "update-work-item", + "--work-item-id", + String(workItemId(workItem)), + "--field", + `${fieldName}=${urls.join("\n")}`, + "--multiline-fields-format", + `${fieldName}=markdown`, + ]); +} + +function updatedUrlsForAction(action, urls, prUrl) { + if (action === "opened" || action === "reopened") { + return urls.includes(prUrl) ? urls : [...urls, prUrl]; + } + + if (action === "closed") { + return urls.filter((url) => url !== prUrl); + } + + throw new Error(`Unsupported pull request action: ${action}`); +} + +function isReviewPullRequest(pullRequest, marker = DEFAULT_METADATA_MARKER) { + return Boolean( + parseSyncMetadata(pullRequest.body || "", marker) || + pullRequest.title?.startsWith("[API Review]") || + pullRequest.head?.ref?.startsWith("apireview/review") + ); +} + +function main() { + const event = parseEvent(); + const action = process.env.API_REVIEW_PR_ACTION || event.action; + const pullRequest = event.pull_request; + const fieldName = process.env.PENDING_API_REVIEWS_FIELD || DEFAULT_PENDING_REVIEWS_FIELD; + const marker = process.env.API_REVIEW_METADATA_MARKER || DEFAULT_METADATA_MARKER; + + if (!pullRequest) { + warn("Event does not contain a pull_request payload; skipping."); + return; + } + + if (action !== "opened" && action !== "closed" && action !== "reopened") { + warn(`Pull request action ${action} does not affect pending API reviews; skipping.`); + return; + } + + if (!isReviewPullRequest(pullRequest, marker)) { + warn("Pull request does not look like an API review PR; skipping."); + return; + } + + const metadata = parseSyncMetadata(pullRequest.body || "", marker); + if (!metadata) { + warn(`Pull request does not contain ${marker} metadata; skipping package work item update.`); + return; + } + + const prUrl = process.env.API_REVIEW_PR_URL || pullRequest.html_url; + if (!prUrl) { + throw new Error("Pull request URL is required."); + } + + const workItem = getPackageWorkItem(metadata); + if (!workItem) { + warn("API review metadata does not contain a packageWorkItemId; skipping package work item update."); + return; + } + + const existingUrls = pendingReviewUrls(workItem, fieldName); + const nextUrls = updatedUrlsForAction(action, existingUrls, prUrl); + + if (nextUrls.join("\n") === existingUrls.join("\n")) { + log(`Package work item ${workItemId(workItem)} already has desired ${fieldName} value.`); + return; + } + + updatePendingReviews(workItem, fieldName, nextUrls); + log(`Updated package work item ${workItemId(workItem)} ${fieldName} for PR ${prUrl}.`); +} + +main(); diff --git a/scripts/api_md_workflow/README.md b/scripts/api_md_workflow/README.md index 4d96401c29a5..011068fdfe4a 100644 --- a/scripts/api_md_workflow/README.md +++ b/scripts/api_md_workflow/README.md @@ -1,21 +1,232 @@ # API Review PR Helper -This folder contains the standalone Python helper used to create API review PRs from generated `api.md` files. +This folder contains the standalone helper used to create API review pull requests from generated `api.md` files. ## Purpose -`create_api_review_pr.py` compares a baseline package release tag with a target API surface, creates or reuses dedicated API review branches, and opens a draft PR that shows the `api.md` diff. +`create_api_review_pr.py` automates API review PR creation for a package by: + +1. Generating baseline and target API markdown snapshots. +2. Creating or reusing dedicated baseline/review branches. +3. Creating or reusing a draft GitHub PR with the `api.md` diff. +4. Assigning the PR to API review architects resolved from `.github/ARCHITECTS`. +5. Updating PR body sync metadata so future automation can identify branch relationships. +6. Letting the API review pending workflow update the package ADO work item from PR events. The API consistency workflow helpers live under `.github/workflows/src/api-md-consistency`. -## Usage +## Inputs and Output + +Required inputs: + +- `--package-name`: package folder name, such as `azure-ai-projects`. +- `--base`: baseline package tag, required to match `_` and exist locally/remotely. + +Optional inputs: + +- `--target`: one of: + - package tag (for static tag-to-tag reviews), + - branch name on `origin`, + - `owner:branch` fork branch reference. +- `--python` / `--runtime`: runtime executable used for `azpysdk apistub` generation. + +Primary output: + +- A draft PR URL representing baseline vs review branch `api.md` diff. + +Secondary output: + +- PR body sync metadata includes the package work item ID when available, so `.github/workflows/api-review-pending.yml` can synchronize `Custom.PendingAPIReviews` on PR open, close, and reopen events. +- Matching API review architects are requested as GitHub reviewers when a PR number is available. + +## High-Level Flow + +`create_api_review_pr.py` executes this logical sequence: + +1. Validate environment and arguments. +2. Resolve package directory. +3. Capture baseline `api.md` + `api.metadata.yml` from `--base` tag. +4. Capture target `api.md` + `api.metadata.yml` from resolved target ref. +5. Exit early if baseline and target `api.md` are identical. +6. Resolve branch reuse or branch creation for baseline and review branches. +7. Fetch the package work item ID for working-branch sync metadata. +8. Create or reuse PR between baseline and review branches. +9. Ensure PR sync metadata block in PR body. +10. Request API review architects as PR reviewers. +11. Restore original local branch. + +## Detailed Decision Paths + +### 1) Target Resolution + +`--target` resolution order and behavior: + +- If target is omitted: use `origin/main`. +- If target looks like package tag and exists as tag: use tag. +- Else if target is plain branch and exists on `origin`: use `origin/`. +- Else if target is `owner:branch` and exists on fork: use `FETCH_HEAD`. +- Else: fail. + +Implication for PR body labeling: + +- Tag target: labeled as `Target tag` and called out as static tag-to-tag review. +- Branch target: labeled as `Working branch` or `Working PR` if a matching open PR exists. + +### 2) API Snapshot Capture + +For both baseline and target refs, the script overlays package files from that ref, then runs `azpysdk apistub --md --extract-metadata`. + +Captured artifacts: + +- `api.md` bytes (required). +- `api.metadata.yml` bytes (optional but expected for metadata hash checks). +- parsed package version from `_version.py` or `version.py`. + +After each capture, the script resets package files in the working tree to avoid local drift. + +### 3) API Difference Gate + +Diff condition is intentionally narrow: + +- If `base.api_md == target.api_md`: no branches or PR are created. +- Metadata-only differences do not trigger PR creation. + +### 4) Branch Reuse vs Branch Creation + +Branch naming convention: + +- baseline: `apireview/base__` +- review: `apireview/review__` + +Reuse logic: + +- Enumerate existing remote branches with same prefix. +- Read branch state (`api.md`, `api.metadata.yml`, `apiMdSha256`). +- Reuse branch only when branch state matches desired state. +- For review branch, required ancestor must include selected baseline branch. + +Creation logic: + +- Baseline branch starts from `origin/main` and commits baseline `api.md`/metadata. +- Review branch starts from selected baseline branch and commits target `api.md`/metadata. +- Both are pushed with `--force-with-lease`. + +### 5) PR Reuse vs PR Creation + +If both branches are reused: + +- Search for existing open PR matching baseline/review pair. +- If found, reuse PR and update body sync metadata block if stale. + +Otherwise: + +- Attempt draft PR creation via GitHub REST API. +- If creation fails, search for already-open PR as fallback. +- If fallback PR exists, reuse it. +- If no PR is found, log manual compare URL and diagnostics. -The script includes Python package discovery, version parsing, `api.md` generation, git branch orchestration, and GitHub PR creation in one file. +Whenever a PR is created or reused, the script attempts to request API review from matching architects before updating the package work item. -`create_api_review_pr.py` compares a baseline package release tag with a target API surface. The target can be a package release tag, an `origin` branch, or an `owner:branch` fork reference. When the target is a tag, the generated PR body identifies it as a target tag instead of a working branch. +### 6) API Review Architect Assignment + +The script resolves API review architects from `.github/ARCHITECTS` using the package directory path and CODEOWNERS-style matching. + +Behavior: + +- Matching `@user` owners are requested as GitHub reviewers. +- Matching `@org/team` owners are requested as GitHub team reviewers using the team slug. +- The PR author is removed from direct user reviewer requests because GitHub rejects self-review requests. +- If no architects match, or all direct reviewers resolve to the PR author, reviewer assignment is skipped with diagnostics. +- GitHub reviewer request failures are warnings; branch/PR creation and work-item sync continue. + +## PR Body Sync Metadata + +The PR body can include a hidden metadata block (`api-md-review-sync`) with: + +- repository slug, +- package name and dir, +- baseline/review branch names, +- working branch owner/name, +- optional working PR number, +- optional package work item id. + +Behavior: + +- Existing stale sync block is replaced. +- Block is omitted for target-tag flows (no working branch to track). + +## ADO Package Work Item Sync Flow + +For working-branch sync flows, the script fetches the package work item before creating or reusing the API review PR: + +1. Fetch package work item via: + +```bash +azsdk package get-work-item --package-name -o json +``` + +2. Extract the work item `id` and write it to PR body sync metadata as `packageWorkItemId`. + +The `.github/workflows/api-review-pending.yml` workflow owns `Custom.PendingAPIReviews` updates. On API review PR `opened` or `reopened`, it appends the PR URL if missing. On `closed`, it removes the PR URL. + +The workflow updates the work item with: + +```bash +azsdk package update-work-item \ + --work-item-id \ + --field "Custom.PendingAPIReviews=" \ + --multiline-fields-format "Custom.PendingAPIReviews=markdown" +``` + +Failure policy: + +- Work item fetch failures set sync metadata `packageWorkItemId` to `ERROR`. +- PR flow still succeeds; the event-driven workflow skips the package work item update when `packageWorkItemId` is unavailable. + +## Error Handling Strategy + +Hard failures: + +- dirty working tree, +- detached HEAD, +- invalid/missing base tag, +- unresolved target reference, +- missing required API artifact generation. + +Soft failures (warning and continue): + +- inability to request API review architects as PR reviewers, +- inability to update PR body sync metadata, +- inability to fetch package work item details, +- inability to update package work item pending review URLs, +- draft PR creation failure when existing PR fallback succeeds. + +## Example Usage + + +Baseline tag vs fork working branch: + +Release from main review (most common): + +```bash +python scripts/api_md_workflow/create_api_review_pr.py \ + --package-name azure-ai-projects \ + --base azure-ai-projects_2.1.0 \ + --target main +``` + +```bash +python scripts/api_md_workflow/create_api_review_pr.py \ + --package-name azure-ai-projects \ + --base azure-ai-projects_2.1.0 \ + --target someuser:feature/api-changes +``` -Example comparing two package release tags: +Tag-to-tag review (uncommon): ```bash -python scripts/api_md_workflow/create_api_review_pr.py --package-name azure-ai-projects --base azure-ai-projects_2.1.0 --target azure-ai-projects_2.2.0 +python scripts/api_md_workflow/create_api_review_pr.py \ + --package-name azure-ai-projects \ + --base azure-ai-projects_2.1.0 \ + --target azure-ai-projects_2.2.0 ``` diff --git a/scripts/api_md_workflow/create_api_review_pr.py b/scripts/api_md_workflow/create_api_review_pr.py index 057fd406a30d..4d4bf286d51e 100644 --- a/scripts/api_md_workflow/create_api_review_pr.py +++ b/scripts/api_md_workflow/create_api_review_pr.py @@ -68,6 +68,11 @@ class ArchitectReviewers: teams: list[str] +@dataclass +class PackageWorkItem: + id: int + + GitRunner = Callable[[list[str], bool], CommandResult] _git_runner: GitRunner | None = None _github_api: "GitHubApi | None" = None @@ -176,7 +181,7 @@ def normalize_pull_request(pr: dict[str, Any] | None) -> dict[str, Any] | None: pr["head"].get("repo") if isinstance(pr["head"].get("repo"), dict) else {} ) owner = repo.get("owner") if isinstance(repo.get("owner"), dict) else {} - owner_login = owner.get("login") + owner_login = owner.get("login") if isinstance(owner, dict) else None if isinstance(pr.get("author"), dict): author_login = pr["author"].get("login") elif isinstance(pr.get("user"), dict): @@ -586,33 +591,6 @@ def parse_simple_yaml(text: str) -> dict[str, str]: return result -def package_version_major_minor(package_version: str) -> str: - match = re.match(r"^(\d+)(?:\.(\d+))?", package_version.strip()) - if not match: - raise RuntimeError( - f"ERROR: could not derive major/minor version from package version '{package_version}'." - ) - major = match.group(1) - minor = match.group(2) or "0" - return f"{major}.{minor}" - - -def parse_package_work_item_id(output: str) -> int | None: - try: - parsed = json.loads(output) - if isinstance(parsed, dict): - value = parsed.get("work_item_id") or parsed.get("workItemId") - if isinstance(value, int): - return value - if isinstance(value, str) and value.isdigit(): - return int(value) - except json.JSONDecodeError: - pass - - match = re.search(r"Work Item ID:\s*(\d+)", output) - return int(match.group(1)) if match else None - - def find_azsdk_executable() -> str | None: azsdk_from_path = shutil.which("azsdk") if azsdk_from_path: @@ -644,60 +622,30 @@ def resolve_azsdk_executable() -> str: ) -def ensure_azsdk_find_work_item_available() -> None: +def ensure_azsdk_work_item_commands_available() -> None: azsdk_executable = resolve_azsdk_executable() - result = run( - [azsdk_executable, "package", "find-work-item", "--help"], - check=False, - capture=True, - ) - if result.status == 0: - return - - details = combined_command_output(result) - raise RuntimeError( - "ERROR: azsdk CLI is installed, but the 'package find-work-item' command is unavailable. " - f"Update it by running 'pwsh {AZSDK_INSTALL_SCRIPT}'." - + (f"\n{details}" if details else "") - ) - - -def package_work_item_id(package_name: str, package_version: str) -> int | str: - package_version_major_minor_value = package_version_major_minor(package_version) - result = run( - [ - resolve_azsdk_executable(), - "package", - "find-work-item", - "--package-name", - package_name, - "--package-version", - package_version_major_minor_value, - "--language", - "Python", - ], - check=False, - capture=True, - ) - output = combined_command_output(result) - - if result.status != 0: - log_warning( - f"WARNING: failed to resolve package work item ID for {package_name} {package_version_major_minor_value}. " - "PR body sync metadata will set packageWorkItemId to ERROR." - + (f"\n{output}" if output else "") + missing_commands: list[str] = [] + command_details: list[str] = [] + for command_name in ["get-work-item"]: + result = run( + [azsdk_executable, "package", command_name, "--help"], + check=False, + capture=True, ) - return "ERROR" + if result.status == 0: + continue - work_item_id = parse_package_work_item_id(result.stdout) - if work_item_id is None: - log_warning( - f"WARNING: azsdk package find-work-item completed for {package_name} {package_version_major_minor_value} " - "but did not return a work item ID. PR body sync metadata will set packageWorkItemId to NONE." - + (f"\n{output}" if output else "") + missing_commands.append(f"package {command_name}") + details = combined_command_output(result) + if details: + command_details.append(f"{command_name}:\n{details}") + + if missing_commands: + raise RuntimeError( + "ERROR: azsdk CLI is installed, but the following commands are unavailable: " + f"{', '.join(missing_commands)}. Update it by running 'pwsh {AZSDK_INSTALL_SCRIPT}'." + + (f"\n{chr(10).join(command_details)}" if command_details else "") ) - return "NONE" - return work_item_id def metadata_sha_or_none(metadata_bytes: bytes | None) -> str | None: @@ -849,6 +797,47 @@ def architects_for_package( return matching_architects +def get_package_work_item(package_name: str) -> PackageWorkItem: + result = run( + [ + "azsdk", + "package", + "get-work-item", + "--package-name", + package_name, + "-o", + "json", + ], + check=False, + capture=True, + ) + if result.status != 0: + raise RuntimeError( + "Failed to get package work item via `azsdk package get-work-item -o json`." + + (f"\n stderr: {result.stderr.strip()}" if result.stderr.strip() else "") + ) + + try: + data = json.loads(result.stdout) + except json.JSONDecodeError as error: + raise RuntimeError( + f"Failed to parse package work item JSON: {error}" + ) from error + + raw_work_item_id = data.get("id") + if raw_work_item_id is None: + raise RuntimeError("Package work item response did not contain an 'id'.") + + try: + work_item_id = int(raw_work_item_id) + except (TypeError, ValueError) as error: + raise RuntimeError( + f"Invalid package work item id: {raw_work_item_id}" + ) from error + + return PackageWorkItem(id=work_item_id) + + def branch_remote_ref(branch: str) -> str: return f"{REMOTE}/{branch}" @@ -1024,7 +1013,7 @@ def build_sync_metadata_object( base_branch: str, review_branch: str, head_selector: str, - package_work_item_id_value: int | str | None = None, + work_item_metadata_value: int | str | None = None, ) -> dict[str, Any] | None: working_branch = sync_working_branch_info(head_selector, package_name) if not working_branch: @@ -1046,8 +1035,8 @@ def build_sync_metadata_object( if working_pr and isinstance(working_pr.get("number"), int) else None ) - if package_work_item_id_value is not None: - metadata["packageWorkItemId"] = package_work_item_id_value + if work_item_metadata_value is not None: + metadata["packageWorkItemId"] = work_item_metadata_value return metadata @@ -1358,7 +1347,7 @@ def parse_args(argv: list[str]) -> argparse.Namespace: def main(argv: list[str] | None = None) -> int: args = parse_args(argv or sys.argv[1:]) if sync_working_branch_info(args.target or "main", args.package_name): - ensure_azsdk_find_work_item_available() + ensure_azsdk_work_item_commands_available() package_dir = find_package_dir(args.package_name) log_info(f"Found package at: {package_dir}") @@ -1480,16 +1469,16 @@ def main(argv: list[str] | None = None) -> int: working_selector = args.target or "main" working_reference = target_reference_info(working_selector, args.package_name) baseline_ref = baseline_reference_markdown(args.base) - package_work_item_id_value: int | str | None = None + work_item_metadata_value: int | str | None = None if sync_working_branch_info(working_selector, args.package_name): try: - package_work_item_id_value = package_work_item_id( - args.package_name, target_version - ) + package_work_item = get_package_work_item(args.package_name) + work_item_metadata_value = package_work_item.id except Exception as error: # pylint: disable=broad-except - package_work_item_id_value = "ERROR" + work_item_metadata_value = "ERROR" log_warning( - "WARNING: failed to resolve package work item ID. " + "WARNING: failed to get package work item via " + "`azsdk package get-work-item -o json`. " "PR body sync metadata will set packageWorkItemId to ERROR." + (f"\n{error}" if str(error) else "") ) @@ -1499,7 +1488,7 @@ def main(argv: list[str] | None = None) -> int: base_branch=base_branch, review_branch=review_branch, head_selector=working_selector, - package_work_item_id_value=package_work_item_id_value, + work_item_metadata_value=work_item_metadata_value, ) sync_metadata_block = build_sync_metadata_block(sync_metadata) body = build_review_pr_body( diff --git a/scripts/api_md_workflow/create_api_review_pr_test.py b/scripts/api_md_workflow/create_api_review_pr_test.py index a0e2e45a201f..02b754556207 100644 --- a/scripts/api_md_workflow/create_api_review_pr_test.py +++ b/scripts/api_md_workflow/create_api_review_pr_test.py @@ -1,7 +1,5 @@ import json -import tempfile import unittest -from pathlib import Path from unittest.mock import MagicMock, patch from scripts.api_md_workflow import create_api_review_pr as workflow @@ -31,7 +29,6 @@ def __init__(self, head_results=None, search_results=None, on_lookup=None): self.head_results = head_results or [] self.search_results = search_results or [] self.on_lookup = on_lookup - self.reviewers = [] def _lookup(self, results): if self.on_lookup: @@ -51,13 +48,7 @@ def update_pull_request_body(self, _number, _body): return None def create_draft_pull_request(self, _base, _head, _title, _body): - return { - "number": 1, - "html_url": "https://github.com/Azure/azure-sdk-for-python/pull/1", - } - - def request_reviewers(self, pr_number, reviewers, team_reviewers=None): - self.reviewers.append((pr_number, reviewers, team_reviewers or [])) + return {"html_url": "https://github.com/Azure/azure-sdk-for-python/pull/1"} class ApiReviewPrTests(unittest.TestCase): @@ -157,8 +148,7 @@ def on_lookup(): workflow.target_reference_info("azure-example_1.2.3"), { "label": "Target tag", - "markdown": "[tag `azure-example_1.2.3`]" - "(https://github.com/Azure/azure-sdk-for-python/commit/abc123def456)", + "markdown": "[tag `azure-example_1.2.3`](https://github.com/Azure/azure-sdk-for-python/commit/abc123def456)", }, ) self.assertEqual(pr_lookup_count, 0) @@ -202,8 +192,7 @@ def on_lookup(): workflow.target_reference_info("azure-example_1.2.3", "azure-example"), { "label": "Target tag", - "markdown": "[tag `azure-example_1.2.3`]" - "(https://github.com/Azure/azure-sdk-for-python/commit/abc123def456)", + "markdown": "[tag `azure-example_1.2.3`](https://github.com/Azure/azure-sdk-for-python/commit/abc123def456)", }, ) self.assertEqual(pr_lookup_count, 0) @@ -233,13 +222,11 @@ def test_build_sync_metadata_object_records_fork_owner_and_branch(self): base_branch="apireview/base_azure-example_1.0.0", review_branch="apireview/review_azure-example_1.1.0", head_selector="example:users/example/feature", - package_work_item_id_value=31370, ) self.assertEqual(metadata["workingOwner"], "example") self.assertEqual(metadata["workingBranch"], "users/example/feature") self.assertEqual(metadata["workingPrNumber"], 47204) - self.assertEqual(metadata["packageWorkItemId"], 31370) def test_build_sync_metadata_object_omits_metadata_for_tag_targets(self): pr_lookup_count = 0 @@ -302,7 +289,6 @@ def test_build_review_pr_body_includes_sync_metadata_for_working_branch_reviews( "workingOwner": "Azure", "workingBranch": "main", "workingPrNumber": None, - "packageWorkItemId": "31370", } ) @@ -322,7 +308,6 @@ def test_build_review_pr_body_includes_sync_metadata_for_working_branch_reviews( self.assertNotIn("Static tag-to-tag review", body) self.assertIn("