Skip to content

Read a pull request from Azure DevOps as well as from GitHub - #13

Merged
siegfriedpammer merged 2 commits into
mainfrom
azure-devops-as-a-pull-request-host
Sep 14, 2026
Merged

Read a pull request from Azure DevOps as well as from GitHub#13
siegfriedpammer merged 2 commits into
mainfrom
azure-devops-as-a-pull-request-host

Conversation

@siegfriedpammer

Copy link
Copy Markdown
Member

Every pull-request fact a review needs came from GitHubService directly, so a repository hosted anywhere else could not be reviewed. This introduces IPullRequestHost, decided once per workspace from origin's URL (STAMPEDED_PR_HOST=github|azdo overrides), with two implementations: GitHubService over gh and AzureDevOpsService over az and its azure-devops extension.

  • GitHub's vocabulary stays the model's vocabulary; Azure DevOps maps onto it inside its own implementation. Panes show HostName instead of the word "GitHub".
  • The fetch refspec comes from the host (Azure DevOps has refs/pull/N/merge but no /head).
  • A check carries the run its failed log is fetched by, instead of the Checks pane scraping a GitHub Actions URL.
  • ExternalTool.RunAsync turns an executable that never started into a ToolFailedException, so a machine without az reports it instead of loading forever.
  • Refused with a reason on Azure DevOps: PRs from forks, line totals and check state in the PR list, server-side branch update.

Also: untracked files are left out of the uncommitted-work step - build output and scratch files are not part of a change.

🤖 Generated with Claude Code

siegfriedpammer and others added 2 commits September 11, 2026 13:32
Every pull-request fact a review needs came from GitHubService, a wrapper over
gh that eight UI files called directly, so a repository hosted anywhere else
could not be reviewed at all. Two implementations is what justifies an
interface: IPullRequestHost, decided once per workspace from origin's URL, and
the UI stops knowing which host answered.

GitHub's vocabulary stays the vocabulary of the model - APPROVE /
REQUEST_CHANGES, APPROVED / CHANGES_REQUESTED, LEFT / RIGHT, MERGEABLE /
BLOCKED. The panes and the pure functions under them already speak it, and six
test files pin it; Azure DevOps, which counts votes from 10 to -10 and has no
review object at all, maps onto it inside its own implementation and nowhere
else. What a pane shows the reader comes from HostName instead of the word
"GitHub", in code and in 35 XAML strings.

The new host talks to az and its azure-devops extension, with az devops invoke
for the REST surface the extension has no verb for - threads, iterations, build
logs, the completion PATCH. Auth rides on az login the way GitHub's rides on gh
auth, so CLAUDE.md's "everything external is a CLI" holds; it names one more
CLI. The project is named on the commands that take it and left off the ones
addressed by pull-request id, which reject it.

Three things the shape of the change forced:

- A refspec now comes from the host. Azure DevOps advertises refs/pull/N/merge
  but no /head, so the source branch is fetched into the same local ref and
  everything keyed on it - the cache, the review store - is untouched.
- A check carries the run its failed log is fetched by, rather than the Checks
  pane scraping GitHub Actions' URL out of a link. A host that names a build
  another way fills the same field.
- ExternalTool.RunAsync turns an executable that never started into a
  ToolFailedException. Without it a machine with no az left the pull-request
  pane loading forever on a Win32Exception no caller catches - and every caller
  is written to report a command that failed.

Not on Azure DevOps, refused with a reason rather than hidden: pull requests
from forks, line totals and check state in the list (a call per row), and the
server-side branch update, for which there is no API.

Assisted-by: Claude:claude-opus-5:Claude Code

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KQpLoBSiUdCcGT2gYWbBDm
A review reads a change, and an untracked file is not part of one: build
output, a scratch file, a local config sit next to the work in every real
clone, and reading them as additions buries the change under them. The
working-tree diff now reports only what git tracks.

IsDirtyAsync goes with it. It decides whether a checkout contributes an
uncommitted step at all, so leaving untracked files in it would leave a
clone with nothing but build output in it offering an empty step.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VyPi76bQi4vTUCYT5Ebt3D
@siegfriedpammer
siegfriedpammer merged commit 863b8a9 into main Sep 14, 2026
3 checks passed
@siegfriedpammer
siegfriedpammer deleted the azure-devops-as-a-pull-request-host branch September 14, 2026 10:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant