Skip to content

Latest commit

 

History

History
959 lines (819 loc) · 44.3 KB

File metadata and controls

959 lines (819 loc) · 44.3 KB

Agent File Format (Markdown + YAML Front Matter)

Part of the ado-aw documentation.

Input Format (Markdown with Front Matter)

The compiler expects markdown files with YAML front matter similar to gh-aw:

---
name: "name for this agent"
description: "One line description for this agent"
target: standalone # Optional: "standalone" (default), "1es", "job", or "stage". See docs/targets.md.
engine: copilot # Engine identifier. Defaults to copilot. Currently only 'copilot' (GitHub Copilot CLI) is supported.
# engine:                        # Alternative object format (with additional options)
#   id: copilot
#   model: claude-opus-4.7
#   timeout-minutes: 30
workspace: repo # Optional: "root", "repo" (alias: "self"), or a checked-out repository alias. If not specified, defaults to "root" when no additional repositories are listed in `repos:`, and to "repo" when one or more additional repos are checked out. See "Workspace Defaults" below.
pool:                          # Optional pool configuration
  vmImage: ubuntu-22.04        # Microsoft-hosted (default for non-1ES targets)
# pool:                        # Self-hosted pool
#   name: MySelfHostedPool
#   demands:                   # Optional ordered Azure Pipelines demands
#     - CustomCapability -equals required-value
# pool:                        # 1ES pool format
#   name: AZS-1ES-L-MMS-ubuntu-22.04
#   os: linux                  # Operating system: "linux" or "windows". Defaults to "linux".
# pool:                        # Pool with per-job overrides (see "Per-Job Pool Overrides" below)
#   name: SpecializedPool
#   overrides:
#     detection:
#       vmImage: ubuntu-22.04
#     safe-outputs:
#       vmImage: ubuntu-22.04
#     conclusion:
#       vmImage: ubuntu-22.04
repos:                           # compact repository declarations (replaces repositories: + checkout:)
  - MyProject/my-repo               # shorthand: alias="my-repo", type=git, ref=refs/heads/main, checkout=true
  - reponame=MyProject/another-repo # shorthand with explicit alias
  - name: octo/templates           # object form for an external repository
    type: github                 # external repo resource type; default is git
    ref: refs/heads/release/2.x
    checkout: false              # declared as resource only, not checked out by the agent
    endpoint: github-templates   # required for type: github/githubenterprise/bitbucket
imports:                         # reusable markdown components; see docs/imports.md
  - ./components/local-guidance.md
  - uses: octo/shared/components/notify.md@0123456789abcdef0123456789abcdef01234567
    endpoint: github-shared-components
    with:                        # non-secret import-schema inputs
      channel: service-alerts
tools:                         # optional tool configuration
  bash: ["cat", "ls", "grep"]  # explicit bash allow-list; when omitted, all bash tools are allowed (unrestricted)
  edit: true                   # enable file editing tool (default: true)
  cache-memory: true           # persistent memory across runs (see docs/tools.md)
  # cache-memory:              # Alternative object format (with options)
  #   allowed-extensions: [.md, .json]
  azure-devops: true           # first-class ADO MCP integration (see docs/tools.md)
  # azure-devops:              # Alternative object format (with scoping)
  #   version: "2.8.1"         # Optional exact-semver override; defaults to compiler pin
  #   toolsets: [repositories, work-items]
  #   allowed: [wit_get_work_item]
  #   org: myorg
runtimes:                      # optional runtime configuration (language environments)
  lean: true                   # Lean 4 theorem prover (see docs/runtimes.md)
  # lean:                      # Alternative object format (with toolchain pinning)
  #   toolchain: "leanprover/lean4:v4.29.1"
  # python: true               # Python runtime — auto-installs via UsePythonVersion@0 (see docs/runtimes.md)
  # python:                    # Alternative object format (pin version, configure internal feed)
  #   version: "3.12"
  #   feed-url: "https://pkgs.dev.azure.com/myorg/_packaging/myfeed/pypi/simple/"
  # node: true                 # Node.js runtime — auto-installs via UseNode@1 (see docs/runtimes.md)
  # node:                      # Alternative object format (pin version, configure internal feed)
  #   version: "22.x"
  #   feed-url: "https://pkgs.dev.azure.com/ORG/PROJECT/_packaging/FEED/npm/registry/"
  # dotnet: true               # .NET runtime — auto-installs via UseDotNet@2 (see docs/runtimes.md)
  # dotnet:                    # Alternative object format (pin version, configure internal feed via nuget.config)
  #   version: "8.0.x"          # use "global.json" to pin from the repo's global.json
  #   feed-url: "https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v3/index.json"
# env:                         # workflow-level environment variables (accepted by parser, not yet forwarded to compiled pipeline output)
#   CUSTOM_VAR: "value"
# inlined-imports: false        # When true, resolve {{#runtime-import ...}} markers at compile time
#                               # (default: false — markers are resolved at pipeline runtime, so
#                               # prompt-body edits do not require recompilation).
#                               # See docs/runtime-imports.md for full details.
mcp-servers:
  my-custom-tool:              # containerized MCP server (requires container field)
    enabled: true
    container: "node:20-slim"
    entrypoint: "node"
    entrypoint-args: ["path/to/mcp-server.js"]
    args: ["--pull=always"]     # Docker runtime args inserted before the image
    mounts: ["$(Build.SourcesDirectory):/workspace:ro"]
    env:
      CUSTOM_TOKEN: ""          # empty string = pass through from pipeline env
    allowed:
      - custom_function_1
      - custom_function_2
  remote-tool:                  # HTTP MCP server (see docs/mcp.md)
    url: "https://mcp.example.com"
    headers:
      Authorization: "Bearer $(MCP_TOKEN)"
    allowed: [search, fetch]
safe-outputs:                  # optional per-tool configuration for safe outputs
  staged: false               # cooperative preview default; per-tool override supported
  create-work-item:
    work-item-type: Task
    description-field: System.Description
    tags:
      - automated
      - agent-created
    artifact-link:             # optional: link work item to repository branch
      enabled: true
      branch: main
  assign-work-item:
    target: "*"               # required only for numeric pre-existing IDs
    allowed: ["user@example.com"]
    blocked: ["svc-*"]
  jobs:                       # custom Agent-callable jobs (see docs/safe-outputs.md)
    send-notification:
      description: Notify release operators.
      max: 1
      output: Notification proposal accepted.
      inputs:
        title:
          description: Notification title.
          type: string
          required: true
      env:
        NOTIFICATION_TOKEN: $(SHARED_NOTIFICATION_TOKEN)
      steps:
        - bash: jq -e '.items[] | select(.type == "send-notification")' "$ADO_AW_AGENT_OUTPUT"
  threat-detection:            # section-level Detection configuration
    enabled: true              # boolean only; false keeps a pass-through Detection job
    prompt: |                  # appended to the fixed detector prompt
      Focus on unsafe deserialization and authentication bypasses.
    engine:                    # optional overlay on top-level engine:
      model: gpt-5-mini        # currently only the copilot engine ID is supported
    steps:                     # trusted ADO steps before AI analysis
      - bash: echo "Prepare detector"
    post-steps:                # trusted ADO steps after AI analysis
      - bash: echo "Run additional scanner"
on:                            # trigger configuration (unified under on: key)
                               # `on:` is the COMPLETE declaration of when this
                               # pipeline runs. Omitting it entirely produces a
                               # manual / API-queued-only pipeline.
                               # See "Push (CI) Triggering" below.
  push:                        # optional: start on pushes (ADO `trigger:`)
    branches:                  #   `push: none` never starts on a push
      include: [main]
      exclude: [wip/*]
    paths:
      include: ["src/**"]
      exclude: ["docs/**"]
  schedule: daily around 14:00 # fuzzy schedule - see docs/schedule-syntax.md
  pipeline:
    name: "Build Pipeline"     # source pipeline name
    project: "OtherProject"    # optional: project name if different
    branches:                  # optional: branches to trigger on
      - main
      - release/*
    filters:                   # optional runtime filters (compiled to gate step)
      source-pipeline: "Build*"
      branch: "refs/heads/main"  # triggering branch (Build.SourceBranch)
      time-window:
        start: "09:00"
        end: "17:00"
      build-reason:
        include: [IndividualCI]
        exclude: [Schedule]
      expression: "eq(variables['Custom.Flag'], 'true')"  # raw ADO condition
  pr:                          # PR trigger
    branches:
      include: [main]
    paths:
      include: [src/*]
    mode: synthetic            # synthetic (default) | policy. Controls how
                               # `on.pr` builds reach the pipeline.
                               #   - synthetic: a Setup-job script calls the
                               #     ADO REST API on every CI build, finds the
                               #     open PR for `Build.SourceBranch`, and
                               #     promotes the build to PR semantics if it
                               #     matches `branches`/`paths`. No Build
                               #     Validation branch policy required. Zero
                               #     or multiple matches → Agent job
                               #     self-skips cleanly. Emits an
                               #     all-branches `trigger:` so those CI
                               #     builds actually happen.
                               #   - policy: the operator has installed a
                               #     Build Validation branch policy. Compiler
                               #     omits all synth wiring AND emits
                               #     `trigger: none` so feature-branch pushes
                               #     do not queue duplicate CI builds. Real
                               #     PR-typed builds drive everything.
                               # See "PR Triggering in Azure Repos" below.
    filters:                   # runtime PR filters (compiled to gate step)
      title: "*[review]*"
      author:
        include: ["alice@corp.com"]
      draft: false
      labels:
        any-of: ["run-agent"]
      source-branch: "feature/*"
      target-branch: "main"
      commit-message: "*[skip-agent]*"
      changed-files:
        include: ["src/**/*.rs"]
      min-changes: 5
      max-changes: 100
      time-window:
        start: "09:00"
        end: "17:00"
      build-reason:
        include: [PullRequest]
      expression: "eq(variables['Custom.Flag'], 'true')"  # raw ADO condition
execution-context:             # optional execution-context plugin (see docs/execution-context.md)
  enabled: true                # master switch; defaults to true. Set false to disable globally.
  pr:                          # PR-context contributor. Activates on PR-triggered builds when on.pr is set.
    enabled: true              # defaults to true when on.pr is configured. Set false to opt out
                               # (also suppresses auto-adding the read-only git commands to the
                               # agent's bash allow-list).
    checks:
      enabled: false            # OPT-IN: include PR Build Validation check results (default off)
  manual:
    enabled: true               # defaults to true when parameters are declared
    include-email: false
  pipeline:
    enabled: true               # defaults to true when on.pipeline is configured
  ci-push:
    enabled: false              # opt-in "since last green build" CI/push context
  workitem:
    enabled: true               # defaults to true with PR context
    max-items: 5
    max-body-kb: 32
  schedule:
    enabled: false              # OPT-IN: "since last run" diff context for scheduled builds (requires on.schedule)
  repo:
    enabled: false              # opt-in repository identity context
    conventions: false          # opt-in deeper probe (CODEOWNERS / CONTRIBUTING.md / .editorconfig)
steps:                         # inline steps before agent runs (same job, generate context)
  - bash: echo "Preparing context for agent"
    displayName: "Prepare context"
post-steps:                    # inline steps after agent runs (same job, process artifacts)
  - bash: echo "Processing agent outputs"
    displayName: "Post-steps"
setup:                         # separate job BEFORE agentic task
  - bash: echo "Setup job step"
    displayName: "Setup step"
teardown:                      # separate job AFTER safe outputs processing
  - bash: echo "Teardown job step"
    displayName: "Teardown step"
network:                       # optional network policy (standalone target only)
  allowed:                       # allowed host patterns and/or ecosystem identifiers
    - python                   # ecosystem identifier — expands to Python/PyPI domains
    - "*.mycompany.com"        # raw domain pattern
  blocked:                     # blocked host patterns or ecosystems (removes from allow list)
    - "evil.example.com"
# variable-groups:              # optional: import ADO Library variable groups (standalone/1es only)
#   - My Variable Group         # each entry must be the exact ADO Library group name (see "Variable Groups" section)
permissions:                   # optional ADO access token configuration (see docs/network.md#permissions-ado-access-tokens)
  read: my-read-arm-connection   # shorthand: proxy gets the ARM SC token; Agent/MCP/az get no real token
  # read:                        # object form: narrow capabilities / add cross-org or project scope
  #   service-connection: my-read-arm-connection
  #   capabilities: [core, repos] # discovery is always enabled
  #   allow:                     # additive to current org/project/repo and type: git repos:
  #     - organization: partner-org # same AAD tenant; cross-tenant needs another credential
  #       projects:
  #         - project: Shared
  #           project-id: 33333333-3333-3333-3333-333333333333 # optional GUID-form calls
  #           repositories: [shared-api] # empty/omitted => project reads only
  write: my-write-arm-connection # OPTIONAL ARM SC for Stage 3 executor writes.
                                 # Default: executor uses $(System.AccessToken).
                                 # Set this only for cross-org writes or
                                 # named-identity attribution.
supply-chain:                  # optional internal supply-chain mirror (see docs/supply-chain.md)
  feed:                          # mirror binaries (compiler, AWF, ado-script) from an ADO Artifacts feed
    name: my-project/my-feed     # feed name or project/feed; scalar `feed: my-feed` shorthand also works
    service-connection: feed-conn  # optional; omit for same-org feeds (uses $(System.AccessToken))
  # pipeline-artifact:           # alternative binary source; mutually exclusive with feed
  #   project: AgentPlayground   # validated ADO project name or GUID
  #   definition-id: 2560        # positive producer pipeline definition ID
  #   run-id: 630001             # positive, exact producer build ID
  #   artifact: ado-aw-candidate # complete payload/checksum/provenance artifact
  registry:                      # mirror AWF/MCPG images from an internal ACR
    name: myacr.azurecr.io/mirror  # registry host or base path (artifact names kept under it)
    service-connection: acr-conn   # REQUIRED when registry is set (ACR has no System.AccessToken path)
  service-connection: shared-conn  # optional feed/registry fallback; never applies to pipeline-artifact
# ado-aw-debug:                 # debug-only knobs; see docs/ado-aw-debug.md
#   skip-integrity: false       # omit generated pipeline integrity verification
parameters:                    # optional ADO runtime parameters (surfaced in UI when queuing a run)
  - name: clearMemory
    displayName: "Clear agent memory"
    type: boolean
    default: false
---

## Task

Describe the agent's task here. This markdown body is read by the AI agent at
runtime — write it as clear, structured natural-language instructions.

Conclusion job: when safe-outputs: is configured the compiler automatically emits an always-running Conclusion job that files a work-item report on pipeline failures and surfaces diagnostic signals. See docs/conclusion.md.

Reusable Imports (imports: / import-schema:)

imports: lets a workflow reuse local or cross-repository markdown components. Remote branches, tags, and SHAs resolve to an immutable commit SHA at compile time. Each imported file is parsed as regular ado-aw markdown with YAML front matter; the compiler validates optional import-schema: inputs, applies {{ inputs.<key> }} substitutions (a compile-time {{ ... }} replacement — not the ADO ${{ ... }} template delimiter), then merges the imported front matter and body into the consumer workflow. Imported body content is inlined into the agent prompt at compile time (ahead of the consumer's own body); see imports.md for the full reference.

imports:
  - ./components/local-policy.md
  - PlatformProject/shared-agents/components/notify.md@v2
  - uses: components/deploy.md@release/v2
    repository: shared-agents
    with:
      environment: prod
      region: westus3
  - uses: components/github-notify.md@main
    repository: octo/shared-agents
    source: github.com

Object-form fields:

Field Description
uses Local path, combined project/repo/path@ref shorthand, or canonical remote path@ref.
repository Remote repository. A bare name means the current ADO project; project/repo means the current ADO organization.
source Optional compile-time source: cross-org ADO collection URL, github.com, or a GHES host. Omitted means the current ADO organization.
with Non-secret values validated against the imported file's import-schema:.

Import specs may also include #Section to import only a markdown heading section, and a trailing ? to make the import optional. Imports may themselves declare imports; expansion is breadth-first with cycle detection and bounded depth/count/manifest size.

Reusable components declare compile-time inputs with import-schema::

import-schema:
  channel:
    type: string
    required: true
  severity:
    type: choice
    options: [info, warning, critical]
    default: info
  labels:
    type: array
    items:
      type: string

Supported types are string, number, boolean, choice, array, and object (object properties are currently one level deep). See imports.md for the full syntax, cache layout, merge semantics, limitations, and custom safe-output component examples.

Inline step validation (setup / steps / post-steps / teardown)

Inline steps under setup, steps, post-steps, teardown, safe-outputs.threat-detection.steps, and safe-outputs.threat-detection.post-steps are authored as raw Azure DevOps YAML and emitted into the generated pipeline verbatim (a passthrough). For steps that invoke a built-in ADO task the compiler also knows (e.g. CopyFiles@2, Docker@2, DotNetCoreCLI@2, and most other first-party tasks), ado-aw lint performs an advisory validation of the inputs: mapping against the task's typed schema — checking for missing required inputs, unknown input keys, bad constrained values, and (for command/mode tasks) inputs supplied for the wrong command.

This validation is surfaced through the lint channel, not compile:

  • Run ado-aw lint <agent.md> (add --json for machine-readable findings), or call the lint_workflow tool on the author-facing MCP server. Each invalid step produces a task-input-invalid warning finding with the offending task id and which step list it came from.
  • It is warning-only: findings never fail lint (exit code stays 0) and never affect compile or the emitted YAML — the step is always passed through unchanged.
  • A task the compiler does not model, or a non-task step (bash:/script:/ checkout:), produces no finding.

Surfacing this through lint (rather than as a compile-time stderr warning) keeps the feedback in the structured channel that authoring agents already consume to check the steps they synthesise, and keeps the in-pipeline integrity recompile quiet. So adding validation coverage can only ever surface authoring mistakes — it never rejects a workflow that compiled before. See ir.md (tasks/parse.rs) for the mechanism and how to extend coverage.

Debug-only ado-aw-debug:

ado-aw-debug: is accepted in front matter for repository dogfooding and local diagnostics. It is not a regular safe-output tool. Use skip-integrity to omit generated pipeline integrity verification; see ado-aw-debug.md for the full reference.

GitHub issue filing now uses regular safe-outputs.create-github-issue and safe-outputs.set-github-issue-type; see safe-outputs.md.

Per-Job Pool Overrides (pool.overrides:)

By default pool: is applied to every generated job — Setup, Agent, Detection, SafeOutputs, Teardown, and Conclusion. The optional pool.overrides: map lets you assign a different pool to individual jobs while keeping the default for the rest. It is a sub-key of pool:, not a separate top-level key.

pool:
  name: SpecializedLinuxPool       # default for all jobs
  overrides:
    detection:
      vmImage: ubuntu-22.04        # lightweight Microsoft-hosted image
    safe-outputs:
      vmImage: ubuntu-22.04
    conclusion:
      vmImage: ubuntu-22.04

Valid keys

Key Generated job
setup Setup (emitted only when setup: steps are declared)
agent Agent
detection Detection
safe-outputs SafeOutputs (and SafeOutputs_Reviewed — see below)
safe-outputs-reviewed SafeOutputs_Reviewed only (overrides the safe-outputs inheritance)
teardown Teardown (emitted only when teardown: steps are declared)
conclusion Conclusion (emitted only when safe-outputs: is configured)

The Teardown job runs with an always() condition, so its steps execute even when the Agent, Detection, SafeOutputs, or a custom safe-output job fails or is skipped. Write teardown steps as unconditional cleanup — do not assume the upstream jobs succeeded.

safe-outputs-reviewed inherits the safe-outputs override unless it has its own entry. manual-review is not a valid key — the ManualReview job is agentless and always runs on pool: server.

Unknown keys produce a compiler warning and are ignored (forward-compat).

Constraints

  • Each override value accepts the same object pool format as the top-level pool: (Microsoft-hosted vmImage: or self-hosted name:). A bare pool-name string is not accepted here — use name:. The same mutual-exclusion rules apply: name: and vmImage: cannot both be specified; demands: requires name:.
  • Not supported for target: 1es — the 1ES pipeline template controls pool selection. Specifying pool.overrides: with target: 1es is a compile-time error.

Trust boundary note

When a pool.overrides: entry points to a different self-hosted pool than the default pool:, that pool's administrators and agents are trusted with the pipeline artifacts and credentials available to that job. For Detection, SafeOutputs, and Conclusion, this includes the safe-output NDJSON and the write-capable SC_WRITE_TOKEN. For agent and setup, the overridden pool's administrators are trusted with the checked-out source tree and the read-only build token. Using a Microsoft-hosted vmImage: override does not change the trust boundary.

The workspace: field controls which directory the agent runs in. When it is not set explicitly, the compiler chooses a default based on which repositories are checked out (entries in repos: with checkout: true, which is the default):

  • If no additional repositories are checked out (i.e. only the pipeline's own repository is checked out via the implicit self), workspace: defaults to root — the agent runs in the pipeline's working directory root.
  • If one or more additional repositories are checked out, workspace: defaults to repo — the agent runs inside the trigger repository's directory.

Set workspace: explicitly to root, repo (alias self), or a specific checked-out repository alias to override this behavior.

Deprecated directory markers

Earlier releases substituted the directory markers {{ workspace }}, {{ working_directory }}, and {{ trigger_repo_directory }} inside custom steps: / post-steps: / setup: / teardown: blocks. These are deprecated — they encouraged hard-coding a fixed path anchor, which is incorrect under multi-checkout where $(Build.SourcesDirectory) is the shared root of every checked-out repository.

Reference the explicit ADO path instead:

  • $(Build.SourcesDirectory) — the checkout root (the trigger repo root when only self is checked out).
  • $(Build.SourcesDirectory)/self — the compiler-owned self checkout path when one or more additional repositories are checked out.
  • $(Build.SourcesDirectory)/<alias> — a specific checked-out repository.

self identity. The compiler resolves the self repository's name at compile time from the Azure DevOps git remote (or ADO_AW_COMPILE_REMOTE_URL) and bakes it into the compiled pipeline, so safe outputs targeting repository: self never depend on Build.Repository.* — which names the triggering repository and differs from self on repository-resource-triggered runs. If no ADO remote can be resolved at compile time the compiler warns and falls back to $(Build.Repository.Name); compile from an Azure DevOps clone to avoid it.

The legacy_path_markers codemod automatically rewrites any remaining markers in front matter to the path they resolved to on the next compile (see docs/codemods.md). Markers left in the agent body cannot be migrated automatically and are reported as a compile warning. The compiler also emits warning-only advisories when a $(Build.SourcesDirectory)/<seg> reference or a {{#runtime-import …}} target points at a path that will not exist under the resolved checkout layout.

Repositories (repos:)

The repos: field provides a compact way to declare additional repository resources and control which ones the agent checks out. It replaces the legacy repositories: + checkout: pair.

Each entry can be:

Form Syntax Description
Shorthand - org/repo Alias derived from last segment, type=git, ref=refs/heads/main, checkout=true
Shorthand with alias - alias=org/repo Explicit alias before =
Object - name: org/repo Full control over all fields

Object fields:

Field Default Description
name (required) Full org/repo name (maps to ADO name:)
alias last segment of name Repository alias (maps to ADO repository:)
type git ADO repository resource type
ref refs/heads/main Branch or tag reference
endpoint (none) Azure DevOps service connection. Required for type: github, githubenterprise, or bitbucket; not needed for same-org Azure Repos git.
checkout true Whether the agent job clones this repo
fetch-depth (ADO default) Shallow-clone depth for this repo's checkout (ADO fetchDepth). 0 = full history
fetch-tags (ADO default) Whether to fetch git tags during checkout (ADO fetchTags)

Aliases must be unique case-insensitively because they become checkout directory names on Windows agents. root, repo, and self are reserved in every casing; self is the compiler-owned path for the pipeline repository.

Tuning checkout fetch behavior (fetch-depth / fetch-tags)

On large monorepos the checkout step can dominate the run. Azure DevOps can apply a pipeline-level shallow-fetch setting (newer pipelines commonly use depth 1), while tag syncing can also add substantial transfer. fetch-depth and fetch-tags let source-controlled YAML override those settings per repository:

repos:
  - name: my-org/monorepo
    fetch-depth: 1      # shallow — only the tip commit
    fetch-tags: false   # skip the (often huge) tag fetch
  • fetch-depth: 0 explicitly emits fetchDepth: 0, disabling shallow fetch even when the pipeline UI is configured for depth 1. Full history can be very expensive in a large or old repository.
  • When a field is omitted the ADO default applies, so agents that don't set these compile unchanged.
  • Setting fetch-depth/fetch-tags on an entry with checkout: false has no effect (no checkout step is emitted for it); the compiler emits a warning.

Tuning the trigger repository (self)

The trigger repository is always checked out as checkout: self and is not otherwise a repos: entry. To tune its fetch behavior, add a reserved entry whose name is exactly self:

repos:
  - name: self
    fetch-depth: 1
    fetch-tags: false

A self entry contributes only fetch tuning — it does not declare an extra repository resource or an additional checkout. The tuning is applied to the checkout: self step in every job (Setup, Agent, Detection, SafeOutputs, Teardown). Because the tuning comes from source, the compiled lock stays in sync and the runtime "Verify pipeline integrity" step keeps passing — no need to hand-edit the lock or set ado-aw-debug.skip-integrity.

A self entry accepts only fetch-depth and fetch-tags; setting any other field (alias, type, ref, checkout) on it is rejected at compile time. A bare self entry with no fetch fields (e.g. - name: self or the - self shorthand) is a harmless no-op — it changes nothing.

persistCredentials is intentionally not exposed on self; see docs/execution-context.md for the trust-boundary rationale.

Examples

Three repos, all checked out (most common case):

repos:
  - my-org/tools
  - my-org/schemas
  - my-org/docs

Mixed: two checked out, one resource-only (used by templates):

repos:
  - my-org/tools
  - my-org/schemas
  - name: my-org/pipeline-templates
    checkout: false

Custom ref and explicit alias:

repos:
  - name: my-org/docs
    alias: docs-v2
    ref: refs/heads/release/2.x

Legacy syntax (auto-rewritten)

The legacy repositories: + checkout: fields are auto-converted to repos: by the repos_unified codemod. On the next ado-aw compile, any source that still uses the legacy fields is rewritten in place to the new shape — each repositories: entry becomes a repos: entry, with checkout: false added for entries that weren't listed under checkout:. Mixing the legacy fields with an existing repos: block is rejected; pick one shape.

Variable Groups (variable-groups:)

Import one or more Azure DevOps variable groups (ADO "Library" groups) into the generated pipeline so a source-clean lock can consume secrets that are managed once at the project level — for example a GitHub App private key shared across many pipelines:

variable-groups:
  - Agentic Workflows
  - Shared Secrets

Each entry is the name of a variable group. The compiler emits a top-level variables: block with one - group: import per entry, in declaration order:

variables:
  - group: Agentic Workflows
  - group: Shared Secrets

Groups are evaluated in order, so a later group wins on key collisions.

Authorization and import are both required

In Azure DevOps, two independent things are needed before a group's variables are available to a YAML run:

  1. Authorization — the pipeline definition must be granted permission to use the group (done in the ADO Library UI / API, outside ado-aw).
  2. YAML import — the pipeline YAML must explicitly pull the group in with variables: - group: <name>.

Authorization alone is not sufficient — without the YAML import the variables are unavailable at runtime. variable-groups: provides the YAML import; you still have to authorize the group on the pipeline definition.

Names only — never values

Only group names belong in variable-groups:. ado-aw never resolves, prints, logs, or serialises a group's variable values. Steps continue to reference secret variables by macro ($(VAR_NAME)) exactly as before — for instance engine.github-app-token.private-key names a variable that a group supplies:

variable-groups:
  - Agentic Workflows
engine:
  id: copilot
  github-app-token:
    app-id: 1234567
    owner: octo-org
    private-key: AGENTIC_WORKFLOWS_GITHUB_APP_PRIVATE_KEY

Group names that contain ADO expressions (${{, $(, $[), pipeline commands (##vso[, ##[), the compiler's template marker ({{), or control characters (including newlines) are rejected at compile time. Names with leading or trailing whitespace are also rejected — the entry is emitted verbatim as - group: <name>, so it must match the ADO group name exactly.

Duplicate imports are rejected too. Azure DevOps variable group names are case-insensitive, so two entries that differ only by case (e.g. Shared Secrets and shared secrets) are treated as the same group and fail compilation — remove the redundant entry.

Target support

variable-groups: is only valid for pipeline-level targets — standalone (default) and 1es. Azure DevOps job / stage templates cannot declare pipeline-level variables: (the parent pipeline that includes the template owns them), so variable-groups: on target: job or target: stage is a hard compile-time error. Import the group in the parent pipeline that includes the template instead.

Inlined Imports

The inlined-imports: field controls when {{#runtime-import ...}} markers in the markdown body are resolved. It defaults to false. See runtime-imports.md for the full marker syntax, path resolution rules, and runtime behavior.

When inlined-imports: false, the compiler leaves runtime-import markers to be resolved on the pipeline runner. This is the default behavior, and it means prompt-body edits do not require recompiling the generated YAML.

When inlined-imports: true, the compiler resolves all runtime-import markers at compile time, including the implicit top-level marker that normally reloads the body itself. The emitted YAML contains the fully expanded prompt body, so the pipeline file is self-contained.

The trade-off is that the generated YAML is larger, and prompt-body edits require ado-aw compile plus committing the updated pipeline file.

A small, fixed set of ADO path-anchor variables — including $(Build.SourcesDirectory) and $(Build.Repository.Name) — is substituted into the prompt consistently in both modes. Build.Repository.Name identifies the triggering repository and is not the compiler-owned self checkout path. Arbitrary $(...) macros and pipeline/secret variables are not expanded; see ADO variables in the prompt.

Filter Validation

The compiler validates filter configurations at compile time and will emit errors for impossible or conflicting combinations:

Condition Severity Message
min-changes > max-changes Error No PR can satisfy both constraints
time-window.start = time-window.end Error Zero-width window never matches
Same value in author.include and author.exclude Error Conflicting include/exclude
Same value in build-reason.include and build-reason.exclude Error Conflicting include/exclude (both PR and pipeline filters)
Label in both labels.any-of and labels.none-of Error Label both required and blocked
Label in both labels.all-of and labels.none-of Error Label both required and blocked
Empty labels filter (no any-of/all-of/none-of) Warning No label checks applied

Errors cause compilation to fail. Fix the conflicting filter configuration before recompiling.

Filter Behavior Notes

Time Windows

Time windows use half-open intervals: [start, end). A window of start: "09:00", end: "17:00" matches from 09:00 up to but not including 17:00. A build triggered at exactly 17:00 UTC will not match.

Overnight windows are supported: start: "22:00", end: "06:00" matches from 22:00 through midnight to 05:59.

All times are evaluated in UTC.

Changed Files

The changed-files filter checks the list of files modified in the PR. If the PR has no changed files (empty diff) and an include pattern is set, the filter will not match. An exclude-only filter (no include) with no changed files passes vacuously (no excluded files are present).

Expression Escape Hatch

The expression field on pr.filters and pipeline.filters is an advanced, unsafe escape hatch. Its value is inserted verbatim into the Agent job's ADO condition: field. It can reference any ADO pipeline variable, including secrets. The compiler validates against ##vso[ injection and ADO compile-time template expressions (${{), but otherwise trusts the value. Only use this if the built-in filters are insufficient.

Pipeline Requirements

The filter gate step uses System.AccessToken for self-cancellation (PATCH to the builds REST API) and PR metadata retrieval. This requires:

  1. "Allow scripts to access the OAuth token" must be enabled on the pipeline definition in ADO (Project Settings → Pipelines → Settings).
  2. The pipeline's build service account must have permission to cancel builds.

If the token is unavailable, the gate step logs a warning and the build completes as "Succeeded" (with the agent job skipped via condition) rather than "Cancelled".

Push (CI) Triggering (on.push)

on: is the complete declaration of when a pipeline runs. If a workflow does not ask for a trigger, it does not get one — a workflow with no on: key at all compiles to a manual / API-queued-only pipeline.

That has to be stated explicitly in the compiled YAML, because Azure DevOps reads a missing top-level trigger: key as "run CI on every branch", not "no CI". The compiler therefore always emits both trigger: and pr:.

on:
  push: none                   # never start on a push
on:
  push:                        # start only on pushes to main touching src/
    branches:
      include: [main, "release/*"]
      exclude: ["wip/*"]
    paths:
      include: ["src/**"]
      exclude: ["docs/**"]

branches and paths are passed through to ADO's native trigger: filters verbatim; the compiler does not rewrite or narrow them.

What gets emitted

Front matter Top-level trigger:
no on: at all none — manual / API-queued only
on.schedule or on.pipeline only none
on.pr (default mode: synthetic) all branches (include: ['*'])
on.pr.mode: policy none
on.push: none none
on.push: {branches, paths} the authored filter block

An explicit on.push always wins, including over the all-branches trigger that mode: synthetic emits and over the none that a schedule or mode: policy would otherwise produce. "Run nightly, and whenever main moves" is a legitimate shape:

on:
  schedule: daily around 03:00
  push:
    branches:
      include: [main]

on.push controls only the trigger: key. The pr: key is independent and stays driven by on.pr — setting on.push never enables or disables PR triggering.

Note: on.push is unrelated to execution-context.ci-push, which stages context facts inside a build that has already started.

PR Triggering in Azure Repos

Azure DevOps Services ignores the YAML pr: block unless a per-branch Build Validation branch policy is registered server-side. Without that policy, a git push to a feature branch fires the compiled pipeline as Build.Reason = IndividualCI even when an open PR exists — the gate evaluator's "not a PR build" bypass triggers and exec-context-pr.js is skipped. PR-aware agents (e.g. PR reviewers) silently degrade.

ado-aw lets the agent author pick one of two coherent strategies via on.pr.mode:

on.pr.mode Synthesis wiring Top-level trigger: Use when
synthetic (default) emitted (synthPr Setup step, coalesced env, broadened conditions) all branches (include: ['*']) No branch policy. The vast majority of agents.
policy omitted trigger: none Operator has installed a Build Validation branch policy and wants real PR-typed builds only, no duplicate CI builds.

An explicit on.push overrides the trigger: column in both rows.

mode: synthetic — how it works under the hood

On every CI build:

  1. Real PR build? If Build.Reason == PullRequest (a branch policy is configured), the synth step no-ops and the existing PR path handles everything.
  2. GitHub-typed repo resource? GitHub repos already get correct pr: semantics from ADO. The synth step no-ops.
  3. Look up the PR. Otherwise, the script calls GET /{project}/_apis/git/repositories/{repoId}/pullrequests filtered by sourceRefName == Build.SourceBranch and status = active.
  4. Filter by target branch. PRs whose targetRefName does not match on.pr.branches.include (respecting exclude) are dropped.
  5. Exactly one match. Zero or multiple matches → emit AW_SYNTHETIC_PR_SKIP=true; the Agent job self-skips cleanly with a single info log line. Never noisy, never red.
  6. Path filter. If on.pr.paths is configured, the script enforces it against the PR's changed-file list (which ADO's CI trigger ignores). Empty intersection → skip.
  7. Promote. Otherwise, emit AW_SYNTHETIC_PR=true plus the PR identifiers as Setup-job outputs. Downstream gate.js and exec-context-pr.js env blocks coalesce these with the real System.PullRequest.* variables, so the gate evaluator runs the full PR-spec predicates and aw-context/pr/{base.sha,head.sha} is staged for the agent.

Why the CI trigger is not auto-narrowed in mode: synthetic

pr.branches.include lists PR target branches (e.g. main), but ADO trigger: fires on pushes to the listed branches. Narrowing trigger: to pr.branches.include would suppress CI on the feature branches synthPr actually needs to react to (pushing to feature/x with an open PR feature/x → main would never queue a build). The compiler therefore emits an all-branches trigger: in synth mode, and relies on the synthPr Setup step's fast-exit for cost control: a single listActivePullRequestsBySourceRef call returns [] on branches without a matching PR and the Agent job self-skips cleanly via AW_SYNTHETIC_PR_SKIP=true.

If you do want to narrow it, set on.push explicitly — but remember it must cover the source branches of the PRs you care about, not their target branches. Setting on.push: none alongside mode: synthetic disables synthesis in practice, because there are no CI builds left for the synthPr step to run on.

mode: policy — when to choose it

Choose mode: policy when the operator has explicitly installed an Azure DevOps Build Validation branch policy targeting the compiled pipeline. In this mode the compiler:

  • Omits all synth wiring (synthPr step, PR_SYNTH_SPEC env, AW_SYNTHETIC_PR_SKIP guard, coalesced env macros, broadened exec-context-pr.js condition).
  • Emits trigger: none so feature-branch pushes do not queue duplicate CI builds alongside the policy-driven PR build.

Result: every PR update fires exactly one PR-typed build (Build.Reason == PullRequest); commit-driven CI is fully silenced.