feat(compile): add Conclusion job for pipeline failure reporting#1076
feat(compile): add Conclusion job for pipeline failure reporting#1076jamesadevine wants to merge 15 commits into
Conversation
1720ec7 to
67e3050
Compare
🔍 Rust PR ReviewSummary: Generally solid — the Conclusion job wires up correctly and the TypeScript logic handles edge cases well. Two issues worth fixing before merge. Findings🐛 Bugs / Logic Issues
|
🔍 Rust PR ReviewSummary: Needs changes — two correctness bugs that will silently break the core feature (failure detection and per-tool opt-out), plus a data-loss codemod bug. Findings🐛 Bugs / Logic Issues1. let agent_result = format!("$[dependencies.{}.result]", agent_id.as_str());
// ...
conclusion_step = conclusion_step
.with_env("AW_AGENT_RESULT", EnvValue::Literal(agent_result))
.with_env("AW_DETECTION_RESULT", EnvValue::Literal(detection_result))
.with_env("AW_SAFEOUTPUTS_RESULT", EnvValue::Literal(safeoutputs_result));ADO only evaluates The fix is to hoist these to job-level 2. Per-tool configs are emitted by Rust but never consumed by The Rust compiler passes for tool_key in &["noop", "missing-tool", "missing-data"] {
if let Some(tool_config) = front_matter.safe_outputs.get(*tool_key) {
let env_key = format!("AW_{}_CONFIG", tool_key.to_uppercase().replace('-', "_"));
// ...
}
}But
The conclusion test ( 3. Codemod silently drops non-mapping let Value::Mapping(wi_map) = wi_val else {
// work-item was present but not a mapping — put it back as-is
continue; // <── wi_val was already removed; this drops it
};
let Value::Mapping(wi_map) = wi_val else {
tool_map.insert(wi_key, wi_val); // restore
continue;
};4. Default work-item type is workItemType: readOptionalEnv(env, "AW_WORK_ITEM_TYPE") ?? "Bug",
✅ What Looks Good
|
🔍 Rust PR ReviewSummary: Solid architecture with one wiring gap and a few logic nuances worth addressing before merge. Findings🐛 Bugs / Logic Issues
Users can configure per-tool settings via
if (!config.reportFailureAsWorkItem) {
logInfo("Conclusion work-item filing disabled via AW_REPORT_FAILURE_AS_WORK_ITEM=false");
return 0; // exits before filing noop/missing-tool/missing-data WIs too
}The name and the PR description ("Global opt-out — no failure work items at all") suggest this flag is intentionally global. But return {
enabled: config.reportFailureAsWorkItem, // always true here because of the early return above
...
};This is dead code — No per-tool opt-out path for
This should be documented; it's a footgun for users who want "no pipeline-failure WIs but still want noop WIs."
|
🔍 Rust PR ReviewSummary: needs changes — two correctness bugs and a documentation/front-matter mismatch that would produce hard parse errors for users following the docs. Findings🐛 Bugs / Logic Issues
if has_write_sc {
conclusion_step = conclusion_step.with_env(
"SYSTEM_ACCESSTOKEN",
EnvValue::AdoMacro("$(SC_WRITE_TOKEN)"),
);
}
The correct form is
conclusion:
work-item-type: Bug
area-path: "MyProject\\MyTeam"But The PR description itself says "Lives entirely under
The table in
The test fixture uses 🔒 Security Concerns
|
🔍 Rust PR ReviewSummary: Looks good overall — clean architecture shift, solid test coverage, and the migration path (codemod + backward-compat) is well-handled. A few issues worth addressing before merge. Findings🐛 Bugs / Logic Issues
|
🔍 Rust PR ReviewSummary: Solid implementation with one real resilience bug worth fixing before merge; everything else is high quality. Findings🐛 Bugs / Logic Issues
|
🔍 Rust PR ReviewSummary: Good architectural direction — moving WI filing from Stage 3 into an always-running Conclusion job is the right separation of concerns. One real bug will silently neutralise the manifest-based signal reporting (noop/missing_tool/missing_data) on every pipeline run. Findings🐛 Bugs / Logic Issues
The Conclusion job downloads the // build_conclusion_job – download step
.with_input("artifact", "safe_outputs")
.with_input("path", "$(Pipeline.Workspace)/conclusion_inputs");
// ...and the env var:
EnvValue::Literal("$(Pipeline.Workspace)/conclusion_inputs".to_string()),But the "ado-aw execute ... --safe-output-dir \"$(Pipeline.Workspace)/analyzed_outputs_$(Build.BuildId)\" ..."The ...and skip noop/missing_tool/missing_data signal reporting entirely. Pipeline-failure reporting (driven by upstream job results, not the manifest) is unaffected and will still work. Fix — add a copy into cp "$(Pipeline.Workspace)/analyzed_outputs_$(Build.BuildId)/safe-outputs-executed.ndjson" \
"$(Agent.TempDirectory)/staging/safe-outputs-executed.ndjson" 2>/dev/null || true
|
🔍 Rust PR ReviewSummary: Solid feature with good test coverage — one correctness bug needs fixing before merge. Findings🐛 Bugs / Logic Issues
|
🔍 Rust PR ReviewSummary: Solid architecture, but there's a blocking bug: the documented Findings🐛 Bugs / Logic Issues
🔒 Security Concerns
|
Add an always-running Conclusion job to the canonical pipeline shape that reports pipeline failures and diagnostic signals (noop, missing-tool, missing-data) to Azure DevOps work items. Key changes: - New ConclusionConfig front-matter type (conclusion: block) - New conclusion.js ado-script bundle (TypeScript, ncc-bundled) - build_conclusion_job() in agentic_pipeline.rs with condition: always() - Work-item write helpers in shared/wit.ts (create, comment, WIQL dedup) - Removed work-item filing from noop/missing-tool safe-output executors (moved to conclusion job - separation of concerns) - Renamed src/safeoutputs/ to src/safe_outputs/ (snake_case convention) Pipeline shape: Setup -> Agent -> Detection -> SafeOutputs -> Teardown -> Conclusion Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…oss-platform path in conclusion test - noop/missing-tool now return plain success (not warning) since work-item filing moved to the Conclusion job - conclusion test uses stringContaining for cross-platform path separators Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Use anyhow::Context instead of unwrap_or_default for tags serialization (propagates errors instead of silently dropping tags) - Change DownloadPipelineArtifact step to condition: always() with continueOnError: true (avoids red failure noise when artifact is unavailable due to early Agent failure) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tten codemod Non-breaking change: conclusion job config now lives under safe-outputs: (aligned with gh-aw's pattern) instead of a separate conclusion: block. Three opt-out levels: - Global: safe-outputs: report-failure-as-work-item: false - Per-tool: safe-outputs: noop: report-as-work-item: false - Disable tool: safe-outputs: noop: false The Conclusion job is always emitted when safe-outputs: exists (gh-aw pattern where noop is always-on). Per-tool config (title-prefix, work-item-type, area-path, tags) is passed as JSON env vars (AW_NOOP_CONFIG, AW_MISSING_TOOL_CONFIG, AW_MISSING_DATA_CONFIG). Adds codemod 0003_flatten_work_item_config that migrates the old nested work-item: sub-block to the flat form (title -> title-prefix, enabled -> report-as-work-item). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…onfigured When permissions.write is set, the Conclusion job must use the service-connection-minted token (SC_WRITE_TOKEN) instead of System.AccessToken — matching the SafeOutputs job's pattern. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1. Hoist $[dependencies.*.result] to job-level variables (ADO only evaluates runtime expressions in variables:/condition:, NOT step env). Step env now uses $(AW_AGENT_RESULT) macro references. 2. conclusion.js now reads per-tool configs from AW_NOOP_CONFIG, AW_MISSING_TOOL_CONFIG, AW_MISSING_DATA_CONFIG env vars. Per-tool opt-out (report-as-work-item: false) and per-tool overrides (title-prefix, work-item-type, area-path, tags) now actually work. 3. Codemod 0003 no longer drops non-mapping work-item: values — restores the value to the mapping if it is not a Mapping (e.g. work-item: true). 4. Default work-item type changed from Bug to Task (matching the documented default and the old WorkItemReportConfig convention). Filed #1081 for the type-level fix (EnvValue::RuntimeExpression variant). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Matches gh-aw pattern: each per-tool config field gets its own env var (AW_NOOP_TITLE_PREFIX, AW_NOOP_WORK_ITEM_TYPE, etc.) instead of a single AW_NOOP_CONFIG JSON blob. Avoids ADO variable expansion corrupting JSON payloads. Also removes dead global env var reads from conclusion.js (AW_WORK_ITEM_TYPE, AW_WORK_ITEM_AREA_PATH, etc.) that the compiler never emitted. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…kout 1. SC_WRITE_TOKEN: use EnvValue::secret (not AdoMacro) to avoid $($(SC_WRITE_TOKEN)) double expansion and allowlist bypass 2. Remove checkout_self_step from Conclusion job — only needs downloaded bundle and pipeline artifact, saves ~30s 3. Rename shadowed prefix → env_prefix in per-tool loop 4. Fix docs/conclusion.md: config lives under safe-outputs: not a stale conclusion: block; fix field name (title-prefix not work-item-title) and default (Task not Bug) 5. Fix docs/front-matter.md: remove conclusion: field reference Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
AdoMacro values must be bare variable names (e.g. "Build.Reason"); the $() wrapper is added by lowering. Direct construction bypassing the ado_macro() factory could pass pre-wrapped names like "$(SC_WRITE_TOKEN)" which would emit $($(SC_WRITE_TOKEN)) — a double expansion that ADO silently drops to an empty string. The guard rejects names containing $, (, or ) in both lower_env_value and lower_env_value_as_expr_atom, with an actionable error message pointing to PipelineVar/secret. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…r fix
1. titlePrefix now supports {pipeline_name}/{signal} placeholders via
renderTitle (was dead code — always called with undefined template)
2. report-as-work-item env var uses as_bool()/as_str() instead of
v.to_string() which JSON-encodes strings as "\"false\"" — silently
inverting the opt-out
3. Document pipeline_failure has no per-tool config (deliberate)
4. Document enabled field is always true from conclusion.js (guarded
by main() early return and per-tool opt-out in fileSignal)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…fy pipeline_failure 1. conclusion.js step now guards against missing node binary — logs a warning and exits 0 instead of failing with exit 127. This ensures the Conclusion job never fails the pipeline on infra issues (UseNode failure, missing runner tooling). 2. Remove duplicated comment block in build_conclusion_job. 3. Add comment in fileSignal explaining why pipeline_failure has no per-tool config entry (intentional, matches gh-aw). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…bVariable import 1. Copy safe-outputs-executed.ndjson from analyzed_outputs to the safe_outputs artifact staging directory. Without this, the Conclusion job could never find the manifest — noop/missing-tool/ missing-data signal reporting was silently broken. 2. Move JobVariable import to file-level (consistent with other IR imports). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…align title-prefix
1. Copy safe-outputs-executed.ndjson into the safe_outputs artifact
so the Conclusion job can find noop/missing-tool/missing-data
entries. Without this, diagnostic signal reporting was silently
skipped (pipeline-failure reporting was unaffected).
2. Extend bash guard to also check conclusion.js exists on disk
before running node — handles download failures gracefully.
3. Align title-prefix with gh-aw: it is a prefix prepended to the
pipeline name (${titlePrefix} ${pipelineName}), not a template
with placeholder substitution. Matches gh-aw missing_issue_helpers.
4. Move JobVariable import to file-level (consistency with other IR
imports).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
validate_safe_outputs_keys rejected report-failure-as-work-item as an unrecognised tool name, breaking the documented global opt-out. Add SAFE_OUTPUT_CONFIG_KEYS for global config-level keys (not tool names) and allow them in the validation pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ion job Addresses review feedback plus two latent bugs found while re-evaluating against main (which now has the supply-chain mirror feature, #1080): - Conclusion job now reuses ado_script::install_and_download_steps_typed, which (a) respects the supply-chain feed mirror and (b) unzips to /tmp/ado-aw-scripts/ (the hand-rolled copy double-nested to /tmp/ado-aw-scripts/ado-script/ado-script/, so conclusion.js was never at the referenced path). - release.yml now auto-globs ado-script/*.js instead of a manual file list, so every built bundle ships. conclusion.js was missing from the manual list and would never have reached a release. - New bundle-coverage vitest guards that every src/<name>/ bundle dir is wired into the npm build chain (the safety net for auto-globbing). - buildTitle truncates to ADO's 255-char System.Title limit. - Ported the WIQL trust-boundary comment to wit.ts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
c3188ff to
623d410
Compare
🔍 Rust PR ReviewSummary: Solid implementation of the Conclusion job feature — architecture is well-thought-out and follows existing conventions. Three items worth addressing before merge. Findings🐛 Bugs / Logic Issues
When
|
Summary
Adds an always-running Conclusion job to the canonical pipeline shape, mirroring gh-aw's conclusion job pattern. The Conclusion job reports pipeline failures and diagnostic signals (noop, missing-tool, missing-data) to Azure DevOps work items.
Pipeline shape
The Conclusion job is always emitted when
safe-outputs:exists (gh-aw pattern — noop is always-on).Configuration (under
safe-outputs:)Three opt-out levels:
Per-tool config (aligned with gh-aw's flat field style):
Key changes
scripts/ado-script/src/conclusion/index.ts→conclusion.js(ncc-bundled)build_conclusion_job()inagentic_pipeline.rs, emitted whensafe_outputsis non-emptysafe-outputs:— no separateconclusion:block0003_flatten_work_item_configmigrates oldnoop: work-item: {title, ...}→ flatnoop: {title-prefix, ...}createWorkItem,addWorkItemComment,findWorkItemByTitle,fileOrAppendWorkIteminshared/wit.tsnoop.rs/missing_tool.rs(now handled by conclusion job)src/safeoutputs/→src/safe_outputs/(snake_case consistency)How it works
safe-outputs:is configuredsafe_outputsartifact (containssafe-outputs-executed.ndjson)conclusion.jsreads upstream job results + the NDJSON manifest + per-tool config (passed asAW_NOOP_CONFIGetc. JSON env vars)Test plan
cargo build✅cargo test conclusion— 4 Rust tests pass (1 types + 3 compiler integration)cargo test flatten_work_item— 6 codemod tests passnpx vitest run src/conclusion— 8 TypeScript tests pass (conclusion.js logic)npx vitest run src/shared/__tests__/wit.test.ts— WI write helper tests passnpm run typecheck— TypeScript type-checks clean