# Azure DevOps REST API Reference for vcpkg CI

## Base URL

```
https://dev.azure.com/vcpkg/public/_apis
```

All endpoints below are appended to this base. The `vcpkg/public` project is **publicly readable** — no Authorization header is required.

## API Version

Always append `?api-version=7.0` (or include it in query strings).

---

## Build Endpoints

### Get Build Details

```
GET https://dev.azure.com/{org}/{project}/_apis/build/builds/{buildId}?api-version=7.0
```

**Response fields:**
```json
{
  "id": 129315,
  "buildNumber": "20240315.1",
  "status": "completed",
  "result": "failed",
  "reason": "pullRequest",
  "sourceBranch": "refs/pull/12345/merge",
  "sourceVersion": "abc123def456...",
  "requestedFor": { "displayName": "Jane Developer" },
  "startTime": "2024-03-15T10:00:00Z",
  "finishTime": "2024-03-15T14:30:00Z",
  "definition": { "name": "vcpkg.CI" },
  "_links": {
    "web": { "href": "https://dev.azure.com/vcpkg/public/_build/results?buildId=129315" }
  }
}
```

**`result` values:** `succeeded` | `failed` | `partiallySucceeded` | `canceled`
**`reason` values:** `pullRequest` | `schedule` | `manual` | `individualCI` | `batchedCI`

---

### Get Build Timeline

```
GET https://dev.azure.com/{org}/{project}/_apis/build/builds/{buildId}/timeline?api-version=7.0
```

Returns all stages, jobs, tasks and their results.

**Response structure:**
```json
{
  "records": [
    {
      "id": "guid",
      "parentId": "parent-guid-or-null",
      "type": "Job",
      "name": "x64_windows",
      "displayName": "x64_windows",
      "state": "completed",
      "result": "failed",
      "startTime": "2024-03-15T10:05:00Z",
      "finishTime": "2024-03-15T12:15:00Z",
      "log": {
        "id": 42,
        "type": "Container",
        "url": "https://dev.azure.com/vcpkg/public/_apis/build/builds/129315/logs/42"
      },
      "errorCount": 1,
      "warningCount": 0
    }
  ]
}
```

**`type` values:** `Stage` | `Phase` | `Job` | `Task`
**`result` values:** `succeeded` | `failed` | `skipped` | `canceled` | `succeededWithIssues`

**To find failed jobs:**
```python
failed_jobs = [r for r in records if r['type'] == 'Job' and r['result'] == 'failed']
```

**Job names map to triplets** (underscores in job names, hyphens in triplets):
- `x64_windows` → `x64-windows`
- `x64_windows_static` → `x64-windows-static`
- `arm64_osx` → `arm64-osx`
- `x64_linux` → `x64-linux`
- `arm_neon_android` → `arm-neon-android`

---

### Get a Specific Log

```
GET https://dev.azure.com/{org}/{project}/_apis/build/builds/{buildId}/logs/{logId}?api-version=7.0
```

Returns plain text of the log. Use this to read the "*** Test Modified Ports" task log, which contains `REGRESSION:` lines for **all** failures including those without artifact logs (e.g., `FILE_CONFLICTS`).

**Finding the right log ID:** Look up the task record in the timeline response — the `log.id` field on the `"*** Test Modified Ports"` task record gives the log ID. Alternatively, use `log.url` directly.

**Scanning for REGRESSION lines:**
```powershell
$logText = Invoke-RestMethod "https://dev.azure.com/{org}/{project}/_apis/build/builds/{buildId}/logs/{logId}?api-version=7.0"
$logText -split "`n" | Where-Object { $_ -match '^REGRESSION:' }
```

**Example output:**
```
REGRESSION: kf6i18n:x64-windows failed with FILE_CONFLICTS. If expected, add kf6i18n:x64-windows=fail to .../ci.baseline.txt.
REGRESSION: kf6itemmodels:x64-windows failed with FILE_CONFLICTS. If expected, add kf6itemmodels:x64-windows=fail to .../ci.baseline.txt.
REGRESSION: v8:x64-windows failed with BUILD_FAILED. If expected, add v8:x64-windows=fail to .../ci.baseline.txt.
```

> ⚠️ **`FILE_CONFLICTS` failures do NOT produce failure log artifacts.** The only record of them is in this step log. Always scan all "Test Modified Ports" task logs before relying on artifacts alone.

### List All Logs

```
GET https://dev.azure.com/{org}/{project}/_apis/build/builds/{buildId}/logs?api-version=7.0
```

---

## Artifact Endpoints

### List All Artifacts

```
GET https://dev.azure.com/{org}/{project}/_apis/build/builds/{buildId}/artifacts?api-version=7.0
```

**Response:**
```json
{
  "count": 39,
  "value": [
    {
      "id": 1107682,
      "name": "failure logs for x64-windows",
      "source": "7922e5c4-...",
      "resource": {
        "type": "PipelineArtifact",
        "data": "DCCD3D9C96CE79A758B255BD87397A07371B8394EE52F6BFF35346C144D63F3501",
        "properties": {
          "RootId": "196FDE3B...",
          "artifactsize": "8586850",
          "HashType": "DEDUP1024K",
          "DomainId": "0"
        },
        "url": "https://dev.azure.com/vcpkg/.../_apis/build/builds/129315/artifacts?artifactName=failure%20logs%20for%20x64-windows&api-version=7.0",
        "downloadUrl": "https://artprodwus21.artifacts.visualstudio.com/.../_apis/artifact/{base64hash}/content?format=zip"
      }
    }
  ]
}
```

> ⚠️ **`resource.type` is `"PipelineArtifact"`**, not `"Container"`. The Container listing API
> (`GET /_apis/resources/Containers/{id}`) does **not** work for these artifacts.
> Use `resource.downloadUrl` directly to download the ZIP.

**Artifact naming conventions:**
| Artifact Name Pattern | Contents | Priority |
|---|---|---|
| `failure logs for {triplet}` | Per-port build failure log directories | **High** — primary source |
| `file lists for {triplet}` | Installed file manifests | Low — skip unless needed |
| `format.diff` | Formatting/manifest diff | **Medium** — indicates PR format issues |
| `z azcopy logs for {triplet}` | Binary cache transfer logs | Low — infrastructure only |

---

### Download Artifact ZIP (Confirmed Working)

Use the `downloadUrl` from the artifact listing. It resolves to an `artprodwus21.artifacts.visualstudio.com` URL. **`web_fetch` cannot download binary ZIPs** — use PowerShell:

```powershell
# $downloadUrl = value from resource.downloadUrl in the artifacts list response
Invoke-WebRequest -Uri $downloadUrl -OutFile "$tmpDir\{triplet}.zip" -UseBasicParsing
Expand-Archive "$tmpDir\{triplet}.zip" -DestinationPath "$tmpDir\{triplet}" -Force

# List failing ports — each is a subdirectory inside the artifact folder
$root = "$tmpDir\{triplet}\failure logs for {triplet}"
Get-ChildItem $root -Directory | Select-Object -ExpandProperty Name
```

The ZIP always contains one top-level folder named exactly `"failure logs for {triplet}"`.
Inside that folder, each **failing port has its own subdirectory** named after the port.

> ❌ The Container API (`GET /_apis/resources/Containers/{containerId}`) does **not** work
> for `PipelineArtifact` type. Do not attempt to use it.

---

## Test Results Endpoints

### List Test Runs for a Build

```
GET https://dev.azure.com/{org}/{project}/_apis/test/runs?buildId={buildId}&api-version=7.0
```

**Response:**
```json
{
  "value": [
    {
      "id": 9876,
      "name": "x64-windows",
      "state": "Completed",
      "totalTests": 500,
      "passedTests": 497,
      "failedTests": 3
    }
  ]
}
```

### Get Test Run Results

```
GET https://dev.azure.com/{org}/{project}/_apis/test/runs/{runId}/results?api-version=7.0&outcomes=Failed
```

Filter by `outcomes=Failed` to get only failures.

**Response includes:**
- `testCaseName` — usually `portname:triplet`
- `outcome` — `Failed`
- `errorMessage` — short error description
- `stackTrace` — full failure output

---

## URL Parsing

Given `https://dev.azure.com/vcpkg/public/_build/results?buildId=129315&view=results`:

| Component | Value | How to Extract |
|-----------|-------|----------------|
| Organization | `vcpkg` | 4th path segment |
| Project | `public` | 5th path segment |
| Build ID | `129315` | `buildId` query parameter |

```python
# Python example
from urllib.parse import urlparse, parse_qs
url = "https://dev.azure.com/vcpkg/public/_build/results?buildId=129315&view=results"
parts = urlparse(url)
segments = parts.path.strip('/').split('/')
org = segments[0]       # "vcpkg"
project = segments[1]   # "public"
build_id = parse_qs(parts.query)['buildId'][0]  # "129315"
```

---

## Rate Limits and Auth

- **Public projects** (like `vcpkg/public`): No authentication needed; anonymous access is allowed
- **Rate limit**: ~200 requests per minute for anonymous; increase by adding a PAT as Bearer token if needed
- **PAT header**: `Authorization: Basic {base64(":pat_token")}`
