Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/ISSUE_TEMPLATE/bug-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ body:
label: Version with bug
description: In what version do you see this issue? Run `dotnet workload list` to find your version.
options:
- 11.0.0-preview.4
- 11.0.0-preview.3
- 11.0.0-preview.2
- 11.0.0-preview.1
Expand Down Expand Up @@ -171,6 +172,7 @@ body:
- 11.0.0-preview.1
- 11.0.0-preview.2
- 11.0.0-preview.3
- 11.0.0-preview.4
validations:
required: true
- type: dropdown
Expand Down
8 changes: 7 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,9 +331,15 @@ Skills are modular capabilities that can be invoked directly or used by agents.
- **Categories**: Build, WindowsTemplates, macOSTemplates, Blazor, MultiProject, Samples, AOT, RunOnAndroid, RunOniOS
- **Note**: **ALWAYS use this skill** instead of manual `dotnet test` commands for integration tests

11. **dependency-flow** (`.github/skills/dependency-flow/SKILL.md`)
- **Purpose**: MAUI-specific dependency flow rules, channel conventions, and feed lookup workflows
- **Trigger phrases**: "feeds for .NET MAUI X.Y.Z", "where is MAUI build", "promote build to public feed", "what channels is MAUI on", "subscription health for MAUI"
- **Wraps**: `maestro-cli` skill (from `dotnet-dnceng@dotnet-arcade-skills` plugin) and maestro MCP tools
- **Note**: Provides MAUI-specific guardrails on top of core Maestro/darc operations — channel naming, safety deny-list, input validation, and prompt injection defense

#### Internal Skills (Used by Agents)

11. **try-fix** (`.github/skills/try-fix/SKILL.md`)
12. **try-fix** (`.github/skills/try-fix/SKILL.md`)
- **Purpose**: Proposes ONE independent fix approach, applies it, tests, records result with failure analysis, then reverts
- **Used by**: pr agent Phase 3 (Fix phase) - rarely invoked directly by users
- **Behavior**: Reads prior attempts to learn from failures. Max 5 attempts per session.
Expand Down
225 changes: 225 additions & 0 deletions .github/docs/trigger-azdo-pipeline-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
# Triggering Azure DevOps Pipelines from GitHub Actions (No PAT)

This guide explains how to invoke Azure DevOps pipelines (e.g. in **dnceng-public** or **DevDiv**)
from GitHub Actions using **OIDC federated credentials** — no PAT or stored secrets needed.

## Architecture

```
GitHub Actions ──► GitHub OIDC Provider ──► Azure AD (federated credential) ──► AzDO REST API
(JWT id-token) (exchange for bearer token) (Run Pipeline)
```

1. The workflow requests an OIDC JWT from GitHub's token endpoint
2. The JWT is exchanged with Azure AD via the managed identity's federated credential
3. Azure AD returns a bearer token scoped to Azure DevOps
4. The bearer token is used to call the AzDO REST API to trigger the pipeline

> **Important:** The `azure/login` GitHub Action may be **blocked by org policy**
> (e.g. in the `dotnet` org). The workflow uses **manual OIDC token exchange via
> `curl`** instead, which works everywhere that `id-token: write` is allowed.

## Prerequisites

- Azure CLI installed locally (for one-time setup)
- Access to an Azure subscription + resource group
- **Project Collection Administrator** (or delegated) access in the target AzDO org to add users
- GitHub repo admin access to configure secrets

---

## Step 1: Create a User-Assigned Managed Identity

```bash
# Choose your resource group and identity name
RG="rg-maui-automation"
IDENTITY_NAME="id-maui-azdo-trigger"
LOCATION="eastus"

# Create the resource group if it doesn't exist
az group create --name $RG --location $LOCATION

# Create the managed identity
az identity create --name $IDENTITY_NAME --resource-group $RG --location $LOCATION

# Capture the IDs you'll need
CLIENT_ID=$(az identity show --name $IDENTITY_NAME --resource-group $RG --query clientId -o tsv)
PRINCIPAL_ID=$(az identity show --name $IDENTITY_NAME --resource-group $RG --query principalId -o tsv)
TENANT_ID=$(az account show --query tenantId -o tsv)
SUBSCRIPTION_ID=$(az account show --query id -o tsv)

echo "CLIENT_ID: $CLIENT_ID"
echo "PRINCIPAL_ID: $PRINCIPAL_ID"
echo "TENANT_ID: $TENANT_ID"
echo "SUBSCRIPTION_ID: $SUBSCRIPTION_ID"
```

## Step 2: Add OIDC Federated Credential for GitHub Actions

This lets GitHub Actions authenticate as the identity without storing any secrets.

> **Critical: Subject claim is CASE-SENSITIVE.** The GitHub username/org in the
> subject must match the exact casing used by GitHub (e.g. `JanKrivanek` not
> `jankrivanek`). A mismatch produces `AADSTS70021`.

> **Microsoft tenant restriction:** For managed identities in the Microsoft
> corporate tenant (`72f988bf-...`), the OIDC token must include an `enterprise`
> claim with value `microsoft`, `github`, or `microsoftopensource`. Personal forks
> outside these GitHub Enterprise orgs will fail with `AADSTS7002381`.
> This means **only repos in `dotnet`, `microsoft`, etc. orgs work** — not personal forks.

```bash
# Allow from main branch
az identity federated-credential create \
--name github-actions-main \
--identity-name $IDENTITY_NAME \
--resource-group $RG \
--issuer "https://token.actions.githubusercontent.com" \
--subject "repo:dotnet/maui:ref:refs/heads/main" \
--audiences "api://AzureADTokenExchange"
```

> **Subject claim mapping:** The OIDC token's `sub` claim is what Azure AD matches
> against the `--subject` parameter. For `issue_comment` events (like the `/review`
> command), the workflow runs from the default branch, so the subject is
> `repo:dotnet/maui:ref:refs/heads/main`. For `pull_request` events, the subject
> would be `repo:dotnet/maui:pull_request`. This is why the case-sensitivity
> warning above is critical — the `sub` claim value must match exactly.

Add more federated credentials for other branches or trigger types as needed:

```bash
# Specific dev branch
az identity federated-credential create \
--name github-actions-dev-branch \
--identity-name $IDENTITY_NAME \
--resource-group $RG \
--issuer "https://token.actions.githubusercontent.com" \
--subject "repo:dotnet/maui:ref:refs/heads/dev/myteam/feature" \
--audiences "api://AzureADTokenExchange"

# Pull request events
az identity federated-credential create \
--name github-actions-pr \
--identity-name $IDENTITY_NAME \
--resource-group $RG \
--issuer "https://token.actions.githubusercontent.com" \
--subject "repo:dotnet/maui:pull_request" \
--audiences "api://AzureADTokenExchange"

# GitHub environment (recommended for production — enables approval gates)
az identity federated-credential create \
--name github-actions-env-azdo \
--identity-name $IDENTITY_NAME \
--resource-group $RG \
--issuer "https://token.actions.githubusercontent.com" \
--subject "repo:dotnet/maui:environment:azdo-trigger" \
--audiences "api://AzureADTokenExchange"
```

## Step 3: Add the Identity to Azure DevOps

The managed identity must be added as a user in **each** AzDO organization you want to trigger pipelines in.

### Adding the identity

1. Go to the AzDO org → **Organization Settings** → **Users**
2. Click **Add users**
3. Search for the managed identity by its **display name**
4. Set **Access level** to **Basic** (see note below)
5. Add the user to the target project
6. Click **Add**

> **Critical: Access level must be Basic, not Stakeholder.** Stakeholder access
> does not grant sufficient permissions for build operations. Even with explicit
> "Queue builds" permissions, Stakeholder-level identities get `TF215106: Access
> denied` errors. Request **Basic** access when filing the request.

> **Important:** Use the identity's **Object (Principal) ID** from the
> **Enterprise Applications** pane in Entra admin center — NOT the App
> Registration object ID.

### Grant Build Queue Permission

The identity needs **"Queue builds"** permission on the target pipeline(s):

1. Go to the project → **Pipelines** → find the target pipeline
2. Click the **⋮** menu → **Manage security**
3. Find your managed identity user
4. Set **"Queue builds"** to **Allow**

### Per-organization requirements

| AzDO Organization | Project | Example Pipelines |
|---|---|---|
| `dnceng-public` | `public` | 302 (maui-pr), 314 (maui-pr-devicetests) |
| `DevDiv` | `DevDiv` | 27723 |

## Step 4: Set GitHub Repository Secrets

In **dotnet/maui** → **Settings** → **Secrets and variables** → **Actions**, add:

| Secret Name | Value |
|---|---|
| `AZDO_TRIGGER_CLIENT_ID` | The managed identity's Client ID |
| `AZDO_TRIGGER_TENANT_ID` | Your Azure AD Tenant ID |
| `AZDO_TRIGGER_SUBSCRIPTION_ID` | Your Azure Subscription ID |

> Using distinct secret names (prefixed with `AZDO_TRIGGER_`) avoids conflicts
> with any existing `AZURE_*` secrets in the repo.

## Step 5: Create the GitHub Actions Workflow

See [`.github/workflows/review-trigger.yml`](../workflows/review-trigger.yml) for a ready-to-use workflow.

## How It Works (Token Flow)

```
1. Workflow declares `permissions: { id-token: write }` at job level
2. Step 1 requests an OIDC JWT from GitHub's token endpoint via
$ACTIONS_ID_TOKEN_REQUEST_URL (audience: api://AzureADTokenExchange)
3. Step 2 sends the JWT to Azure AD token endpoint as a client_assertion
(grant_type=client_credentials) for the managed identity's client_id
4. Azure AD validates the JWT against the federated credential and returns
a bearer token scoped to AzDO (resource: 499b84ac-1321-427f-aa17-267ca6975798)
5. Step 3 calls POST dev.azure.com/{org}/{project}/_apis/pipelines/{id}/runs
with the bearer token
6. AzDO validates the token, checks the identity's permissions, and queues the build
```

> **Why not `azure/login`?** The `dotnet` GitHub org restricts which third-party
> Actions can run. `azure/login@v3` causes `startup_failure` because it's not in
> the org's allowed actions list. The manual `curl`-based OIDC exchange achieves
> the same result without any third-party dependencies.

## Troubleshooting

| Symptom | Cause | Fix |
|---------|-------|-----|
| `startup_failure` (no logs at all) | Third-party Action blocked by org policy | Don't use `azure/login`. Use manual `curl`-based OIDC exchange. |
| `AADSTS70021: No matching federated identity record found` | Subject claim case mismatch | Federated credential subject is **case-sensitive**. Use exact GitHub username casing (e.g. `JanKrivanek` not `jankrivanek`). |
| `AADSTS7002381: ... enterprise claim ... actual value is ''` | Personal fork outside GitHub Enterprise | Microsoft tenant requires `enterprise` claim. Only repos in `dotnet`, `microsoft`, etc. GitHub Enterprise orgs work. |
| `TF215106: Access denied. <name> needs Queue builds permissions` | Stakeholder access level or missing permission | Upgrade identity to **Basic** access (not Stakeholder). Verify "Queue builds" is explicitly allowed on the pipeline. |
| `TF401444: Sign-in required` | Identity not added to AzDO org | Add the MI as a user in the AzDO Organization Settings → Users. |
| `403` from AzDO REST API | Missing permissions | Ensure the identity has "Queue builds" on the specific pipeline AND Basic access level. |
| `OIDC environment variables not available` | Missing `id-token: write` permission | Add `permissions: { id-token: write }` at the **job** level (not workflow level). |
| `Failed to get Azure AD token` | Wrong client_id/tenant_id or federated credential mismatch | Verify secrets match the MI's Client ID and Tenant ID. Check federated credential subject matches the actual OIDC claim. |

## Lessons Learned

1. **`azure/login` Action is blocked** in the `dotnet` GitHub org — use manual
`curl`-based OIDC token exchange instead.
2. **Federated credential subjects are case-sensitive** — `JanKrivanek` ≠
`jankrivanek`. Always verify exact GitHub username/org casing.
3. **Microsoft tenant requires GitHub Enterprise membership** — personal forks
fail with `AADSTS7002381`. Only repos in enterprise-managed orgs work.
4. **Stakeholder access is insufficient** — even with explicit "Queue builds"
permissions, Stakeholder-level identities get `TF215106`. Request Basic.
5. **Add identity to EACH AzDO org separately** — permissions in `dnceng-public`
don't carry over to `DevDiv` and vice versa.

## References

- [Use service principals and managed identities in Azure DevOps](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity)
- [AzDO Pipelines REST API — Run Pipeline](https://learn.microsoft.com/en-us/rest/api/azure/devops/pipelines/runs/run-pipeline?view=azure-devops-rest-7.1)
- [GitHub OIDC token docs](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
82 changes: 82 additions & 0 deletions .github/skills/agentic-labeler/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
name: agentic-labeler
description: >-
Labels issues and pull requests in the dotnet/maui repository based on
technical content, area-matching rules, and platform-file conventions.
Used by the gh-aw agentic-labeler workflow and available for batch
evaluation and interactive Copilot CLI usage.
metadata:
author: dotnet-maui
version: "1.0"
---

# Agentic Labeler

Labeling rules for the [dotnet/maui](https://github.com/dotnet/maui) repository. These rules are the canonical source of truth for how issues and PRs should be labeled. They are consumed by the `agentic-labeler` gh-aw workflow and can also be used standalone for batch evaluation or interactive labeling.

## Label discovery

- Fetch the current list of labels using the `list_label` MCP tool (provided by the `labels` toolset). Note the **singular** name — it is `list_label`, not `list_labels`.
- **Important pagination caveat:** the `list_label` tool only returns the first ~100 labels (no pagination). This repo has ~440 labels, so many `area-*`, `platform/*`, and status labels will be missing from the listing. If you have a strong candidate label name in mind that isn't in the listing, **verify it exists** with the `get_label` tool before adding it. The label families enumerated below (`area-*`, `platform/*`, `t/*`, `s/*`, `i/*`, `p/*`) are reliable guides; use `get_label` for anything else.
- You may apply **any** existing label, not just `area-*` and `platform/*`. Examples of other useful label families that exist in this repo (with **exact** names — emoji suffixes are part of the label and must be matched verbatim):
- **Kind:** `t/bug`, `t/enhancement ☀️`, `t/docs 📝`, `t/breaking 💥`, `t/native-embedding`, `t/desktop`, `t/a11y`
- **Status / signal (issues):** `i/regression`, `s/needs-repro`, `s/needs-info`, `s/needs-attention`, `s/duplicate 2️⃣`, `s/no-repro`, `s/not-a-bug`
- **Priority:** `p/0`, `p/1`, `p/2`, `p/3`
- **PR-specific status caveat:** **do not** apply `s/needs-info` or `s/needs-repro` to pull requests — repo automation rewrites or removes them and posts a comment. On PRs, use `s/pr-needs-author-input` instead when more information is needed.
- Do **not** create new labels. Only labels that already exist in the repository will be accepted.

## Labeling rules

### `area-*` labels (issues and PRs)

Pick one or more `area-*` labels based on the subject matter:

- Specific control mentioned → matching `area-controls-<name>` (e.g., `CollectionView` → `area-controls-collectionview`, `Entry` → `area-controls-entry`).
- Layout, measure/arrange, sizing issues → `area-layout`.
- Navigation, Shell routing, page navigation → `area-navigation` (or `area-controls-shell` when Shell-specific).
- XAML parsing, markup extensions, XamlC, source generators → `area-xaml`.
- Hot reload, build, MSBuild, workload, project templates, tooling → `area-tooling`, `area-templates`, or `area-setup` as appropriate.
- BlazorWebView / Blazor hybrid → `area-blazor`.
- Essentials APIs (non-UI: connectivity, sensors, preferences, etc.) → `area-essentials`.
- Drawing / Microsoft.Maui.Graphics → `area-drawing`.
- Gestures (tap, pan, swipe, pinch) → `area-gestures`.
- Lifecycle, hosting, app startup, DI → `area-core-lifecycle` / `area-core-hosting`.
- Dispatcher / main thread / threading → `area-core-dispatching`.
- Localization / RTL / culture → `area-localization`.
- Docs only → `area-docs`.

Prefer the most specific label. It is fine to apply both a generic and a specific area label (e.g., `area-layout` + `area-controls-collectionview`) when both clearly apply.

### `platform/*` labels

This is the most important behavior for PRs.

**For pull requests**, infer `platform/*` labels primarily from the **changed files**, using the rules below. Each rule maps a file pattern to one or more platform labels. Apply a `platform/*` label if **any** changed file matches that pattern. The path patterns intentionally target the established MAUI source-layout conventions (`Platform/<Name>/` and `Platforms/<Name>/`) — do not match on bare `/Android/`, `/iOS/`, `/Windows/`, etc., as those occur in templates, docs, and unrelated tooling paths.

Note on iOS / MacCatalyst: file-extension patterns and directory patterns map differently because of MAUI's compilation conventions — they are split into separate rows below.

| File pattern (changed in the PR) | Label(s) to apply |
| --- | --- |
| `*.android.cs`, `*.Android.cs`, paths containing `/Platform/Android/`, `/Platforms/Android/`, `/AndroidNative/` | `platform/android` |
| `*.ios.cs`, `*.iOS.cs` (file-extension pattern — these compile for **both** iOS and MacCatalyst) | `platform/ios` **and** `platform/macos` |
| Paths containing `/Platform/iOS/` or `/Platforms/iOS/` (directory pattern — these compile **only** for the iOS TFM) | `platform/ios` only |
| `*.maccatalyst.cs`, `*.MacCatalyst.cs`, paths containing `/Platform/MacCatalyst/`, `/Platforms/MacCatalyst/` | `platform/macos` |
| `*.windows.cs`, `*.Windows.cs`, paths containing `/Platform/Windows/`, `/Platforms/Windows/` | `platform/windows` |
| `*.tizen.cs`, paths containing `/Platform/Tizen/`, `/Platforms/Tizen/` | `platform/tizen` |

Notes:

- If a PR touches **only shared / cross-platform code** (e.g., `src/Core/src/*.cs` without a platform suffix, or `src/Controls/src/Core/`), do **not** apply any `platform/*` label.
- If a PR touches **multiple platforms**, apply each matching `platform/*` label.
- `.ios.cs` files compile for both iOS and MacCatalyst (see split table rows above).
- `.maccatalyst.cs` files do **not** compile for iOS — apply only `platform/macos` for those.

**For issues**, infer `platform/*` labels only if the reporter clearly indicates a platform (explicit mention of Android / iOS / macOS / Windows / Tizen in the title, body, or attached logs/stack traces). Do not guess. If the report says "all platforms" or doesn't specify, apply no `platform/*` label.

### What NOT to do

- Do **not** create new labels — apply only labels that already exist in the repository.
- Do **not** add `platform/*` labels to PRs that don't touch platform-specific files.
- Do **not** post a comment summarizing the labels — labels speak for themselves.
- Do **not** close, lock, or otherwise modify the issue/PR beyond labeling.
- Be conservative; precision beats recall. Only apply labels that clearly fit.
Loading
Loading