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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/rust-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,16 @@ jobs:

- uses: Swatinem/rust-cache@v2

- name: Install shellcheck
# ubuntu-latest already ships shellcheck, but install explicitly to
# guarantee it on every refresh of the runner image and to surface
# a clear failure when the bash-lint integration test is enforced.
run: sudo apt-get update && sudo apt-get install -y shellcheck

- name: Build
run: cargo build --verbose

- name: Run tests
env:
ENFORCE_BASH_LINT: "1"
run: cargo test --verbose
19 changes: 19 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,25 @@ cargo test
cargo clippy
```

### Bash step lint

The `tests/bash_lint_tests.rs` integration test compiles a representative set
of fixtures and runs `shellcheck` against every literal `bash:` body in the
generated YAML. It catches silent-failure patterns that ADO's "fail on last
command" default would let through (e.g. `cd "$X"` without `|| exit`, tilde
inside double quotes, masked-return assignments).

The test is skipped if `shellcheck` is not on PATH. Install locally with
`brew install shellcheck` (macOS) or `apt-get install -y shellcheck` (Debian
/ Ubuntu); CI installs it in `.github/workflows/rust-tests.yml` and sets
`ENFORCE_BASH_LINT=1` so a missing shellcheck becomes a hard failure rather
than a silent skip.

When adding a new bash step, run `cargo test --test bash_lint_tests` and fix
anything it flags. If a finding is genuinely intentional, add a
`# shellcheck disable=SCxxxx` comment immediately above the offending line in
the bash body — shellcheck honours the directive and it's inert at runtime.

## Common Tasks

### Compile a markdown pipeline
Expand Down
42 changes: 42 additions & 0 deletions docs/extending.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,45 @@ To add a new filter type:
`validate_pr_filters()` or `validate_pipeline_filters()`
5. **Write tests** — lowering test, validation test, and codegen test in
`filter_ir.rs`

## Bash steps in pipeline templates

Pipeline templates and Rust step generators emit dozens of multi-line `bash:`
steps. ADO bash steps fail only on the *last* command's exit status by
default, so a chain like `mkdir … && curl … && cd … && cmd` can silently
swallow earlier failures.

Rather than spread `set -eo pipefail` boilerplate across every step, the
project enforces hygiene via `tests/bash_lint_tests.rs`, which compiles a set
of fixtures and runs `shellcheck` against every literal `bash:` body in the
generated YAML. The lint catches:

- **SC2164** — `cd $X` without `|| exit` (the canonical silent-failure)
- **SC2155** — `local var=$(cmd)` masking the inner exit code
- **SC2086 / SC2046** — unquoted variables / command substitutions
- **SC2154** — variables referenced but never assigned
- **SC2088** — tilde inside double quotes (does not expand at all)

When you add or modify a bash step:

1. Run `cargo test --test bash_lint_tests` (locally requires `shellcheck` on
PATH; install with `brew install shellcheck` or
`apt-get install -y shellcheck`). CI sets `ENFORCE_BASH_LINT=1` so a
missing shellcheck becomes a hard failure rather than a silent skip.
2. Fix any finding by adjusting the bash. Common fixes: `cd "$X" || exit 1`,
`exit "$CODE"`, `"$HOME/.foo"` instead of `"~/.foo"`, quoting variable
expansions.
3. If a finding is genuinely intentional, add a
`# shellcheck disable=SCxxxx` comment immediately above the line in the
bash body. Such directives are bash comments and have no runtime effect.

Do **not** sprinkle `set -eo pipefail` into every step to silence the lint —
that approach was tried (PR #492) and was rejected because it adds noise,
drifts as new steps are added, and doesn't address the actual silent-failure
patterns that the lint surfaces. Use targeted `set -eo pipefail` only when a
step has a real fail-fast requirement that the lint cannot express (the
current uses are on AWF/MCPG download and the `tee`-piped agent run).

The exclude list (`SC1090`, `SC1091`, `SC2034`, `SC2016`) is documented in
`tests/bash_lint_tests.rs`. Each entry has a justification — do not extend
without one.
1 change: 1 addition & 0 deletions src/compile/extensions/trigger_filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ impl CompilerExtension for TriggerFiltersExtension {
let mut steps = Vec::new();
steps.push(format!(
r#"- bash: |
set -eo pipefail
mkdir -p /tmp/ado-aw-scripts
curl -fsSL "{RELEASE_BASE_URL}/v{version}/checksums.txt" -o /tmp/ado-aw-scripts/checksums.txt
curl -fsSL "{RELEASE_BASE_URL}/v{version}/scripts.zip" -o /tmp/ado-aw-scripts/scripts.zip
Expand Down
37 changes: 22 additions & 15 deletions src/data/1es-base.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ extends:
{{ engine_install_steps }}

- bash: |
set -eo pipefail
COMPILER_VERSION="{{ compiler_version }}"
DOWNLOAD_DIR="$(Pipeline.Workspace)/agentic-pipeline-compiler"
DOWNLOAD_URL="https://github.com/githubnext/ado-aw/releases/download/v${COMPILER_VERSION}/ado-aw-linux-x64"
Expand All @@ -70,7 +71,7 @@ extends:
curl -fsSL -o "$DOWNLOAD_DIR/checksums.txt" "$CHECKSUM_URL"

echo "Verifying checksum..."
cd "$DOWNLOAD_DIR"
cd "$DOWNLOAD_DIR" || exit 1
grep "ado-aw-linux-x64" checksums.txt | sha256sum -c -
mv ado-aw-linux-x64 ado-aw
chmod +x ado-aw
Expand Down Expand Up @@ -156,7 +157,7 @@ extends:
curl -fsSL -o "$DOWNLOAD_DIR/checksums.txt" "$CHECKSUM_URL"

echo "Verifying checksum..."
cd "$DOWNLOAD_DIR"
cd "$DOWNLOAD_DIR" || exit 1
grep "awf-linux-x64" checksums.txt | sha256sum -c -
mv awf-linux-x64 awf
chmod +x awf
Expand Down Expand Up @@ -204,6 +205,7 @@ extends:

# Wait for server to be ready
READY=false
# shellcheck disable=SC2034 # i is intentionally unused; wait-N-times loop
for i in $(seq 1 30); do
if curl -sf "http://localhost:$SAFE_OUTPUTS_PORT/health" > /dev/null 2>&1; then
echo "SafeOutputs HTTP server is ready"
Expand All @@ -221,14 +223,15 @@ extends:
# Start MCP Gateway (MCPG) on host
- bash: |
# Substitute runtime values into MCPG config
MCPG_CONFIG=$(cat /tmp/awf-tools/staging/mcpg-config.json \
| sed "s|\${SAFE_OUTPUTS_PORT}|$(SAFE_OUTPUTS_PORT)|g" \
| sed "s|\${SAFE_OUTPUTS_API_KEY}|$(SAFE_OUTPUTS_API_KEY)|g" \
| sed "s|\${MCP_GATEWAY_API_KEY}|$(MCP_GATEWAY_API_KEY)|g")
MCPG_CONFIG=$(sed \
-e "s|\${SAFE_OUTPUTS_PORT}|$(SAFE_OUTPUTS_PORT)|g" \
-e "s|\${SAFE_OUTPUTS_API_KEY}|$(SAFE_OUTPUTS_API_KEY)|g" \
-e "s|\${MCP_GATEWAY_API_KEY}|$(MCP_GATEWAY_API_KEY)|g" \
/tmp/awf-tools/staging/mcpg-config.json)

# Log the template config (before API key substitution) for debugging.
echo "Starting MCPG with config template:"
cat /tmp/awf-tools/staging/mcpg-config.json | python3 -m json.tool
python3 -m json.tool < /tmp/awf-tools/staging/mcpg-config.json

# Remove any leftover container or stale output from a previous interrupted run
# (--rm only cleans up on clean exit; OOM/SIGKILL may leave it behind)
Expand Down Expand Up @@ -267,6 +270,7 @@ extends:

# Wait for MCPG to be ready
READY=false
# shellcheck disable=SC2034 # i is intentionally unused; wait-N-times loop
for i in $(seq 1 30); do
if curl -sf "http://localhost:{{ mcpg_port }}/health" > /dev/null 2>&1; then
echo "MCPG is ready"
Expand All @@ -284,6 +288,7 @@ extends:
# Health check passing doesn't guarantee stdout is flushed, so poll.
echo "Waiting for gateway output file..."
GATEWAY_READY=false
# shellcheck disable=SC2034 # i is intentionally unused; wait-N-times loop
for i in $(seq 1 15); do
if [ -s "$GATEWAY_OUTPUT" ] && jq -e '.mcpServers' "$GATEWAY_OUTPUT" > /dev/null 2>&1; then
echo "Gateway output is ready"
Expand Down Expand Up @@ -361,7 +366,7 @@ extends:
"$(Pipeline.Workspace)/awf/awf" logs summary --source "$(Agent.TempDirectory)/staging/logs/firewall" 2>/dev/null || true
fi

exit $AGENT_EXIT_CODE
exit "$AGENT_EXIT_CODE"
displayName: "Run copilot (AWF network isolated)"
workingDirectory: {{ working_directory }}
env:
Expand Down Expand Up @@ -433,6 +438,7 @@ extends:
{{ engine_install_steps }}

- bash: |
set -eo pipefail
COMPILER_VERSION="{{ compiler_version }}"
DOWNLOAD_DIR="$(Pipeline.Workspace)/agentic-pipeline-compiler"
DOWNLOAD_URL="https://github.com/githubnext/ado-aw/releases/download/v${COMPILER_VERSION}/ado-aw-linux-x64"
Expand All @@ -444,7 +450,7 @@ extends:
curl -fsSL -o "$DOWNLOAD_DIR/checksums.txt" "$CHECKSUM_URL"

echo "Verifying checksum..."
cd "$DOWNLOAD_DIR"
cd "$DOWNLOAD_DIR" || exit 1
grep "ado-aw-linux-x64" checksums.txt | sha256sum -c -
mv ado-aw-linux-x64 ado-aw
chmod +x ado-aw
Expand All @@ -469,7 +475,7 @@ extends:
curl -fsSL -o "$DOWNLOAD_DIR/checksums.txt" "$CHECKSUM_URL"

echo "Verifying checksum..."
cd "$DOWNLOAD_DIR"
cd "$DOWNLOAD_DIR" || exit 1
grep "awf-linux-x64" checksums.txt | sha256sum -c -
mv awf-linux-x64 awf
chmod +x awf
Expand All @@ -487,8 +493,8 @@ extends:
displayName: "Pre-pull AWF container images (v{{ firewall_version }})"

- bash: |
mkdir -p {{ working_directory }}/safe_outputs
cp -a "$(Pipeline.Workspace)/agent_outputs_$(Build.BuildId)/." {{ working_directory }}/safe_outputs
mkdir -p "{{ working_directory }}/safe_outputs"
cp -a "$(Pipeline.Workspace)/agent_outputs_$(Build.BuildId)/." "{{ working_directory }}/safe_outputs"
displayName: "Prepare safe outputs for analysis"

- bash: |
Expand Down Expand Up @@ -526,7 +532,7 @@ extends:
| tee "$THREAT_OUTPUT_FILE" \
&& AGENT_EXIT_CODE=0 || AGENT_EXIT_CODE=$?

exit $AGENT_EXIT_CODE
exit "$AGENT_EXIT_CODE"
displayName: "Run threat analysis (AWF network isolated)"
workingDirectory: {{ working_directory }}
env:
Expand All @@ -550,7 +556,7 @@ extends:
RESULT_LINE=$(grep "THREAT_DETECTION_RESULT:" "$(Agent.TempDirectory)/threat-analysis-output.txt" | tail -1)
if [ -n "$RESULT_LINE" ]; then
# Extract JSON after the prefix
JSON_CONTENT=$(echo "$RESULT_LINE" | sed 's/.*THREAT_DETECTION_RESULT://')
JSON_CONTENT="${RESULT_LINE##*THREAT_DETECTION_RESULT:}"
echo "$JSON_CONTENT" > "$(Agent.TempDirectory)/analyzed_outputs/threat-analysis.json"
echo "Extracted threat analysis JSON:"
cat "$(Agent.TempDirectory)/analyzed_outputs/threat-analysis.json"
Expand Down Expand Up @@ -635,6 +641,7 @@ extends:
artifact: analyzed_outputs_$(Build.BuildId)

- bash: |
set -eo pipefail
COMPILER_VERSION="{{ compiler_version }}"
DOWNLOAD_DIR="$(Pipeline.Workspace)/agentic-pipeline-compiler"
DOWNLOAD_URL="https://github.com/githubnext/ado-aw/releases/download/v${COMPILER_VERSION}/ado-aw-linux-x64"
Expand All @@ -646,7 +653,7 @@ extends:
curl -fsSL -o "$DOWNLOAD_DIR/checksums.txt" "$CHECKSUM_URL"

echo "Verifying checksum..."
cd "$DOWNLOAD_DIR"
cd "$DOWNLOAD_DIR" || exit 1
grep "ado-aw-linux-x64" checksums.txt | sha256sum -c -
mv ado-aw-linux-x64 ado-aw
chmod +x ado-aw
Expand Down
Loading
Loading