ado-script
ado-script is the umbrella name for the TypeScript workspace at
scripts/ado-script/.
It produces small, ncc-bundled Node programs that the compiler injects into emitted
pipelines as runtime helpers. Today it produces fifteen bundles:
gate.js— trigger-filter gate evaluator (Setup job)import.js— runtime prompt resolver described in Runtime Imports (Agent job)exec-context-pr.js— PR merge-base precompute described in Execution Context (Agent job, PR builds only)exec-context-pr-synth.js— synthetic-PR resolver that promotes CI builds with an open PR to PR semantics (Setup job)exec-context-manual.js— manual-context precompute that stages requested-by identity and parameter snapshot (Agent job)exec-context-pipeline.js— pipeline-completion precompute that stages upstream build metadata (Agent job)exec-context-ci-push.js— CI/push precompute that stages changed files and commits since the last green build (Agent job)exec-context-workitem.js— PR-linked work-item precompute that stages work-item metadata and descriptions (Agent job)exec-context-schedule.js— scheduled-run precompute that stages changed files since the last successful scheduled run (Agent job)exec-context-pr-checks.js— PR Build Validation checks precompute that stages succeeded/failing check results (Agent job)exec-context-repo.js— repository identity precompute that stages branch, SHA, last release tag, and commits-since-tag facts (Agent job)conclusion.js— Conclusion job reporter that files/comments ADO work items for pipeline failures and diagnostic signals (Conclusion job)approval-summary.js— renders a sanitized per-tool summary of proposed safe outputs into the build’sado-aw-safe-outputssummary tab (end of Agent job)github-app-token.js— mints (and revokes) a GitHub App installation token for the Copilot engine whenengine.github-app-tokenis configured (Agent + Detection jobs)prepare-pr-base.js— Agentpatch-basemode uses ADO diff metadata to fetch and verify only the source/target history needed for the merge-base (bounded 200/500/2000 dual-ref fallback); SafeOutputstarget-worktreemode fetches only the target tip at depth 1 (issue #1453)
What gate.js does
Section titled “What gate.js does”gate.js is a single-shot Node program that runs as a step in the
pipeline’s Setup job and decides whether the downstream Agent /
SafeOutputs jobs should execute. It evaluates a declarative GateSpec
against runtime facts (PR title, labels, changed files, build reason,
etc.) and emits exactly one ##vso[task.setvariable] line:
##vso[task.setvariable variable=SHOULD_RUN;isOutput=true]true (or false)Downstream jobs gate themselves on that variable via a condition:
clause emitted by the compiler.
The gate is a data interpreter, not a code evaluator. The GateSpec
is a typed JSON document; predicates are dispatched via a switch on a
discriminated union. There is no eval, no Function, no vm — a
compromised compiler cannot use the spec to run arbitrary code on the
pipeline runner.
What import.js does
Section titled “What import.js does”import.js is a single-shot Node program. It accepts the prompt file path as
its first positional argument and resolves {{#runtime-import path}} markers in
place. The compiler runs it as a post-prepare-prompt step when
inlined-imports: false. See
Runtime Imports for the author-facing marker
syntax.
CLI contract
Section titled “CLI contract”import.js takes no environment variables — all inputs are explicit CLI flags
so the resolver cannot be influenced by ambient pipeline state:
node import.js <prompt-file> --base "$(Build.SourcesDirectory)" \ --var "Build.SourcesDirectory=$(Build.SourcesDirectory)" \ --var "Build.Repository.Name=$(Build.Repository.Name)"--base <path>— root that relative marker paths resolve against. The compiler always passes$(Build.SourcesDirectory)(ADO expands the macro before Node runs).import.jsrejects absolute and..paths.--var name=value(repeatable) — a small, compiler-owned allowlist of non-secret ADO variables (currentlyBuild.BuildId,Build.Repository.Name,Build.SourcesDirectory, andSystem.CollectionUri, defined byPROMPT_ADO_VARSinsrc/compile/extensions/ado_script.rs).import.jsreceives the concrete expanded value and literally substitutes every$(name)occurrence in the final prompt. Unknown$(...)macros are left untouched. The allowlist is the set of--varflags the compiler emits, so an untrusted agent body cannot introduce new substitution variables.
Resolution is single-pass: nested markers inside an inlined body are not re-expanded.
The bundle lives at import.js and ships in the same
ado-script.zip release asset as all other bundles, so pipelines download it
through the same Agent-job asset flow. import.js uses only the Node
standard library, so the ncc bundle is small (~1.5 KB) and carries no
SDK dependency.
The Stage-2 threat-analysis prompt is not runtime-imported.
src/data/threat-analysis.md is include_str!’d into the ado-aw
binary and inlined into the emitted YAML at compile time, matching
gh-aw’s pattern (their threat_detection.md ships with the setup
action and is read directly from disk — no marker, no resolver).
What exec-context-pr.js does
Section titled “What exec-context-pr.js does”exec-context-pr.js is a single-shot Node program injected into the Agent
job’s prepare phase only on PR-triggered builds. It resolves the PR
merge-base and stages artefacts the agent can use to reason about the diff:
aw-context/└── pr/ ├── base.sha # target merge-base SHA (40-char hex) ├── head.sha # PR head SHA (40-char hex) └── error.txt # present only on failure; one-line reasonIt also appends a tailored prompt fragment to /tmp/awf-tools/agent-prompt.md
(the file assembled by the “Prepare agent prompt” base-YAML step). On success
the fragment tells the agent its PR number, project, repository, and provides
pre-filled example ADO MCP tool call arguments. On failure it surfaces a
graceful degradation message so the agent can still proceed without panicking.
Merge-base resolution
Section titled “Merge-base resolution”Two paths, both computing the same “merge-base of target tip and PR head”:
- Synthetic merge commit — ADO’s default PR checkout produces a merge
commit (
HEADhas ≥ 2 parents).HEAD^1is the target tip;HEAD^2is the PR head. The bundle runsgit merge-base HEAD^1 HEAD^2; if that fails in a shallow checkout, it fetches both target and source refs with bounded deepening and retries before failing closed. - Bounded deepening — when
HEADis a normal commit (shallow clone without a merge commit), the bundle fetches source and target together at increasing depths (--depth=200,500,2000) untilgit merge-base origin/<target> HEADresolves.
Trust boundary
Section titled “Trust boundary”SYSTEM_ACCESSTOKEN is mapped only into this step’s env: block — never into
the agent step’s env. The bearer is passed to spawned git child processes via
GIT_CONFIG_COUNT / GIT_CONFIG_KEY_0 / GIT_CONFIG_VALUE_0 env vars (the
http.extraheader pattern), so it never appears in argv or in .git/config.
Env-var contract
Section titled “Env-var contract”| Env var | ADO source | Purpose |
|---|---|---|
SYSTEM_ACCESSTOKEN |
$(System.AccessToken) |
Bearer for git fetch of the target branch |
SYSTEM_PULLREQUEST_PULLREQUESTID |
$(System.PullRequest.PullRequestId) |
PR number (validated: digits only) |
SYSTEM_PULLREQUEST_TARGETBRANCH |
$(System.PullRequest.TargetBranch) |
Target branch ref (e.g. refs/heads/main) |
SYSTEM_TEAMPROJECT |
$(System.TeamProject) |
ADO project name (validated: alphanumeric + . _ -) |
BUILD_REPOSITORY_NAME |
$(Build.Repository.Name) |
Repository name (validated: alphanumeric + . _ -) |
BUILD_SOURCESDIRECTORY |
$(Build.SourcesDirectory) |
Workspace root — where aw-context/pr/ is staged |
All four identifier env vars are validated against strict allowlist regexes
before any value is interpolated into a git refspec or the agent prompt. A
validation failure writes aw-context/pr/error.txt and a failure-fragment to
the agent prompt, then exits 0 so the rest of the pipeline can continue.
End-to-end data flow
Section titled “End-to-end data flow” ┌──────────────────────┐ │ Rust compiler │ │ (filter_ir.rs) │ └──────────┬───────────┘ │ build_gate_spec(...) → GateSpec (JSON, base64) ▼ ┌──────────────────────┐ │ Generated pipeline │ │ Setup job: │ │ 1. UseNode@1 │ │ 2. curl + sha256 │ downloads ado-script.zip │ + unzip │ from the matching ado-aw release │ 3. node gate/index │ reads GATE_SPEC env var │ .js │ └──────────┬───────────┘ │ ##vso[task.setvariable variable=SHOULD_RUN;…] ▼ ┌──────────────────────┐ │ Agent / SafeOutputs │ conditioned on SHOULD_RUN=true │ jobs │ └──────────────────────┘The same GateSpec shape is generated as a JSON Schema by
cargo run -- export-gate-schema and converted to TypeScript by
json-schema-to-typescript into src/shared/types.gen.ts. The TS
gate evaluator imports from types.gen.ts, never from a hand-written
mirror of the IR — so the spec contract cannot drift between compiler
and evaluator. CI enforces this with a git diff --exit-code step on
the codegen output.
Runtime stages inside gate.js
Section titled “Runtime stages inside gate.js”gate.js’s entry point is src/gate/index.ts. It runs five stages,
all single-shot, all fail-closed on error:
- Decode + size-cap — base64-decode
GATE_SPEC, reject if the decoded JSON exceedsMAX_SPEC_DECODED_BYTES(256 KiB), thenJSON.parse. - Pre-flight validation — walk the predicate tree and throw on
any unknown
typediscriminant. This catches version drift between a newer compiler and an older bundledgate.jsbefore fact acquisition runs, so the failure mode is “loud” rather than “silent skip when the dependent fact is unavailable”. Deliberately runs beforerunBypassso a malformed spec fails fast regardless of build reason. - Bypass — if
ADO_BUILD_REASONdoes not matchspec.context.build_reason(e.g. spec is forPullRequestbut the build isManual), auto-pass: emitSHOULD_RUN=true, tag the build, completeSucceeded, exit. - Fact acquisition — for every
FactSpecin the spec, either read a pipeline env var (isPipelineVarFact) or call the ADO REST API (pr_metadata,pr_labels,changed_files, …). Each per-fact failure is recorded in thePolicyTrackerand dispatched via that fact’sfailure_policy(fail_closed/fail_open/skip_dependents). - Predicate evaluation — for each
CheckSpec, thePolicyTrackerdecides whether the check isevaluate,pass,skip, orfailbased on which referenced facts are still available. Evaluator dispatches the predicate via theswitchinevaluatePredicate. Failing checks emitaddBuildTagand the overallSHOULD_RUNistrueiff every check ispassorskip.
If SHOULD_RUN ends up false, selfCancelIfRequested issues a
best-effort BuildStatus.Cancelling PATCH so the pipeline run is
visibly cancelled in the ADO UI rather than just paused on a gated
job.
Runtime env-var contract
Section titled “Runtime env-var contract”The compiler injects these environment variables on the
bash: node gate.js step. gate.js reads them via
process.env:
| Env var | Source | Purpose |
|---|---|---|
GATE_SPEC |
compiled inline (base64) | The full GateSpec JSON |
SYSTEM_ACCESSTOKEN |
$(System.AccessToken) |
ADO REST auth |
SYSTEM_COLLECTIONURI |
ADO auto-injected | ADO org base URL (read via getWebApi(); falls back to SYSTEM_TEAMFOUNDATIONCOLLECTIONURI) |
ADO_BUILD_REASON |
$(Build.Reason) |
Used by the bypass branch |
ADO_BUILD_ID |
$(Build.BuildId) |
Used for selfCancelIfRequested |
ADO_PROJECT / ADO_REPO_ID / ADO_PR_ID |
compiler-injected | PR-derived facts |
ADO_* (fact-specific) |
Fact::ado_exports() in Rust |
Per-fact pipeline-variable readers (e.g. ADO_PR_TITLE, ADO_SOURCE_BRANCH) |
ADO_API_TIMEOUT_MS |
optional override | Per-attempt timeout for every ADO REST call. Default 30 000. On timeout, the call is retried once; if the retry also times out, the gate falls back to the per-fact FailurePolicy. |
The exact contract for pipeline-variable facts (which env var maps to
which FactKind) lives in two places that must stay in lockstep:
- Rust:
Fact::ado_exports()insrc/compile/filter_ir.rs - TS:
ENV_BY_FACTplus theFactKindunion inscripts/ado-script/src/shared/env-facts.ts
The codegen drift check only mirrors the GateSpec shape, not the
env-var mapping, so when adding a new pipeline-variable fact you must
update both sides by hand. Fact::ado_exports() carries a docstring
pointing at the TS mirror as a reminder.
Workspace layout
Section titled “Workspace layout”scripts/ado-script/├── package.json # type:module; dep: azure-devops-node-api (lazy-imported)├── tsconfig.json # strict; noUncheckedIndexedAccess; NodeNext├── src/│ ├── shared/ # Reusable across all bundles│ │ ├── types.gen.ts # AUTO-GENERATED from Rust IR — do not edit│ │ ├── auth.ts # WebApi factory; SDK is dynamic-imported here│ │ ├── ado-client.ts # azure-devops-node-api wrapper + retry + timeout + pagination│ │ ├── env-facts.ts # Pipeline-variable readers + ENV_BY_FACT + BRANCH_FACTS + ref-prefix stripping│ │ ├── policy.ts # PolicyTracker state machine│ │ ├── vso-logger.ts # ##vso[…] emitters with property/message escaping; complete() is idempotent│ │ ├── git.ts # execFile wrappers + bearerEnv helper│ │ ├── merge-base.ts # synthetic-merge detection + progressive-deepening fetch│ │ ├── validate.ts # identifier regex guards│ │ ├── prompt.ts # agent-prompt-file append helpers│ │ └── build.ts # Build REST helpers (used by pipeline / ci-push / pr-checks)│ ├── gate/ # gate.js entry point + per-concern modules│ │ ├── index.ts # main(): decode → preflight → bypass → facts → eval → emit│ │ ├── bypass.ts # build-reason auto-pass│ │ ├── facts.ts # fact acquisition (env + REST)│ │ ├── predicates.ts # 11 predicate evaluators + validatePredicateTree + glob ReDoS hardening│ │ └── selfcancel.ts # best-effort build cancellation│ ├── import/ # import.js entry point + runtime prompt resolver│ │ ├── index.ts # main(): expand runtime-import markers in place│ │ └── __tests__/ # marker, path-resolution, and single-pass coverage│ ├── exec-context-pr/ # exec-context-pr.js entry point + PR precompute│ │ ├── index.ts # main(): validate → resolve merge-base → stage SHAs → append prompt│ │ └── __tests__/ # end-to-end / integration tests│ ├── exec-context-pr-synth/ # exec-context-pr-synth.js entry point + synthetic-PR resolver│ │ ├── index.ts # main(): real-PR / GitHub / synth-promote branch resolution → emit AW_PR_*│ │ ├── match.ts # branch/path include-exclude glob matching│ │ ├── spec.ts # PR_SYNTH_SPEC base64 decode + validation│ │ └── __tests__/ # unit tests across the three modules│ ├── exec-context-manual/ # exec-context-manual.js entry point + manual-context precompute│ │ ├── index.ts # main(): collect PARAM_* env vars → JSON snapshot → prompt fragment│ │ └── __tests__/ # unit tests for success / failure / sanitisation paths│ ├── exec-context-pipeline/ # exec-context-pipeline.js entry point + pipeline-completion precompute│ │ ├── index.ts # main(): validate TriggeredBy ids → fetch upstream Build via REST → stage + prompt│ │ └── __tests__/ # unit tests for validate / success / failure / sanitisation paths│ ├── exec-context-ci-push/ # exec-context-ci-push.js entry point + CI/push diff context│ │ ├── index.ts # main(): find last green build → stage changed files / commits → prompt│ │ └── __tests__/ # unit tests for success / fallback / sanitisation paths│ ├── exec-context-workitem/ # exec-context-workitem.js entry point + PR-linked work-item context│ │ ├── index.ts # main(): fetch linked WIs → stage metadata + descriptions → prompt│ │ └── __tests__/ # unit tests for fetch / truncation / sanitisation paths│ ├── exec-context-schedule/ # exec-context-schedule.js entry point + scheduled-run diff context│ │ ├── index.ts # main(): find prior scheduled success → stage changed files / commits → prompt│ │ └── __tests__/ # unit tests for success / no-baseline / sanitisation paths│ ├── exec-context-pr-checks/ # exec-context-pr-checks.js entry point + PR validation checks context│ │ ├── index.ts # main(): fetch policy/build checks → stage failing/succeeded JSON → prompt│ │ └── __tests__/ # unit tests for checks filtering / sanitisation paths│ ├── exec-context-repo/ # exec-context-repo.js entry point + repository identity context│ │ ├── index.ts # main(): stage branch/SHA/tag/commits-since-tag facts → prompt│ │ └── __tests__/ # unit tests for identity / tag fallback / sanitisation paths│ └── conclusion/ # conclusion.js entry point + Conclusion-job reporter│ ├── index.ts # main(): inspect upstream results + safe-outputs manifest → file/append work items│ └── __tests__/ # unit tests for signal detection and work-item filing behaviour├── test/ # End-to-end smoke tests├── gate.js # ncc bundle output (gitignored)├── import.js # ncc bundle output (gitignored)├── exec-context-pr.js # ncc bundle output (gitignored)├── exec-context-pr-synth.js # ncc bundle output (gitignored)├── exec-context-manual.js # ncc bundle output (gitignored)├── exec-context-pipeline.js # ncc bundle output (gitignored)├── exec-context-ci-push.js # ncc bundle output (gitignored)├── exec-context-workitem.js # ncc bundle output (gitignored)├── exec-context-schedule.js # ncc bundle output (gitignored)├── exec-context-pr-checks.js # ncc bundle output (gitignored)├── exec-context-repo.js # ncc bundle output (gitignored)├── conclusion.js # ncc bundle output (gitignored)└── approval-summary.js # ncc bundle output (gitignored)The release workflow (.github/workflows/release.yml) runs
npm ci && npm run build, then zips all thirteen bundles — gate.js,
import.js, exec-context-pr.js, exec-context-pr-synth.js,
exec-context-manual.js, exec-context-pipeline.js,
exec-context-ci-push.js, exec-context-workitem.js,
exec-context-schedule.js, exec-context-pr-checks.js,
exec-context-repo.js, conclusion.js, and approval-summary.js — into the
ado-script.zip release asset. Pipelines download that asset at
runtime by URL pinned to the compiler’s CARGO_PKG_VERSION, verify
its SHA-256 against the checksums.txt asset, then extract.
Schema codegen
Section titled “Schema codegen”types.gen.ts is derived from the Rust IR via
schemars →
json-schema-to-typescript:
┌──────────────────────────┐ schemars ┌──────────────────────────┐│ src/compile/filter_ir.rs │ ───────────► │ schema/gate-spec.schema ││ #[derive(JsonSchema)] │ │ .json │└──────────────────────────┘ └────────────┬─────────────┘ │ json2ts ▼ ┌──────────────────────────────┐ │ src/shared/types.gen.ts │ │ (consumed by gate/*.ts) │ └──────────────────────────────┘npm run codegen runs both stages. The CI workflow
(.github/workflows/ado-script.yml) regenerates the file and runs
git diff --exit-code to fail on drift, on both PRs and pushes to
main. If you change the IR shape in Rust, run
cd scripts/ado-script && npm run codegen and commit the regenerated
types.gen.ts.
The Rust subcommand that emits the schema is intentionally hidden:
cargo run -- export-gate-schema --output schema/gate-spec.schema.jsonHow the bundles are wired into emitted pipelines
Section titled “How the bundles are wired into emitted pipelines”AdoScriptExtension
(src/compile/extensions/ado_script.rs) is the always-on single
extension that owns all ado-script wiring. It has two independent
features, each emitted into the job that actually consumes the
bundle:
Setup job (gate evaluator)
Section titled “Setup job (gate evaluator)”When filters: lowers to non-empty checks, AdoScriptExtension::declarations()
returns three typed Declarations::setup_steps entries for the Setup job:
UseNode@1— installs Node 22.x LTS, capped attimeoutInMinutes: 5.curldownload + verify + extract — fetcheschecksums.txtandado-script.zipfrom thegithubnext/ado-awrelease matchingCARGO_PKG_VERSION, verifies the zip’s SHA-256, thenunzip -o /tmp/ado-aw-scripts/ado-script.zip -d /tmp/ado-aw-scripts/. Also capped attimeoutInMinutes: 5.bash: node '/tmp/ado-aw-scripts/ado-script/gate.js'— runs the gate withGATE_SPECand the env-var contract documented above.
Agent job (runtime-import resolver)
Section titled “Agent job (runtime-import resolver)”AdoScriptExtension::declarations() contributes Agent-job prepare steps when
import.js is active (i.e. inlined-imports: false, the default). It returns
install + download first, then the invocation step.
import.js invocation — active when inlined-imports: false (the default):
UseNode@1— installs Node 22.x LTS, capped attimeoutInMinutes: 5.curldownload + verify + extract — same artefact and verification as the Setup job.bash: node '/tmp/ado-aw-scripts/ado-script/import.js'— expands{{#runtime-import …}}markers in/tmp/awf-tools/agent-prompt.mdin place. See Runtime Imports for marker syntax.
The execution-context bundles (exec-context-*.js) are owned by
ExecContextExtension (src/compile/extensions/exec_context/), not
AdoScriptExtension. Phase ordering (AdoScriptExtension::phase() == System <
ExecContextExtension::phase() == Tool) guarantees the bundle is installed and
on disk before any exec-context invocation runs. ExecContextExtension emits
its own install + download pair for the Agent job and the Setup job
(exec-context-pr-synth.js) independently.
Per-job download (NOT a duplication bug)
Section titled “Per-job download (NOT a duplication bug)”ADO jobs use isolated VMs — /tmp is not shared between jobs.
The ado-script.zip bundle therefore has to be downloaded once per
job that consumes it. When both the Setup gate and Agent resolver/PR-context
are active, install + download steps appear in both Setup and Agent.
That’s correct architecture given ADO’s topology, not waste.
What gets emitted, by case
Section titled “What gets emitted, by case”The rows below assume the synthetic-PR resolver is not active:
| Setup consumer | Agent consumer | Setup-job steps | Agent-job extra steps |
|---|---|---|---|
| no gate | none | (none) | (none) |
| no gate | inlined-imports: false only |
(no Setup job) | install + download + resolver |
| no gate | execution-context contributor(s) only | (no Setup job) | install + download + exec-context bundle(s) |
| no gate | resolver + execution-context | (no Setup job) | install + download + resolver + exec-context bundle(s) |
| gate | none | install + download + gate | (none) |
| gate | any combination | install + download + gate | install + download + (resolver?) + (exec-context bundle(s)?) |
When the synthetic-PR resolver is active (on.pr configured with synthetic-PR
support), the Setup job gains a synthPr step (node exec-context-pr-synth.js)
before any gate step — and the Setup job is emitted even with no gate:
| Setup consumer | Setup-job steps |
|---|---|
| synth-PR (no gate) | install + download + synth-PR |
| gate (no synth-PR) | install + download + gate |
| synth-PR + gate | install + download + synth-PR + gate |
The IR-to-bash codegen that produces the gate step is
compile_gate_step_external in src/compile/filter_ir.rs.
Modifying ado-script
Section titled “Modifying ado-script”Add a new predicate
Section titled “Add a new predicate”- Add a
Predicate+PredicateSpecvariant insrc/compile/filter_ir.rs. Runcargo testand update spec tests. - In
scripts/ado-script/, runnpm run codegensotypes.gen.tspicks up the new variant. - Add a
caseto theswitchinsrc/gate/predicates.ts::evaluatePredicate. - Add the new type name to
KNOWN_PREDICATE_TYPES(right above thevalidatePredicateTreefunction). Both updates are required — the drift testKNOWN_PREDICATE_TYPES stays in sync with evaluatePredicate switchinpredicates.test.tswill fail if you forget either. - Add a vitest case under
src/gate/__tests__/ports/<new-predicate>.test.ts.
Add a new pipeline-variable fact
Section titled “Add a new pipeline-variable fact”- Add a
Factvariant insrc/compile/filter_ir.rsand updateFact::ado_exports(). (Its docstring reminds you about step 3.) npm run codegento regenerate types.- Add an entry to
ENV_BY_FACTand extend theFactKindunion inscripts/ado-script/src/shared/env-facts.ts. Without this step the gate silently treats the fact as missing. - If the fact value is ref-shaped (e.g. a branch name), add it to
the exported
BRANCH_FACTSset so the read-time strip is applied.
Add a new bundle (e.g. poll.js)
Section titled “Add a new bundle (e.g. poll.js)”The existing exec-context-pr.js bundle is a working example of this pattern — see
scripts/ado-script/src/exec-context-pr/ and src/compile/extensions/exec_context/pr.rs.
- Create
src/poll/index.tsand supporting modules underscripts/ado-script/src/poll/. Reuse anything insrc/shared/. - Add a build script to
package.json:and extend"build:poll": "ncc build src/poll/index.ts -o .ado-build/poll -m -t && node -e \"const fs=require('node:fs'); fs.copyFileSync('.ado-build/poll/index.js','poll.js'); fs.rmSync('.ado-build/poll',{recursive:true,force:true});\""buildto also run it. - Add vitest tests under
src/poll/__tests__/. - Wire from a new
CompilerExtension(or extend an existing one) that downloadsado-script.zip(already a release asset) and invokesnode /tmp/ado-aw-scripts/ado-script/poll.jsas a runtime step. - Update release packaging to include
scripts/ado-script/poll.jsinado-script.zipalongside other bundles.
Local development loop
Section titled “Local development loop”From scripts/ado-script/:
npm ci # one-timenpm run codegen # regenerate types.gen.ts (compiles ado-aw first)npm test # vitest unit testsnpm run typecheck # strict tsc --noEmitnpm run build # ncc-bundle to gate.jsnpm run test:smoke # build + smoke test the bundle end-to-endThe Rust-side E2E gate test compiles a real agent, extracts the
emitted GATE_SPEC, and shells out to the bundled gate.js:
cargo test --test gate_e2e -- --ignored --nocaptureBundle-size budget
Section titled “Bundle-size budget”Each bundled artifact must stay under 5 MB. The entry-point
chunk for gate.js is ~78 KB; the lazy-imported
azure-devops-node-api SDK lives in a separate ~2.7 MB chunk loaded
only when an ADO REST call is needed. Pipelines that bypass or rely
only on pipeline-variable facts never load the SDK.
If a future bundle blows the budget:
- First, check ncc’s
--minifyand--targetflags. - If still too large, weigh dropping
azure-devops-node-apiin favor of hand-rolledfetchfor the hot endpoints. The retry / timeout / pagination helpers insrc/shared/ado-client.tsare written so they could wrap either approach.
Out of scope (explicitly)
Section titled “Out of scope (explicitly)”- A user-facing
ado-script:front-matter block. Letting authors run arbitrary TypeScript at pipeline runtime would bypass the safe-output trust boundary and require sandboxing the project does not have. - Migrating the safe-output executors (
src/safe_outputs/*.rs) to Node. Stage 3 keeps a Rust-only execution path. - Migrating the agent-stats parser. It runs in-pipeline as part of Stage 1 wrap-up and has no TypeScript dependency need.
- Bundling Node itself. Pipelines install Node via
UseNode@1.
See also
Section titled “See also”- Filter IR — the IR consumed by
gate.js. - Runtime Imports — author-facing marker syntax for
import.js. - Execution Context — user-facing configuration for all execution-context contributors.
- Extending the Compiler — generic compiler-extension guide.