From 22437fd6aafa24811e8fce5caa4537e3d5bf2d73 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Mon, 10 Aug 2026 16:10:14 -0700 Subject: [PATCH 1/7] Add debugpy release agent Automate the guarded tag, signed build, PyPI publication, and GitHub release workflow without committing internal infrastructure details. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: eaf32286-5971-44a3-ad1e-5fdbb2a89f8d --- .github/agents/debugpy-release.agent.md | 195 ++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 .github/agents/debugpy-release.agent.md diff --git a/.github/agents/debugpy-release.agent.md b/.github/agents/debugpy-release.agent.md new file mode 100644 index 00000000..64ce9ed0 --- /dev/null +++ b/.github/agents/debugpy-release.agent.md @@ -0,0 +1,195 @@ +--- +description: "Release debugpy to PyPI and GitHub. Use when asked to prepare or publish the next patch, minor, major, or explicit debugpy version." +name: "Debugpy Release" +argument-hint: "Specify patch, minor, major, or an explicit version. Say 'prepare' for a dry run." +tools: [vscode/askQuestions, execute/runInTerminal, execute/getTerminalOutput, execute/awaitTerminal, read/readFile, search/fileSearch, search/textSearch, web/fetch, todo] +user-invocable: true +--- + +# Debugpy Release Agent + +Automate the documented internal debugpy release process. Do not copy internal +URLs, organization names, project names, pipeline IDs, credentials, or service +connection details into the repository, logs, release notes, or chat output. + +## Intent + +Interpret requests as follows: + +- "Prepare" or "dry run" performs every read-only preflight and reports the + exact commands and release version, but does not create or push a tag, queue + publishing pipelines, or create a GitHub release. +- "Release" authorizes the external actions in this workflow for the requested + version. Do not ask for redundant confirmation unless the resolved version or + target commit is ambiguous. +- "Next patch", "next minor", and "next major" are calculated from the highest + stable version published by `microsoft/debugpy`, not from local tags. +- An explicit version may be written with or without the leading `v`. Normalize + Git tags to `v`. + +For example, if the latest stable release is `v1.8.21`, the next minor release +is `v1.9.0`. + +## Non-Negotiable Safety Rules + +1. Release only from the current `microsoft/debugpy` `main` commit. +2. The worktree must be clean. Never stash, discard, or include local changes. +3. Versioneer derives the package version from the Git tag. The internal build + must resolve the GitHub repository resource to `refs/tags/`, never the + latest `main`. +4. The build must use real signing. Never publish artifacts from a test-signed + or unsigned build. +5. Queue the release pipeline only after the exact tagged build succeeds. +6. Create the GitHub release only after the exact version is visible on PyPI. + Publishing the GitHub release first can break customer workflows. +7. Do not attach binaries to the GitHub release. +8. Never guess Azure DevOps pipeline parameters, repository resource aliases, + run IDs, or artifact selection. Inspect them and verify the queued run. +9. Never upload with `twine` or a personal PyPI token. PyPI publication must go + through the approved authenticated internal release pipeline. +10. Stop on any failed or partially successful stage. Do not continue with a + newer build or a different artifact. + +## Phase 1: Preflight + +1. Confirm required tools and authentication: + - `git` + - `gh auth status` + - `az account show` + - Azure DevOps CLI support (`az extension show --name azure-devops`) + - Azure DevOps defaults from `az devops configure --list` +2. If the Azure DevOps extension is missing, install it with: + + ```text + az extension add --name azure-devops + ``` + +3. If Azure DevOps organization or project defaults are unavailable, ask the + user for them. Use them only for the current commands; do not commit them. +4. Verify the repository and worktree: + + ```text + git status --short + git remote -v + ``` + +5. Resolve the authoritative `main` commit directly from + `https://github.com/microsoft/debugpy.git`. Fetch it if necessary and require + `HEAD` to equal that commit. Do not release a fork-only commit. +6. Determine the highest stable public version using GitHub releases and PyPI. + Cross-check both sources. Ignore prereleases when calculating the next + patch/minor/major version. +7. Fail if the proposed tag or PyPI version already exists. +8. Verify required checks for the target commit have completed successfully. +9. Run the repository's existing targeted packaging/version checks if + available. At minimum, use a temporary clone or worktree with a local-only + proposed tag to build package metadata without publishing, verify the + resulting version is exactly the proposed version, and remove the temporary + location afterward. +10. Present a concise preflight summary containing: + - Previous stable version + - Proposed version and tag + - Full target commit SHA and subject + - GitHub repository + - Discovered internal build and release pipeline names + +Stop here for prepare/dry-run requests. + +## Phase 2: Create and Push the Tag + +1. Create an annotated tag on the verified `microsoft/debugpy` `main` commit: + + ```text + git tag -a -m "debugpy " + ``` + +2. Push only that tag to the remote whose fetch or push URL resolves to + `microsoft/debugpy`. Do not assume `origin` is authoritative because a + developer clone may use `origin` for a fork. +3. Verify the remote tag resolves to the intended full commit SHA. + +If a local tag was created but the push failed, report that exact state. Never +move or force-push an existing release tag. + +## Phase 3: Queue the Internal Real-Signed Build + +1. Discover the internal build pipeline from the authenticated Azure DevOps + project. Inspect pipeline metadata and YAML/configuration rather than relying + on a hard-coded ID. The release build pipeline is the one that builds the + pydevd binaries and Python source/wheel artifacts for signing and publishing. +2. Inspect its runtime parameters and repository resources. Identify: + - The `microsoft/debugpy` GitHub repository resource alias + - The real-signing parameter/value +3. Queue the pipeline with that repository resource pinned to: + + ```text + refs/tags/ + ``` + + and with real signing selected. +4. Use `az pipelines run` when it can express both settings. Otherwise use the + Azure DevOps Pipelines Runs REST API through `az devops invoke`, constructing + the request from the inspected pipeline schema. Do not guess field names. +5. Immediately inspect the queued run and verify: + - The resolved debugpy resource is the exact tag + - The resolved source commit is the tagged commit + - Real signing is enabled + + Cancel the run if any value is wrong. +6. Wait for completion. Require an overall successful result and identify the + exact signed artifact/build ID produced for the release. + +## Phase 4: Queue the Internal Release Pipeline + +1. Discover the internal release pipeline from the authenticated Azure DevOps + project. It must consume the signed debugpy build, run final tests, and + publish to PyPI through the approved internal publishing path. +2. Inspect how it selects the input build. Pin it to the exact successful build + from Phase 3 whenever the pipeline supports an explicit build/run parameter. + If it selects by tags or branch, verify that its selection resolves to the + exact Phase 3 build before allowing publication. +3. Queue the release pipeline manually and record its run ID. +4. Wait for all validation and publishing stages to succeed. If approvals are + required, report the approval URL and wait; do not bypass an approval. + +## Phase 5: Verify PyPI + +Poll the public PyPI JSON endpoint for the exact normalized version: + +```text +https://pypi.org/pypi/debugpy//json +``` + +Require a successful response and confirm that release files are present. +Compare the published filenames against the successful release artifacts when +that information is available. Do not proceed based only on the Azure DevOps +run result. + +## Phase 6: Create the GitHub Release + +1. Generate release notes from the previous stable tag through the new tag. + Keep relevant issue and pull request links. Review the generated text for + unrelated changes or internal information. +2. Create the release in `microsoft/debugpy`: + + ```text + gh release create --repo microsoft/debugpy --title "debugpy " --notes-file --latest + ``` + +3. Do not pass artifact files to `gh release create`. +4. Verify the release is public, marked latest, points to the intended tag, and + contains no attached binaries. +5. Delete any temporary release-notes file after successful publication. + +## Completion Report + +Report only: + +- Released version and tag +- Tagged commit SHA +- Internal build and release run names/IDs with final status +- PyPI version URL +- GitHub release URL + +On failure, report the failed phase, the exact known external state, and the +safe recovery action. Never describe a partial release as successful. From c75f78b4f3c2482521de12ea41cc10581e11602c Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Mon, 10 Aug 2026 16:25:44 -0700 Subject: [PATCH 2/7] Address release agent review feedback Add deterministic state reconciliation, resumable releases, required-check discovery, exact build pinning, bounded PyPI verification, and backport-aware latest handling. Allow private pipeline URLs through environment variables. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: eaf32286-5971-44a3-ad1e-5fdbb2a89f8d --- .github/agents/debugpy-release.agent.md | 452 ++++++++++++++---------- 1 file changed, 257 insertions(+), 195 deletions(-) diff --git a/.github/agents/debugpy-release.agent.md b/.github/agents/debugpy-release.agent.md index 64ce9ed0..96b3e7d4 100644 --- a/.github/agents/debugpy-release.agent.md +++ b/.github/agents/debugpy-release.agent.md @@ -1,195 +1,257 @@ ---- -description: "Release debugpy to PyPI and GitHub. Use when asked to prepare or publish the next patch, minor, major, or explicit debugpy version." -name: "Debugpy Release" -argument-hint: "Specify patch, minor, major, or an explicit version. Say 'prepare' for a dry run." -tools: [vscode/askQuestions, execute/runInTerminal, execute/getTerminalOutput, execute/awaitTerminal, read/readFile, search/fileSearch, search/textSearch, web/fetch, todo] -user-invocable: true ---- - -# Debugpy Release Agent - -Automate the documented internal debugpy release process. Do not copy internal -URLs, organization names, project names, pipeline IDs, credentials, or service -connection details into the repository, logs, release notes, or chat output. - -## Intent - -Interpret requests as follows: - -- "Prepare" or "dry run" performs every read-only preflight and reports the - exact commands and release version, but does not create or push a tag, queue - publishing pipelines, or create a GitHub release. -- "Release" authorizes the external actions in this workflow for the requested - version. Do not ask for redundant confirmation unless the resolved version or - target commit is ambiguous. -- "Next patch", "next minor", and "next major" are calculated from the highest - stable version published by `microsoft/debugpy`, not from local tags. -- An explicit version may be written with or without the leading `v`. Normalize - Git tags to `v`. - -For example, if the latest stable release is `v1.8.21`, the next minor release -is `v1.9.0`. - -## Non-Negotiable Safety Rules - -1. Release only from the current `microsoft/debugpy` `main` commit. -2. The worktree must be clean. Never stash, discard, or include local changes. -3. Versioneer derives the package version from the Git tag. The internal build - must resolve the GitHub repository resource to `refs/tags/`, never the - latest `main`. -4. The build must use real signing. Never publish artifacts from a test-signed - or unsigned build. -5. Queue the release pipeline only after the exact tagged build succeeds. -6. Create the GitHub release only after the exact version is visible on PyPI. - Publishing the GitHub release first can break customer workflows. -7. Do not attach binaries to the GitHub release. -8. Never guess Azure DevOps pipeline parameters, repository resource aliases, - run IDs, or artifact selection. Inspect them and verify the queued run. -9. Never upload with `twine` or a personal PyPI token. PyPI publication must go - through the approved authenticated internal release pipeline. -10. Stop on any failed or partially successful stage. Do not continue with a - newer build or a different artifact. - -## Phase 1: Preflight - -1. Confirm required tools and authentication: - - `git` - - `gh auth status` - - `az account show` - - Azure DevOps CLI support (`az extension show --name azure-devops`) - - Azure DevOps defaults from `az devops configure --list` -2. If the Azure DevOps extension is missing, install it with: - - ```text - az extension add --name azure-devops - ``` - -3. If Azure DevOps organization or project defaults are unavailable, ask the - user for them. Use them only for the current commands; do not commit them. -4. Verify the repository and worktree: - - ```text - git status --short - git remote -v - ``` - -5. Resolve the authoritative `main` commit directly from - `https://github.com/microsoft/debugpy.git`. Fetch it if necessary and require - `HEAD` to equal that commit. Do not release a fork-only commit. -6. Determine the highest stable public version using GitHub releases and PyPI. - Cross-check both sources. Ignore prereleases when calculating the next - patch/minor/major version. -7. Fail if the proposed tag or PyPI version already exists. -8. Verify required checks for the target commit have completed successfully. -9. Run the repository's existing targeted packaging/version checks if - available. At minimum, use a temporary clone or worktree with a local-only - proposed tag to build package metadata without publishing, verify the - resulting version is exactly the proposed version, and remove the temporary - location afterward. -10. Present a concise preflight summary containing: - - Previous stable version - - Proposed version and tag - - Full target commit SHA and subject - - GitHub repository - - Discovered internal build and release pipeline names - -Stop here for prepare/dry-run requests. - -## Phase 2: Create and Push the Tag - -1. Create an annotated tag on the verified `microsoft/debugpy` `main` commit: - - ```text - git tag -a -m "debugpy " - ``` - -2. Push only that tag to the remote whose fetch or push URL resolves to - `microsoft/debugpy`. Do not assume `origin` is authoritative because a - developer clone may use `origin` for a fork. -3. Verify the remote tag resolves to the intended full commit SHA. - -If a local tag was created but the push failed, report that exact state. Never -move or force-push an existing release tag. - -## Phase 3: Queue the Internal Real-Signed Build - -1. Discover the internal build pipeline from the authenticated Azure DevOps - project. Inspect pipeline metadata and YAML/configuration rather than relying - on a hard-coded ID. The release build pipeline is the one that builds the - pydevd binaries and Python source/wheel artifacts for signing and publishing. -2. Inspect its runtime parameters and repository resources. Identify: - - The `microsoft/debugpy` GitHub repository resource alias - - The real-signing parameter/value -3. Queue the pipeline with that repository resource pinned to: - - ```text - refs/tags/ - ``` - - and with real signing selected. -4. Use `az pipelines run` when it can express both settings. Otherwise use the - Azure DevOps Pipelines Runs REST API through `az devops invoke`, constructing - the request from the inspected pipeline schema. Do not guess field names. -5. Immediately inspect the queued run and verify: - - The resolved debugpy resource is the exact tag - - The resolved source commit is the tagged commit - - Real signing is enabled - - Cancel the run if any value is wrong. -6. Wait for completion. Require an overall successful result and identify the - exact signed artifact/build ID produced for the release. - -## Phase 4: Queue the Internal Release Pipeline - -1. Discover the internal release pipeline from the authenticated Azure DevOps - project. It must consume the signed debugpy build, run final tests, and - publish to PyPI through the approved internal publishing path. -2. Inspect how it selects the input build. Pin it to the exact successful build - from Phase 3 whenever the pipeline supports an explicit build/run parameter. - If it selects by tags or branch, verify that its selection resolves to the - exact Phase 3 build before allowing publication. -3. Queue the release pipeline manually and record its run ID. -4. Wait for all validation and publishing stages to succeed. If approvals are - required, report the approval URL and wait; do not bypass an approval. - -## Phase 5: Verify PyPI - -Poll the public PyPI JSON endpoint for the exact normalized version: - -```text -https://pypi.org/pypi/debugpy//json -``` - -Require a successful response and confirm that release files are present. -Compare the published filenames against the successful release artifacts when -that information is available. Do not proceed based only on the Azure DevOps -run result. - -## Phase 6: Create the GitHub Release - -1. Generate release notes from the previous stable tag through the new tag. - Keep relevant issue and pull request links. Review the generated text for - unrelated changes or internal information. -2. Create the release in `microsoft/debugpy`: - - ```text - gh release create --repo microsoft/debugpy --title "debugpy " --notes-file --latest - ``` - -3. Do not pass artifact files to `gh release create`. -4. Verify the release is public, marked latest, points to the intended tag, and - contains no attached binaries. -5. Delete any temporary release-notes file after successful publication. - -## Completion Report - -Report only: - -- Released version and tag -- Tagged commit SHA -- Internal build and release run names/IDs with final status -- PyPI version URL -- GitHub release URL - -On failure, report the failed phase, the exact known external state, and the -safe recovery action. Never describe a partial release as successful. +--- +description: "Release debugpy to PyPI and GitHub. Use when asked to prepare or publish the next patch, minor, major, or explicit debugpy version." +name: "Debugpy Release" +argument-hint: "Specify patch, minor, major, or an explicit version. Say 'prepare' for a dry run." +tools: [vscode/askQuestions, execute/runInTerminal, execute/getTerminalOutput, execute/awaitTerminal, read/readFile, search/fileSearch, search/textSearch, web/fetch, todo] +user-invocable: true +--- + +# Debugpy Release Agent + +Automate the documented internal debugpy release process. Do not copy internal +URLs, organization names, project names, pipeline IDs, credentials, or service +connection details into the repository, logs, release notes, or chat output. + +## Intent + +Interpret requests as follows: + +- "Prepare" or "dry run" performs every read-only preflight and reports the + exact commands and release version, but does not create or push a tag, queue + publishing pipelines, or create a GitHub release. +- "Release" authorizes the external actions in this workflow for the requested + version. Do not ask for redundant confirmation unless the resolved version or + target commit is ambiguous. +- "Resume" or "finalize" inventories the external state for an explicit version + and continues from the first incomplete phase, but only after verifying every + completed phase against the same tag, commit, build, and package version. +- "Next patch", "next minor", and "next major" are calculated from the highest + stable version published by `microsoft/debugpy`, not from local tags. +- An explicit version may be written with or without the leading `v`. Normalize + Git tags to `v`. + +For example, if the latest stable release is `v1.8.21`, the next minor release +is `v1.9.0`. + +## Non-Negotiable Safety Rules + +1. Start a new release only from the current `microsoft/debugpy` `main` + commit. Resume an existing partial release only from its immutable tag + commit after verifying the completed external state. +2. The worktree must be clean. Never stash, discard, or include local changes. +3. Versioneer derives the package version from the Git tag. The internal build + must resolve the GitHub repository resource to `refs/tags/`, never the + latest `main`. +4. The build must use real signing. Never publish artifacts from a test-signed + or unsigned build. +5. Queue the release pipeline only after the exact tagged build succeeds. +6. Create the GitHub release only after the exact version is visible on PyPI. + Publishing the GitHub release first can break customer workflows. +7. Do not attach binaries to the GitHub release. +8. Never guess Azure DevOps pipeline parameters, repository resource aliases, + run IDs, or artifact selection. Inspect them and verify the queued run. +9. Never upload with `twine` or a personal PyPI token. PyPI publication must go + through the approved authenticated internal release pipeline. +10. Stop on any failed or partially successful stage. Do not continue with a + newer build or a different artifact. Resume only from verified state. + +## Pipeline Locations + +Prefer these environment variables when they are set: + +- `DEBUGPY_INTERNAL_BUILD_PIPELINE_URL` +- `DEBUGPY_INTERNAL_RELEASE_PIPELINE_URL` + +Treat their values as confidential: use them for commands and navigation, but +do not echo or persist them. Validate that each URL belongs to the authenticated +Azure DevOps organization and project before using it. If either variable is +unset, discover the corresponding pipeline from the authenticated project as +described below. + +## Phase 1: Preflight + +1. Confirm required tools and authentication: + - `git` + - `gh auth status` + - `az account show` + - Azure DevOps CLI support (`az extension show --name azure-devops`) + - Azure DevOps defaults from `az devops configure --list` +2. If the Azure DevOps extension is missing, install it with: + + ```text + az extension add --name azure-devops + ``` + +3. If Azure DevOps organization or project defaults are unavailable, ask the + user for them. Use them only for the current commands; do not commit them. +4. Verify the repository and worktree: + + ```text + git status --short + git remote -v + ``` + +5. For a new release, resolve the authoritative `main` commit directly from + `https://github.com/microsoft/debugpy.git`. Fetch it if necessary and require + `HEAD` to equal that commit. Do not release a fork-only commit. For resume, + resolve the commit from the existing remote tag instead and do not retag the + current `main`. +6. Read the complete sets of stable public versions from GitHub releases and + PyPI, ignoring prereleases. Reconcile them deterministically: + - If the highest stable versions match, use that version as the baseline. + - If PyPI contains a newer version than GitHub, treat it as an incomplete + release and resume at GitHub release creation. Do not calculate or publish + a newer version. + - If GitHub contains a newer version than PyPI, stop because customer + workflows may already be inconsistent. Report that PyPI publication for + the existing GitHub release must be recovered before another release. + - If the sets disagree in any other way that affects the proposed version, + stop and report both sets. Never choose one source arbitrarily. +7. Inventory the proposed version's tag, internal build, internal release run, + PyPI files, and GitHub release. For a new release, require all to be absent. + For resume/finalize, require each existing item to match the same commit, + exact signed build, and package version, then continue from the first absent + item. Never recreate, move, or overwrite an existing item. +8. Discover the authoritative required checks for `main` using the GitHub + rules-for-branch API: + + ```text + gh api repos/microsoft/debugpy/rules/branches/main + ``` + + If repository rules do not define them, inspect branch protection: + + ```text + gh api repos/microsoft/debugpy/branches/main/protection/required_status_checks + ``` + + Query check runs and commit statuses for the target SHA, match the required + contexts and integration IDs, and require every required result to be + successful. Do not substitute "all visible checks" for required checks. Stop + if the rules cannot be retrieved or mapped unambiguously. +9. Run the repository's existing targeted packaging/version checks if + available. At minimum, use a temporary clone or worktree with a local-only + proposed tag to build package metadata without publishing, verify the + resulting normalized package version is exactly the proposed version, save + that exact value for all later PyPI checks, and remove the temporary location + afterward. +10. Present a concise preflight summary containing: + - Previous stable version + - Proposed version and tag + - Full target commit SHA and subject + - GitHub repository + - Discovered internal build and release pipeline names + +Stop here for prepare/dry-run requests. + +## Phase 2: Create and Push the Tag + +1. Create an annotated tag on the verified `microsoft/debugpy` `main` commit: + + ```text + git tag -a -m "debugpy " + ``` + +2. Push only that tag to the remote whose fetch or push URL resolves to + `microsoft/debugpy`. Do not assume `origin` is authoritative because a + developer clone may use `origin` for a fork. +3. Verify the remote tag resolves to the intended full commit SHA. + +If a local tag was created but the push failed, report that exact state. Never +move or force-push an existing release tag. + +## Phase 3: Queue the Internal Real-Signed Build + +1. Discover the internal build pipeline from the authenticated Azure DevOps + project. Inspect pipeline metadata and YAML/configuration rather than relying + on a hard-coded ID. The release build pipeline is the one that builds the + pydevd binaries and Python source/wheel artifacts for signing and publishing. + If `DEBUGPY_INTERNAL_BUILD_PIPELINE_URL` is set, resolve the pipeline from + that validated URL instead of searching by name. +2. Inspect its runtime parameters and repository resources. Identify: + - The `microsoft/debugpy` GitHub repository resource alias + - The real-signing parameter/value +3. Queue the pipeline with that repository resource pinned to: + + ```text + refs/tags/ + ``` + + and with real signing selected. +4. Use `az pipelines run` when it can express both settings. Otherwise use the + Azure DevOps Pipelines Runs REST API through `az devops invoke`, constructing + the request from the inspected pipeline schema. Do not guess field names. +5. Immediately inspect the queued run and verify: + - The resolved debugpy resource is the exact tag + - The resolved source commit is the tagged commit + - Real signing is enabled + + Cancel the run if any value is wrong. +6. Wait for completion. Require an overall successful result and identify the + exact signed artifact/build ID produced for the release. + +## Phase 4: Queue the Internal Release Pipeline + +1. Discover the internal release pipeline from the authenticated Azure DevOps + project. It must consume the signed debugpy build, run final tests, and + publish to PyPI through the approved internal publishing path. + If `DEBUGPY_INTERNAL_RELEASE_PIPELINE_URL` is set, resolve the pipeline from + that validated URL instead of searching by name. +2. Inspect how it selects the input build. Pin it to the exact successful build + ID from Phase 3. If explicit pinning is unavailable, verify the selected + build immediately before queueing and again at the final pre-publish + approval. Abort if it is no longer the Phase 3 build. If the pipeline cannot + pin the build and has no pre-publish gate where selection can be reverified, + stop rather than risk publishing a raced artifact. +3. Queue the release pipeline manually and record its run ID. +4. Wait for all validation and publishing stages to succeed. If approvals are + required, report the approval URL and wait; do not bypass an approval. + +## Phase 5: Verify PyPI + +Poll the public PyPI JSON endpoint using the exact normalized package version +produced by the Phase 1 metadata build: + +```text +https://pypi.org/pypi/debugpy//json +``` + +Require a successful response and confirm that release files are present. +Compare the published filenames against the successful release artifacts when +that information is available. Poll every 30 seconds for at most 15 minutes. +If the deadline expires, stop in a resumable state and report that publication +may have succeeded but public index verification did not. Do not proceed based +only on the Azure DevOps run result. + +## Phase 6: Create the GitHub Release + +1. Generate release notes from the previous stable tag through the new tag. + Keep relevant issue and pull request links. Review the generated text for + unrelated changes or internal information. +2. Compare the new version to the highest stable GitHub release that existed + before this workflow. Add `--latest` only when the new version is greater. + Omit it for backports and older release lines. +3. Create the release in `microsoft/debugpy`: + + ```text + gh release create --repo microsoft/debugpy --title "debugpy " --notes-file [--latest] + ``` + +4. Do not pass artifact files to `gh release create`. +5. Verify the release is public, points to the intended tag, and contains no + attached binaries. Verify it is marked latest only when `--latest` was + required. +6. Delete any temporary release-notes file after successful publication. + +## Completion Report + +Report only: + +- Released version and tag +- Tagged commit SHA +- Internal build and release run names/IDs with final status +- PyPI version URL +- GitHub release URL + +On failure, report the failed phase, the exact known external state, and the +safe recovery action. Never describe a partial release as successful. From 5dfaa63bfe8c567481e5645473e0344312219f12 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Mon, 10 Aug 2026 16:42:24 -0700 Subject: [PATCH 3/7] Address remaining release agent feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/agents/debugpy-release.agent.md | 35 ++++++++++++++++--------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/.github/agents/debugpy-release.agent.md b/.github/agents/debugpy-release.agent.md index 96b3e7d4..ce62d3cc 100644 --- a/.github/agents/debugpy-release.agent.md +++ b/.github/agents/debugpy-release.agent.md @@ -25,6 +25,10 @@ Interpret requests as follows: - "Resume" or "finalize" inventories the external state for an explicit version and continues from the first incomplete phase, but only after verifying every completed phase against the same tag, commit, build, and package version. +- If a new-release request discovers a different version already published to + PyPI but missing from GitHub, report that incomplete release and ask before + changing the requested target. A resume/finalize request for that exact + version may continue without additional confirmation. - "Next patch", "next minor", and "next major" are calculated from the highest stable version published by `microsoft/debugpy`, not from local tags. - An explicit version may be written with or without the leading `v`. Normalize @@ -100,8 +104,10 @@ described below. PyPI, ignoring prereleases. Reconcile them deterministically: - If the highest stable versions match, use that version as the baseline. - If PyPI contains a newer version than GitHub, treat it as an incomplete - release and resume at GitHub release creation. Do not calculate or publish - a newer version. + release. For a new-release request, stop and ask whether to finalize that + existing version instead. For a resume/finalize request naming that exact + version, continue at GitHub release creation. Do not calculate or publish a + newer version. - If GitHub contains a newer version than PyPI, stop because customer workflows may already be inconsistent. Report that PyPI publication for the existing GitHub release must be recovered before another release. @@ -157,8 +163,10 @@ Stop here for prepare/dry-run requests. developer clone may use `origin` for a fork. 3. Verify the remote tag resolves to the intended full commit SHA. -If a local tag was created but the push failed, report that exact state. Never -move or force-push an existing release tag. +If a local tag was created but the push failed, a later resume may re-push it +only after verifying that the remote tag is still absent and the local annotated +tag resolves to the previously verified commit. Otherwise stop and report the +exact recovery action. Never move or force-push an existing release tag. ## Phase 3: Queue the Internal Real-Signed Build @@ -225,22 +233,25 @@ only on the Azure DevOps run result. ## Phase 6: Create the GitHub Release -1. Generate release notes from the previous stable tag through the new tag. - Keep relevant issue and pull request links. Review the generated text for - unrelated changes or internal information. +1. Select the release-notes base deterministically. For a backport or older + release line, use the greatest stable tag lower than the new version with the + same major and minor components. Otherwise use the greatest stable tag lower + than the new version. Stop if no valid lower tag exists. Generate release + notes from that tag through the new tag, keep relevant issue and pull request + links, and review the text for unrelated changes or internal information. 2. Compare the new version to the highest stable GitHub release that existed - before this workflow. Add `--latest` only when the new version is greater. - Omit it for backports and older release lines. + before this workflow. Pass `--latest` when the new version is greater and + `--latest=false` for backports and older release lines. 3. Create the release in `microsoft/debugpy`: ```text - gh release create --repo microsoft/debugpy --title "debugpy " --notes-file [--latest] + gh release create --repo microsoft/debugpy --title "debugpy " --notes-file <--latest|--latest=false> ``` 4. Do not pass artifact files to `gh release create`. 5. Verify the release is public, points to the intended tag, and contains no - attached binaries. Verify it is marked latest only when `--latest` was - required. + attached binaries. Verify it is marked latest when `--latest` was used and + is not marked latest when `--latest=false` was used. 6. Delete any temporary release-notes file after successful publication. ## Completion Report From 5c2eaf80ea044ee29f5d199450b6ae3299c68c7e Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Mon, 10 Aug 2026 16:54:36 -0700 Subject: [PATCH 4/7] Harden release resume state handling Make local-only tag recovery re-derivable, prevent republishing after PyPI succeeds, and preserve the Phase 1 latest-selection baseline across resumed runs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: eaf32286-5971-44a3-ad1e-5fdbb2a89f8d --- .github/agents/debugpy-release.agent.md | 35 ++++++++++++++++++++----- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/.github/agents/debugpy-release.agent.md b/.github/agents/debugpy-release.agent.md index ce62d3cc..58c9c34a 100644 --- a/.github/agents/debugpy-release.agent.md +++ b/.github/agents/debugpy-release.agent.md @@ -99,7 +99,19 @@ described below. `https://github.com/microsoft/debugpy.git`. Fetch it if necessary and require `HEAD` to equal that commit. Do not release a fork-only commit. For resume, resolve the commit from the existing remote tag instead and do not retag the - current `main`. + current `main`. If the remote tag is absent but an exact local tag exists, + treat it as a local-tag recovery candidate only when: + - It is an annotated tag and the requested tag name matches exactly. + - The remote tag, PyPI version, GitHub release, and internal release state are + all absent. + - Its target equals the current authoritative `microsoft/debugpy` `main` + commit and that commit passes the required-check gate below. + - Rebuilding metadata from that tag produces the requested normalized + package version. + + When every condition holds, route to Phase 2 to push that exact tag. If the + target no longer equals current `main`, do not move the tag; stop and report + that the user must choose whether to delete the local-only tag and restart. 6. Read the complete sets of stable public versions from GitHub releases and PyPI, ignoring prereleases. Reconcile them deterministically: - If the highest stable versions match, use that version as the baseline. @@ -113,11 +125,19 @@ described below. the existing GitHub release must be recovered before another release. - If the sets disagree in any other way that affects the proposed version, stop and report both sets. Never choose one source arbitrarily. + Record the highest stable GitHub version observed here as the immutable + latest-selection baseline for Phase 6. Do not try to reconstruct this + historical snapshot later during resume. 7. Inventory the proposed version's tag, internal build, internal release run, PyPI files, and GitHub release. For a new release, require all to be absent. - For resume/finalize, require each existing item to match the same commit, - exact signed build, and package version, then continue from the first absent - item. Never recreate, move, or overwrite an existing item. + Except for the PyPI override below, resume/finalize requires every existing + item to match the same commit, exact signed build, and package version, then + continues from the first absent item. Never recreate, move, or overwrite an + existing item. PyPI publication overrides the generic first-absent rule: + when the exact version and files already exist on PyPI but the GitHub release + is absent, require the matching remote tag and proceed only to Phase 6. Do + not re-queue build or release pipelines even if their historical records are + unavailable or expired. 8. Discover the authoritative required checks for `main` using the GitHub rules-for-branch API: @@ -239,9 +259,10 @@ only on the Azure DevOps run result. than the new version. Stop if no valid lower tag exists. Generate release notes from that tag through the new tag, keep relevant issue and pull request links, and review the text for unrelated changes or internal information. -2. Compare the new version to the highest stable GitHub release that existed - before this workflow. Pass `--latest` when the new version is greater and - `--latest=false` for backports and older release lines. +2. Compare the new version to the immutable latest-selection baseline recorded + during Phase 1. Pass `--latest` when the new version is greater and + `--latest=false` for backports and older release lines. Do not re-query or + re-derive the baseline during a later resume. 3. Create the release in `microsoft/debugpy`: ```text From 1c08e1b933a4d44ff6f1fb4f29e8b7fc6b8eaeb3 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Mon, 10 Aug 2026 17:03:47 -0700 Subject: [PATCH 5/7] Clarify release baseline lifecycle Keep local-tag recovery within the full preflight and recompute the latest-selection baseline deterministically for each invocation, with a final release-set recheck. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: eaf32286-5971-44a3-ad1e-5fdbb2a89f8d --- .github/agents/debugpy-release.agent.md | 27 ++++++++++++++++--------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/.github/agents/debugpy-release.agent.md b/.github/agents/debugpy-release.agent.md index 58c9c34a..917f7171 100644 --- a/.github/agents/debugpy-release.agent.md +++ b/.github/agents/debugpy-release.agent.md @@ -109,9 +109,12 @@ described below. - Rebuilding metadata from that tag produces the requested normalized package version. - When every condition holds, route to Phase 2 to push that exact tag. If the - target no longer equals current `main`, do not move the tag; stop and report - that the user must choose whether to delete the local-only tag and restart. + When every condition holds, continue through the remainder of Phase 1, + including baseline recording, inventory, required-check verification, + metadata validation, and the preflight summary. Then route to Phase 2 to push + that exact tag. If the target no longer equals current `main`, do not move + the tag; stop and report that the user must choose whether to delete the + local-only tag and restart. 6. Read the complete sets of stable public versions from GitHub releases and PyPI, ignoring prereleases. Reconcile them deterministically: - If the highest stable versions match, use that version as the baseline. @@ -125,9 +128,10 @@ described below. the existing GitHub release must be recovered before another release. - If the sets disagree in any other way that affects the proposed version, stop and report both sets. Never choose one source arbitrarily. - Record the highest stable GitHub version observed here as the immutable - latest-selection baseline for Phase 6. Do not try to reconstruct this - historical snapshot later during resume. + Record the highest stable GitHub version observed here as the + latest-selection baseline for this invocation. Recompute it deterministically + whenever a later invocation resumes the release; do not claim that an + unpersisted value survives across sessions. 7. Inventory the proposed version's tag, internal build, internal release run, PyPI files, and GitHub release. For a new release, require all to be absent. Except for the PyPI override below, resume/finalize requires every existing @@ -259,10 +263,13 @@ only on the Azure DevOps run result. than the new version. Stop if no valid lower tag exists. Generate release notes from that tag through the new tag, keep relevant issue and pull request links, and review the text for unrelated changes or internal information. -2. Compare the new version to the immutable latest-selection baseline recorded - during Phase 1. Pass `--latest` when the new version is greater and - `--latest=false` for backports and older release lines. Do not re-query or - re-derive the baseline during a later resume. +2. Recheck that the stable GitHub release set has not changed since the Phase 1 + snapshot. If it changed, repeat Phase 1 reconciliation and update the + invocation's latest-selection baseline before continuing. For backports and + older release lines, always pass `--latest=false` regardless of the + baseline. Otherwise pass `--latest` only when the new version is greater + than the current invocation's baseline; pass `--latest=false` when it is + not. 3. Create the release in `microsoft/debugpy`: ```text From 13563bf8941b9b7a03347163ee201f8703e8123c Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Mon, 10 Aug 2026 17:14:38 -0700 Subject: [PATCH 6/7] Disambiguate release recovery inventory Separate local and remote tag state during recovery and exclude the verified in-flight PyPI version from final concurrency reconciliation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: eaf32286-5971-44a3-ad1e-5fdbb2a89f8d --- .github/agents/debugpy-release.agent.md | 42 ++++++++++++++----------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/.github/agents/debugpy-release.agent.md b/.github/agents/debugpy-release.agent.md index 917f7171..087f06af 100644 --- a/.github/agents/debugpy-release.agent.md +++ b/.github/agents/debugpy-release.agent.md @@ -102,8 +102,8 @@ described below. current `main`. If the remote tag is absent but an exact local tag exists, treat it as a local-tag recovery candidate only when: - It is an annotated tag and the requested tag name matches exactly. - - The remote tag, PyPI version, GitHub release, and internal release state are - all absent. + - The remote tag, internal build, internal release run, PyPI version, and + GitHub release are all absent. - Its target equals the current authoritative `microsoft/debugpy` `main` commit and that commit passes the required-check gate below. - Rebuilding metadata from that tag produces the requested normalized @@ -132,16 +132,19 @@ described below. latest-selection baseline for this invocation. Recompute it deterministically whenever a later invocation resumes the release; do not claim that an unpersisted value survives across sessions. -7. Inventory the proposed version's tag, internal build, internal release run, - PyPI files, and GitHub release. For a new release, require all to be absent. - Except for the PyPI override below, resume/finalize requires every existing - item to match the same commit, exact signed build, and package version, then - continues from the first absent item. Never recreate, move, or overwrite an - existing item. PyPI publication overrides the generic first-absent rule: - when the exact version and files already exist on PyPI but the GitHub release - is absent, require the matching remote tag and proceed only to Phase 6. Do - not re-queue build or release pipelines even if their historical records are - unavailable or expired. +7. Inventory the proposed version's local tag, remote tag, internal build, + internal release run, PyPI files, and GitHub release. For a new release, + require the remote tag, internal build, internal release run, PyPI files, and + GitHub release to be absent. Require the local tag to be absent unless step 5 + accepted it as the exact local-tag recovery candidate. Except for the PyPI + override below, resume/finalize requires every existing item to match the + same commit, exact signed build, and package version, then continues from the + first absent item. Never recreate, move, or overwrite an existing item. PyPI + publication overrides the generic first-absent rule: when the exact version + and files already exist on PyPI but the GitHub release is absent, require the + matching remote tag and proceed only to Phase 6. Do not re-queue build or + release pipelines even if their historical records are unavailable or + expired. 8. Discover the authoritative required checks for `main` using the GitHub rules-for-branch API: @@ -264,12 +267,15 @@ only on the Azure DevOps run result. notes from that tag through the new tag, keep relevant issue and pull request links, and review the text for unrelated changes or internal information. 2. Recheck that the stable GitHub release set has not changed since the Phase 1 - snapshot. If it changed, repeat Phase 1 reconciliation and update the - invocation's latest-selection baseline before continuing. For backports and - older release lines, always pass `--latest=false` regardless of the - baseline. Otherwise pass `--latest` only when the new version is greater - than the current invocation's baseline; pass `--latest=false` when it is - not. + snapshot. When comparing it with PyPI, exclude only the exact in-flight + version already verified in Phase 5; its expected PyPI-only state must not be + treated as a new incomplete release. If any other release state changed, + repeat Phase 1 reconciliation with the in-flight version explicitly marked + as expected and update the invocation's latest-selection baseline before + continuing. For backports and older release lines, always pass + `--latest=false` regardless of the baseline. Otherwise pass `--latest` only + when the new version is greater than the current invocation's baseline; pass + `--latest=false` when it is not. 3. Create the release in `microsoft/debugpy`: ```text From 48046cfbeb348b6f0160c44cb913a3185baaa8e0 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Mon, 10 Aug 2026 17:24:49 -0700 Subject: [PATCH 7/7] Tighten release agent execution contract Use host-provided tools, centralize recovery inventory, define backports, unify in-flight PyPI state, and clean temporary artifacts on all exit paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: eaf32286-5971-44a3-ad1e-5fdbb2a89f8d --- .github/agents/debugpy-release.agent.md | 78 +++++++++++++------------ 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/.github/agents/debugpy-release.agent.md b/.github/agents/debugpy-release.agent.md index 087f06af..e4bed199 100644 --- a/.github/agents/debugpy-release.agent.md +++ b/.github/agents/debugpy-release.agent.md @@ -2,7 +2,6 @@ description: "Release debugpy to PyPI and GitHub. Use when asked to prepare or publish the next patch, minor, major, or explicit debugpy version." name: "Debugpy Release" argument-hint: "Specify patch, minor, major, or an explicit version. Say 'prepare' for a dry run." -tools: [vscode/askQuestions, execute/runInTerminal, execute/getTerminalOutput, execute/awaitTerminal, read/readFile, search/fileSearch, search/textSearch, web/fetch, todo] user-invocable: true --- @@ -58,6 +57,9 @@ is `v1.9.0`. through the approved authenticated internal release pipeline. 10. Stop on any failed or partially successful stage. Do not continue with a newer build or a different artifact. Resume only from verified state. +11. Remove temporary clones, worktrees, metadata output, and release-notes files + on both success and failure. If cleanup itself fails, report the exact path + that remains. ## Pipeline Locations @@ -100,21 +102,14 @@ described below. `HEAD` to equal that commit. Do not release a fork-only commit. For resume, resolve the commit from the existing remote tag instead and do not retag the current `main`. If the remote tag is absent but an exact local tag exists, - treat it as a local-tag recovery candidate only when: - - It is an annotated tag and the requested tag name matches exactly. - - The remote tag, internal build, internal release run, PyPI version, and - GitHub release are all absent. - - Its target equals the current authoritative `microsoft/debugpy` `main` - commit and that commit passes the required-check gate below. - - Rebuilding metadata from that tag produces the requested normalized - package version. - - When every condition holds, continue through the remainder of Phase 1, - including baseline recording, inventory, required-check verification, - metadata validation, and the preflight summary. Then route to Phase 2 to push - that exact tag. If the target no longer equals current `main`, do not move - the tag; stop and report that the user must choose whether to delete the - local-only tag and restart. + mark it as a local-tag recovery candidate only when it is annotated, its name + matches exactly, and its target equals the current authoritative + `microsoft/debugpy` `main` commit. Do not duplicate the external-state checks + here; step 7 is the authoritative inventory. Continue through the rest of + Phase 1 and route to Phase 2 only if inventory, required checks, metadata + validation, and the preflight summary all succeed. If the target no longer + equals current `main`, do not move the tag; stop and report that the user + must choose whether to delete the local-only tag and restart. 6. Read the complete sets of stable public versions from GitHub releases and PyPI, ignoring prereleases. Reconcile them deterministically: - If the highest stable versions match, use that version as the baseline. @@ -144,7 +139,8 @@ described below. and files already exist on PyPI but the GitHub release is absent, require the matching remote tag and proceed only to Phase 6. Do not re-queue build or release pipelines even if their historical records are unavailable or - expired. + expired. Record that exact normalized package version as the verified + in-flight version. 8. Discover the authoritative required checks for `main` using the GitHub rules-for-branch API: @@ -167,7 +163,7 @@ described below. proposed tag to build package metadata without publishing, verify the resulting normalized package version is exactly the proposed version, save that exact value for all later PyPI checks, and remove the temporary location - afterward. + afterward even when the build or validation fails. 10. Present a concise preflight summary containing: - Previous stable version - Proposed version and tag @@ -253,29 +249,34 @@ https://pypi.org/pypi/debugpy//json Require a successful response and confirm that release files are present. Compare the published filenames against the successful release artifacts when -that information is available. Poll every 30 seconds for at most 15 minutes. -If the deadline expires, stop in a resumable state and report that publication -may have succeeded but public index verification did not. Do not proceed based -only on the Azure DevOps run result. +that information is available. Record the exact normalized package version as +the verified in-flight version. Poll every 30 seconds for at most 15 minutes. If +the deadline expires, stop in a resumable state and report that publication may +have succeeded but public index verification did not. Do not proceed based only +on the Azure DevOps run result. ## Phase 6: Create the GitHub Release -1. Select the release-notes base deterministically. For a backport or older - release line, use the greatest stable tag lower than the new version with the - same major and minor components. Otherwise use the greatest stable tag lower - than the new version. Stop if no valid lower tag exists. Generate release - notes from that tag through the new tag, keep relevant issue and pull request - links, and review the text for unrelated changes or internal information. +1. Classify a release as a backport or older release line when the target's + `(major, minor)` tuple is lower than the `(major, minor)` tuple of the highest + stable version found during Phase 1. Require the target version to be greater + than every existing stable version on its own release line. For a backport, + use the greatest lower stable tag with the same major and minor components as + the release-notes base. Otherwise use the greatest stable tag lower than the + target. Stop if no valid lower tag exists. Generate release notes from that + tag through the new tag, keep relevant issue and pull request links, and + review the text for unrelated changes or internal information. 2. Recheck that the stable GitHub release set has not changed since the Phase 1 - snapshot. When comparing it with PyPI, exclude only the exact in-flight - version already verified in Phase 5; its expected PyPI-only state must not be - treated as a new incomplete release. If any other release state changed, - repeat Phase 1 reconciliation with the in-flight version explicitly marked - as expected and update the invocation's latest-selection baseline before - continuing. For backports and older release lines, always pass - `--latest=false` regardless of the baseline. Otherwise pass `--latest` only - when the new version is greater than the current invocation's baseline; pass - `--latest=false` when it is not. + snapshot. When comparing it with PyPI, exclude only the verified in-flight + version established either by the Phase 1 PyPI override inventory or by + Phase 5; its expected PyPI-only state must not be treated as a new incomplete + release. If any other release state changed, repeat Phase 1 reconciliation + with the in-flight version explicitly marked as expected and update the + invocation's latest-selection baseline before continuing. For backports and + older release lines, always pass `--latest=false` regardless of the + baseline. Otherwise pass `--latest` only when the new version is greater + than the current invocation's baseline; pass `--latest=false` when it is + not. 3. Create the release in `microsoft/debugpy`: ```text @@ -286,7 +287,8 @@ only on the Azure DevOps run result. 5. Verify the release is public, points to the intended tag, and contains no attached binaries. Verify it is marked latest when `--latest` was used and is not marked latest when `--latest=false` was used. -6. Delete any temporary release-notes file after successful publication. +6. Delete any temporary release-notes file after the creation attempt, whether + publication succeeds or fails. ## Completion Report