diff --git a/.github/workflows/ado-script.yml b/.github/workflows/ado-script.yml new file mode 100644 index 00000000..dd2d6613 --- /dev/null +++ b/.github/workflows/ado-script.yml @@ -0,0 +1,67 @@ +name: ado-script Workspace + +on: + pull_request: + paths: + - "scripts/ado-script/**" + - "src/compile/filter_ir.rs" + - "src/compile/extensions/trigger_filters.rs" + - "Cargo.toml" + - "Cargo.lock" + - ".github/workflows/ado-script.yml" + +env: + CARGO_TERM_COLOR: always + +jobs: + ado-script: + name: Build, Test & Drift-Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: scripts/ado-script/package-lock.json + + - name: Install workspace dependencies + working-directory: scripts/ado-script + run: npm ci + + - name: Regenerate types from Rust IR (codegen) + working-directory: scripts/ado-script + run: npm run codegen + + - name: Verify generated TypeScript is up to date + run: | + if ! git diff --exit-code -- scripts/ado-script/src/shared/types.gen.ts; then + echo "" + echo "::error::types.gen.ts is out of date with the Rust IR." + echo "Run 'cd scripts/ado-script && npm run codegen' and commit the result." + exit 1 + fi + + - name: Run TypeScript tests + working-directory: scripts/ado-script + run: npm test + + - name: Type-check + working-directory: scripts/ado-script + run: npm run typecheck + + - name: Build bundle (gate.js) + working-directory: scripts/ado-script + run: npm run build + + - name: Smoke-test bundle + working-directory: scripts/ado-script + run: npx vitest run -c vitest.config.smoke.ts + + - name: E2E gate test + run: cargo test --test gate_e2e -- --ignored --nocapture diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b2090746..c45b0243 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,11 +56,25 @@ jobs: cd target/release cp ado-aw ado-aw-linux-x64 - - name: Package scripts bundle + - name: Set up Node.js for ado-script bundle + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: scripts/ado-script/package-lock.json + + - name: Build ado-script TypeScript bundle (gate.js) + working-directory: scripts/ado-script + run: | + npm ci + npm run build + # `npm run build` runs codegen + ncc and outputs dist/gate/index.js. + + - name: Package ado-script bundle run: | set -euo pipefail cd scripts - zip -r ../scripts.zip . + zip -r ../ado-script.zip ado-script/dist - name: Upload release assets env: @@ -69,7 +83,7 @@ jobs: TAG="${{ needs.release-please.outputs.tag_name || github.event.inputs.tag_name }}" gh release upload "$TAG" \ target/release/ado-aw-linux-x64 \ - scripts.zip \ + ado-script.zip \ --clobber build-windows: @@ -159,13 +173,13 @@ jobs: TAG="${{ needs.release-please.outputs.tag_name || github.event.inputs.tag_name }}" gh release download "$TAG" \ --pattern "ado-aw-*" \ - --pattern "scripts.zip" \ + --pattern "ado-script.zip" \ --repo "${{ github.repository }}" test -f ado-aw-linux-x64 || { echo "Missing ado-aw-linux-x64"; exit 1; } test -f ado-aw-windows-x64.exe || { echo "Missing ado-aw-windows-x64.exe"; exit 1; } test -f ado-aw-darwin-arm64 || { echo "Missing ado-aw-darwin-arm64"; exit 1; } - test -f scripts.zip || { echo "Missing scripts.zip"; exit 1; } - sha256sum ado-aw-linux-x64 ado-aw-windows-x64.exe ado-aw-darwin-arm64 scripts.zip > checksums.txt + test -f ado-script.zip || { echo "Missing ado-script.zip"; exit 1; } + sha256sum ado-aw-linux-x64 ado-aw-windows-x64.exe ado-aw-darwin-arm64 ado-script.zip > checksums.txt - name: Upload checksums env: diff --git a/.gitignore b/.gitignore index 05451379..78d855eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ target examples/sample-agent.yml +scripts/gate.js *.pyc __pycache__/ diff --git a/AGENTS.md b/AGENTS.md index 84a93381..8b8743e9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -120,8 +120,8 @@ Every compiled pipeline runs as three sequential jobs: ├── ado-aw-derive/ # Proc-macro crate: #[derive(SanitizeConfig)], #[derive(SanitizeContent)] ├── examples/ # Example agent definitions ├── scripts/ # Supporting scripts shipped as release artifacts -│ ├── gate-eval.py # Python gate evaluator (data-driven filter evaluation) -│ └── gate-spec.schema.json # JSON Schema for gate spec (generated from Rust types) +│ ├── ado-script/ # TypeScript workspace for bundled gate.js (and future bundles) +│ └── gate.js # Bundled gate evaluator (built from scripts/ado-script/, see docs/ado-script.md) ├── tests/ # Integration tests and fixtures ├── docs/ # Per-concept reference documentation (see index below) ├── Cargo.toml # Rust dependencies @@ -133,6 +133,7 @@ Every compiled pipeline runs as three sequential jobs: - **Language**: Rust (2024 edition) - Note: Rust 2024 edition exists and is the edition used by this project - **CLI Framework**: clap v4 with derive macros - **Error Handling**: anyhow for ergonomic error propagation +- **Bundled scripts**: TypeScript + ncc (`scripts/ado-script/`) — compiled gate evaluator and future internal helpers; see [`docs/ado-script.md`](docs/ado-script.md). - **Async Runtime**: tokio with full features - **YAML Parsing**: serde_yaml - **MCP Server**: rmcp with server and transport-io features @@ -183,6 +184,9 @@ index to jump to the right page. - [`docs/filter-ir.md`](docs/filter-ir.md) — filter expression IR specification: `Fact`/`Predicate` types, three-pass compilation (lower → validate → codegen), gate step generation, adding new filter types. +- [`docs/ado-script.md`](docs/ado-script.md) — `ado-script` workspace + (`scripts/ado-script/`): the bundled TypeScript runtime helpers (today: + `gate.js`), schemars-driven type codegen, and the A2 design decision. - [`docs/local-development.md`](docs/local-development.md) — local development setup notes. diff --git a/ado-script-design.md b/ado-script-design.md new file mode 100644 index 00000000..835e2b20 --- /dev/null +++ b/ado-script-design.md @@ -0,0 +1,235 @@ +# Design exploration: an `actions/github-script` analog for ADO + +> **Mode**: thought experiment. No scope committed. Goal is to map the design +> space, surface trade-offs, and identify the highest-leverage entry point if +> we ever pull the trigger. + +## 1. The concern that motivates this + +`scripts/gate-eval.py` is already 388 lines. It conflates several +responsibilities: + +1. **Spec deserialization** (base64 → JSON → dict) +2. **Fact acquisition** (env vars, REST API for PR metadata, REST API for + iteration changes, datetime arithmetic) — including auth, URL building, + retry/timeout semantics +3. **Predicate evaluation** (10 predicate types, recursive, with overnight + time-window arithmetic and `_strip_ref_prefix` quirks) +4. **Failure-policy state machine** (`fail_closed` / `fail_open` / + `skip_dependents`, with transitive propagation through fact dependencies) +5. **ADO logging-command emission** (`##vso[...]`) +6. **Self-cancel** (PATCH to builds API) + +Every new filter type forces a coordinated change across: +`filter_ir.rs` (Rust IR) → JSON schema → `gate-eval.py` evaluator → +fixtures → docs. The Python file has no static typing, no test harness in CI +(only Rust-side spec-serialization tests), and grows with the IR. + +There are at least two more places where a similar Python/bash blob is on the +roadmap or already exists: + +- The Stage-3 safe-output executor — currently a typed Rust binary + (`src/execute.rs` + `src/safeoutputs/*.rs`). Strong story today, but every + ADO interaction is hand-rolled HTTP via `reqwest`. +- The agent shim & the prepare/setup steps — currently bash interleaved with + ADO macro expansion. + +The user's instinct: rather than letting `gate-eval.py` grow into a +monstrosity (and rather than reinventing it for each new use case), give +ado-aw a single, well-tested primitive — the way `actions/github-script` +gives gh-aw its "drop in JS, get a pre-authed Octokit + context" lever. + +## 2. What `actions/github-script` actually is + +For grounding, the github-script contract: + +```yaml +- uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { data } = await github.rest.issues.createComment({ + owner: context.repo.owner, repo: context.repo.repo, + issue_number: context.issue.number, body: 'hi', + }); + core.setOutput('comment-id', data.id); +``` + +Mechanics worth copying: + +| Property | Detail | +|---|---| +| Language | Node.js (single ecosystem, ncc-bundled, no `npm install` at runtime) | +| Auth | Pre-injected `github` Octokit, token from input | +| Context | Pre-injected `context` (event payload + repo/issue/PR shortcuts) | +| Helpers | `core` (output/secrets/log), `glob`, `io`, `exec`, `fetch` | +| Wrapper | `(async () => {