From 1784207949553de590e997bd536ae8fe9506ef19 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 00:17:39 +0000 Subject: [PATCH 1/5] feat: remove conditional workflow imports Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .changeset/major-remove-imports-if.md | 9 ++ .github/aw/experiments.md | 1 + .github/aw/syntax-tools-imports.md | 2 + .../daily-safe-output-optimizer.lock.yml | 28 +++--- .../workflows/daily-safe-output-optimizer.md | 19 +--- .../shared/aw-logs-24h-fetch-prompt.md | 15 +++ .../shared/aw-logs-24h-fetch-setup.md | 15 +++ docs/src/content/docs/reference/imports.md | 2 + pkg/parser/import_bfs.go | 94 +++++-------------- pkg/parser/import_bfs_test.go | 67 +------------ pkg/parser/import_field_extractor.go | 94 +++---------------- pkg/parser/import_field_extractor_test.go | 25 +---- pkg/parser/import_processor.go | 2 - pkg/parser/import_remote.go | 1 - pkg/parser/schemas/main_workflow_schema.json | 16 ---- pkg/workflow/compiler_yaml.go | 31 ------ pkg/workflow/compiler_yaml_test.go | 9 -- 17 files changed, 104 insertions(+), 326 deletions(-) create mode 100644 .changeset/major-remove-imports-if.md create mode 100644 .github/workflows/shared/aw-logs-24h-fetch-prompt.md create mode 100644 .github/workflows/shared/aw-logs-24h-fetch-setup.md diff --git a/.changeset/major-remove-imports-if.md b/.changeset/major-remove-imports-if.md new file mode 100644 index 00000000000..33b938e56e7 --- /dev/null +++ b/.changeset/major-remove-imports-if.md @@ -0,0 +1,9 @@ +"gh-aw": major + +Remove `imports.if` support from workflow frontmatter. + +**⚠️ Breaking Change**: `imports:` entries no longer accept an `if` condition because conditional imports can change workflow setup and security posture at runtime. + +**Migration guide:** +- Keep security-relevant imports unconditional. +- For experiment-specific prompt variants, use `{{#if experiments. ...}}` with `{{#runtime-import ...}}` in the workflow body instead. diff --git a/.github/aw/experiments.md b/.github/aw/experiments.md index 8235cdc7750..49bae34cb88 100644 --- a/.github/aw/experiments.md +++ b/.github/aw/experiments.md @@ -292,5 +292,6 @@ experiments: - ❌ **Interpreting early results** (<~20 runs/variant) — chance variation dominates. - ❌ **Experiments as feature flags** — use `features:` for deterministic switches. - ❌ **Engine experiments in one file** — `engine:` cannot switch mid-run; use two parallel files. +- ❌ **Conditional frontmatter imports** — keep imports security-stable and use `{{#if experiments. }}` with `{{#runtime-import path}}` for prompt experiments instead. - ❌ **Nesting `{{#if experiments. }}` inside `{{#runtime-import? }}`** — evaluation order not guaranteed across import boundaries. - ❌ **Writing the internal env-var form** `__GH_AW_EXPERIMENTS__*` — implementation detail, may change. diff --git a/.github/aw/syntax-tools-imports.md b/.github/aw/syntax-tools-imports.md index 403654f5350..c956aa72b9f 100644 --- a/.github/aw/syntax-tools-imports.md +++ b/.github/aw/syntax-tools-imports.md @@ -250,6 +250,8 @@ imports: - `env:` - Environment variables passed into the imported workflow context (object). Use when a shared workflow relies on environment variables that must be supplied by the importing workflow. - `checkout:` - Ref (branch, tag, or SHA) to check out when processing this import (string). Overrides the default checkout for this specific import entry. +Conditional `imports:` entries are not supported. For experiment-specific prompt variants, keep the import unconditional and gate a `{{#runtime-import ...}}` block in the workflow body instead. + Inside the imported workflow, access values via `${{ github.aw.import-inputs. }}`. ### Import File Structure diff --git a/.github/workflows/daily-safe-output-optimizer.lock.yml b/.github/workflows/daily-safe-output-optimizer.lock.yml index 21bed458a66..08ce620867a 100644 --- a/.github/workflows/daily-safe-output-optimizer.lock.yml +++ b/.github/workflows/daily-safe-output-optimizer.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"981668a6b9cdde49ed29e740ddbcb16b3318a431fcae537bb4c848fe801ec300","body_hash":"590bf0d6a114134cf770d6249b47e5ea4b734ea124dd5533eedf7645031279db","strict":true,"agent_id":"claude","engine_versions":{"claude":"2.1.168"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"bc767879df64de334455a579fd07d25393777ab902651d4a65582196ca262471","body_hash":"96d895ec8c2a4d3c1838d9b15e8ad0887a52c964557708a20fcc9d6e16a95a00","strict":true,"agent_id":"claude","engine_versions":{"claude":"2.1.168"}} # gh-aw-manifest: {"version":1,"secrets":["ANTHROPIC_API_KEY","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"docker/build-push-action","sha":"f9f3042f7e2789586610d6e8b85c8f03e5195baf","version":"v7.2.0"},{"repo":"docker/setup-buildx-action","sha":"d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5","version":"v4.1.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.68"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.68"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.68"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) @@ -28,7 +28,7 @@ # Imports: # - ../skills/jqschema/SKILL.md # - shared/activation-app.md -# - shared/aw-logs-24h-fetch.md +# - shared/aw-logs-24h-fetch-setup.md # - shared/daily-audit-discussion.md # - shared/otlp.md # - shared/reporting.md @@ -246,7 +246,7 @@ jobs: id: pick-experiment uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_EXPERIMENT_SPEC: '{"log_fetch_strategy":{"variants":["eager","lazy"],"description":"Tests whether pre-fetching 24h logs in a setup step (eager) vs. letting the agent download them on demand via the gh-aw MCP logs tool (lazy) affects run duration and AI credit consumption","hypothesis":"H0: no change in run_duration_ms. H1: eager reduces run duration by \u003e=15% by eliminating agent log-discovery turns","metric":"run_duration_ms","secondary_metrics":["ai_credits_consumed","mcp_tool_call_count"],"guardrail_metrics":[{"name":"issue_creation_success_rate","direction":"min","threshold":"0.8"}],"min_samples":20,"weight":[50,50],"start_date":"2026-06-09","analysis_type":"mann_whitney","tags":["daily","log-fetching","efficiency","claude"]}}' + GH_AW_EXPERIMENT_SPEC: '{"log_fetch_strategy":{"variants":["eager","lazy"],"description":"Tests whether using a pre-fetched 24h log bundle (eager) vs. forcing on-demand gh-aw MCP log downloads (lazy) affects run duration and AI credit consumption","hypothesis":"H0: no change in run_duration_ms. H1: eager reduces run duration by \u003e=15% by avoiding MCP log-fetch turns","metric":"run_duration_ms","secondary_metrics":["ai_credits_consumed","mcp_tool_call_count"],"guardrail_metrics":[{"name":"issue_creation_success_rate","direction":"min","threshold":"0.8"}],"min_samples":20,"weight":[50,50],"start_date":"2026-06-09","analysis_type":"mann_whitney","tags":["daily","log-fetching","efficiency","claude"]}}' GH_AW_EXPERIMENT_STATE_FILE: /tmp/gh-aw/experiments/state.json GH_AW_EXPERIMENT_STATE_DIR: /tmp/gh-aw/experiments with: @@ -280,21 +280,21 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_7c896013d78630fd_EOF' + cat << 'GH_AW_PROMPT_7d2b309619b81f0f_EOF' - GH_AW_PROMPT_7c896013d78630fd_EOF + GH_AW_PROMPT_7d2b309619b81f0f_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_7c896013d78630fd_EOF' + cat << 'GH_AW_PROMPT_7d2b309619b81f0f_EOF' Tools: create_issue, create_discussion, missing_tool, missing_data, noop - GH_AW_PROMPT_7c896013d78630fd_EOF + GH_AW_PROMPT_7d2b309619b81f0f_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_7c896013d78630fd_EOF' + cat << 'GH_AW_PROMPT_7d2b309619b81f0f_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -323,20 +323,19 @@ jobs: {{/if}} - GH_AW_PROMPT_7c896013d78630fd_EOF + GH_AW_PROMPT_7d2b309619b81f0f_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_7c896013d78630fd_EOF' + cat << 'GH_AW_PROMPT_7d2b309619b81f0f_EOF' - {{#if experiments.log_fetch_strategy == "eager"}} - {{#runtime-import .github/workflows/shared/aw-logs-24h-fetch.md}} - {{/if}} + {{#runtime-import .github/workflows/shared/aw-logs-24h-fetch-setup.md}} {{#runtime-import .github/workflows/shared/activation-app.md}} {{#runtime-import .github/skills/jqschema/SKILL.md}} {{#runtime-import .github/workflows/shared/otlp.md}} {{#runtime-import .github/workflows/shared/reporting.md}} + {{#runtime-import .github/workflows/shared/aw-logs-24h-fetch-prompt.md}} {{#runtime-import .github/workflows/shared/noop-reminder.md}} {{#runtime-import .github/workflows/daily-safe-output-optimizer.md}} - GH_AW_PROMPT_7c896013d78630fd_EOF + GH_AW_PROMPT_7d2b309619b81f0f_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -532,7 +531,6 @@ jobs: GH_TOKEN: ${{ github.token }} - env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - if: needs.activation.outputs.log_fetch_strategy == 'eager' name: Download logs from last 24 hours run: ./gh-aw logs --start-date -1d -o /tmp/gh-aw/aw-mcp/logs diff --git a/.github/workflows/daily-safe-output-optimizer.md b/.github/workflows/daily-safe-output-optimizer.md index e1921f4cdb7..aa985a0cec6 100644 --- a/.github/workflows/daily-safe-output-optimizer.md +++ b/.github/workflows/daily-safe-output-optimizer.md @@ -29,8 +29,7 @@ imports: - uses: shared/skip-if-issue-open.md with: title-prefix: "[safeoutputs]" - - if: experiments.log_fetch_strategy == 'eager' - uses: shared/aw-logs-24h-fetch.md + - shared/aw-logs-24h-fetch-setup.md - shared/activation-app.md - ../skills/jqschema/SKILL.md - uses: shared/daily-audit-base.md @@ -46,8 +45,8 @@ tools: experiments: log_fetch_strategy: variants: [eager, lazy] - description: "Tests whether pre-fetching 24h logs in a setup step (eager) vs. letting the agent download them on demand via the gh-aw MCP logs tool (lazy) affects run duration and AI credit consumption" - hypothesis: "H0: no change in run_duration_ms. H1: eager reduces run duration by >=15% by eliminating agent log-discovery turns" + description: "Tests whether using a pre-fetched 24h log bundle (eager) vs. forcing on-demand gh-aw MCP log downloads (lazy) affects run duration and AI credit consumption" + hypothesis: "H0: no change in run_duration_ms. H1: eager reduces run duration by >=15% by avoiding MCP log-fetch turns" metric: run_duration_ms secondary_metrics: [ai_credits_consumed, mcp_tool_call_count] guardrail_metrics: @@ -91,17 +90,9 @@ Create issues to improve tool descriptions when the workflow prompt is correct b ### Phase 1: Collect Workflow Logs with Safe Output Errors {{#if experiments.log_fetch_strategy == "eager"}} -Logs have been pre-downloaded to `/tmp/gh-aw/aw-mcp/logs/` by the setup step. Use this pre-fetched data directly — do **not** call the `logs` MCP tool. - -1. **Use Pre-Fetched Logs**: - Read the log data from `/tmp/gh-aw/aw-mcp/logs/` directly. - -2. **Verify Log Collection**: - - Check that logs were downloaded successfully in `/tmp/gh-aw/aw-mcp/logs` - - Note how many workflow runs were found - - Look for `summary.json` with aggregated data +{{#runtime-import shared/aw-logs-24h-fetch-prompt.md}} {{else}} -The gh-aw binary has been built and configured as an MCP server. Use the MCP tools directly. +The gh-aw binary has been built and configured as an MCP server. Ignore any pre-downloaded log bundle and use the MCP tools directly. 1. **Download Logs with Safe Output Filter**: Use the `logs` tool from the gh-aw MCP server: diff --git a/.github/workflows/shared/aw-logs-24h-fetch-prompt.md b/.github/workflows/shared/aw-logs-24h-fetch-prompt.md new file mode 100644 index 00000000000..9744981b3d6 --- /dev/null +++ b/.github/workflows/shared/aw-logs-24h-fetch-prompt.md @@ -0,0 +1,15 @@ +## Agentic Workflow Logs (Last 24h) + +Workflow logs have been pre-downloaded to `/tmp/gh-aw/aw-mcp/logs/`. + +**IMPORTANT**: Do NOT run `./gh-aw` or `gh aw` CLI commands directly — the binary is not authenticated in the agent environment. Use the `agentic-workflows` MCP server tools (`status`, `logs`, `audit`) instead for all additional queries. + +### Log Directory Structure + +``` +/tmp/gh-aw/aw-mcp/logs/ +└── run-(id)/ # One directory per workflow run + ├── aw_info.json # Run metadata (engine, workflow, status, tokens) + ├── activation/ # Activation job logs + └── agent/ # Agent job logs +``` diff --git a/.github/workflows/shared/aw-logs-24h-fetch-setup.md b/.github/workflows/shared/aw-logs-24h-fetch-setup.md new file mode 100644 index 00000000000..2278b51f1b4 --- /dev/null +++ b/.github/workflows/shared/aw-logs-24h-fetch-setup.md @@ -0,0 +1,15 @@ +--- +# Pre-fetch last 24 hours of agentic workflow logs for analysis +# Saves logs to /tmp/gh-aw/aw-mcp/logs/ + +tools: + agentic-workflows: + cache-memory: true + timeout: 300 + +steps: + - name: Download logs from last 24 hours + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./gh-aw logs --start-date -1d -o /tmp/gh-aw/aw-mcp/logs +--- diff --git a/docs/src/content/docs/reference/imports.md b/docs/src/content/docs/reference/imports.md index c77b333adcf..ec90dcf0eac 100644 --- a/docs/src/content/docs/reference/imports.md +++ b/docs/src/content/docs/reference/imports.md @@ -44,6 +44,8 @@ imports: `uses` is an alias for `path`; `with` is an alias for `inputs`. +Conditional frontmatter imports are not supported. If an experiment should vary shared prompt content, keep imports unconditional and gate `{{#runtime-import ...}}` inside your `{{#if experiments. ...}}` block instead. + ### Single-import constraint A workflow file can appear at most once in an import graph. If the same file is imported more than once with identical `with` values it is silently deduplicated. Importing the same file with **different** `with` values is a compile-time error: diff --git a/pkg/parser/import_bfs.go b/pkg/parser/import_bfs.go index eafe2494e48..7286f63d2bc 100644 --- a/pkg/parser/import_bfs.go +++ b/pkg/parser/import_bfs.go @@ -49,26 +49,23 @@ func processImportsFromFrontmatterWithManifestAndSource(frontmatter map[string]a } type nestedImportEntry struct { - path string - inputs map[string]any - ifCondition string + path string + inputs map[string]any } type importBFSState struct { queue []importQueueItem visited map[string]bool visitedInputs map[string]map[string]any - visitedIfConds map[string]string processedOrder []string acc *importAccumulator } func newImportBFSState() *importBFSState { return &importBFSState{ - visited: make(map[string]bool), - visitedInputs: make(map[string]map[string]any), - visitedIfConds: make(map[string]string), - acc: newImportAccumulator(), + visited: make(map[string]bool), + visitedInputs: make(map[string]map[string]any), + acc: newImportAccumulator(), } } @@ -134,7 +131,7 @@ func seedSingleImportSpec(importSpec ImportSpec, baseDir string, cache *ImportCa return err } origin := detectRemoteImportOrigin(filePath) - return enqueueImportPath(state, importPath, fullPath, sectionName, baseDir, importSpec.Inputs, importSpec.If, origin) + return enqueueImportPath(state, importPath, fullPath, sectionName, baseDir, importSpec.Inputs, origin) } func splitImportPathAndSection(importPath string) (string, string) { @@ -195,13 +192,12 @@ func detectRemoteImportOrigin(filePath string) *remoteImportOrigin { return origin } -func enqueueImportPath(state *importBFSState, importPath, fullPath, sectionName, baseDir string, inputs map[string]any, ifCondition string, origin *remoteImportOrigin) error { +func enqueueImportPath(state *importBFSState, importPath, fullPath, sectionName, baseDir string, inputs map[string]any, origin *remoteImportOrigin) error { if !state.visited[fullPath] { state.visited[fullPath] = true state.visitedInputs[fullPath] = inputs - state.visitedIfConds[fullPath] = ifCondition state.queue = append(state.queue, importQueueItem{ - importPath: importPath, fullPath: fullPath, sectionName: sectionName, baseDir: baseDir, inputs: inputs, ifCondition: ifCondition, remoteOrigin: origin, + importPath: importPath, fullPath: fullPath, sectionName: sectionName, baseDir: baseDir, inputs: inputs, remoteOrigin: origin, }) parserLog.Printf("Queued import: %s (resolved to %s)", importPath, fullPath) return nil @@ -209,9 +205,6 @@ func enqueueImportPath(state *importBFSState, importPath, fullPath, sectionName, if err := checkImportInputsConsistency(importPath, state.visitedInputs[fullPath], inputs); err != nil { return err } - if err := checkImportIfConditionConsistency(importPath, state.visitedIfConds[fullPath], ifCondition); err != nil { - return err - } parserLog.Printf("Skipping duplicate import: %s (already visited)", importPath) return nil } @@ -370,7 +363,15 @@ func extractFrontmatterForImport(fullPath string, content []byte) (*FrontmatterR } func enqueueNestedImports(frontmatter map[string]any, item importQueueItem, baseDir string, cache *ImportCache, workflowFilePath string, yamlContent string, state *importBFSState) error { - nestedImports := parseNestedImportEntries(frontmatter) + importsField, hasImports := frontmatter["imports"] + if !hasImports { + return nil + } + importSpecs, err := parseImportSpecsFromField(importsField) + if err != nil { + return err + } + nestedImports := nestedEntriesFromSpecs(importSpecs) for _, nestedEntry := range nestedImports { if err := enqueueNestedImportEntry(nestedEntry, item, baseDir, cache, workflowFilePath, yamlContent, state); err != nil { return err @@ -422,11 +423,7 @@ func parseNestedImportEntry(item any) (nestedImportEntry, bool) { } else if inputsVal, ok := nestedItem["inputs"].(map[string]any); ok { nestedInputs = inputsVal } - var nestedIfCond string - if ifVal, ok := nestedItem["if"].(string); ok { - nestedIfCond = ifVal - } - return nestedImportEntry{path: nestedPath, inputs: nestedInputs, ifCondition: nestedIfCond}, true + return nestedImportEntry{path: nestedPath, inputs: nestedInputs}, true default: return nestedImportEntry{}, false } @@ -435,7 +432,7 @@ func parseNestedImportEntry(item any) (nestedImportEntry, bool) { func nestedEntriesFromSpecs(specs []ImportSpec) []nestedImportEntry { nestedImports := make([]nestedImportEntry, 0, len(specs)) for _, spec := range specs { - nestedImports = append(nestedImports, nestedImportEntry{path: spec.Path, inputs: spec.Inputs, ifCondition: spec.If}) + nestedImports = append(nestedImports, nestedImportEntry{path: spec.Path, inputs: spec.Inputs}) } return nestedImports } @@ -453,8 +450,7 @@ func enqueueNestedImportEntry(entry nestedImportEntry, item importQueueItem, bas return formatNestedResolveError(nestedImportPath, nestedFilePath, item, workflowFilePath, yamlContent, err) } canonicalImportPath := canonicalizeNestedImportPath(nestedImportPath, nestedBaseDir, baseDir, nestedRemoteOrigin, nestedFullPath) - effectiveIfCondition := combineImportIfConditions(item.ifCondition, entry.ifCondition) - return enqueueNestedVisitedPath(state, canonicalImportPath, nestedFullPath, nestedSectionName, baseDir, entry.inputs, effectiveIfCondition, nestedRemoteOrigin) + return enqueueNestedVisitedPath(state, canonicalImportPath, nestedFullPath, nestedSectionName, baseDir, entry.inputs, nestedRemoteOrigin) } func resolveNestedImportPathAndOrigin(item importQueueItem, nestedFilePath string) (string, *remoteImportOrigin, error) { @@ -516,13 +512,12 @@ func canonicalizeNestedImportPath(nestedImportPath, nestedBaseDir, baseDir strin return filepath.ToSlash(rel) } -func enqueueNestedVisitedPath(state *importBFSState, nestedImportPath, nestedFullPath, nestedSectionName, baseDir string, inputs map[string]any, ifCondition string, nestedRemoteOrigin *remoteImportOrigin) error { +func enqueueNestedVisitedPath(state *importBFSState, nestedImportPath, nestedFullPath, nestedSectionName, baseDir string, inputs map[string]any, nestedRemoteOrigin *remoteImportOrigin) error { if !state.visited[nestedFullPath] { state.visited[nestedFullPath] = true state.visitedInputs[nestedFullPath] = inputs - state.visitedIfConds[nestedFullPath] = ifCondition state.queue = append(state.queue, importQueueItem{ - importPath: nestedImportPath, fullPath: nestedFullPath, sectionName: nestedSectionName, baseDir: baseDir, inputs: inputs, ifCondition: ifCondition, remoteOrigin: nestedRemoteOrigin, + importPath: nestedImportPath, fullPath: nestedFullPath, sectionName: nestedSectionName, baseDir: baseDir, inputs: inputs, remoteOrigin: nestedRemoteOrigin, }) parserLog.Printf("Discovered nested import: %s (queued)", nestedFullPath) return nil @@ -530,9 +525,6 @@ func enqueueNestedVisitedPath(state *importBFSState, nestedImportPath, nestedFul if err := checkImportInputsConsistency(nestedImportPath, state.visitedInputs[nestedFullPath], inputs); err != nil { return err } - if err := checkImportIfConditionConsistency(nestedImportPath, state.visitedIfConds[nestedFullPath], ifCondition); err != nil { - return err - } parserLog.Printf("Skipping already visited nested import: %s (cycle detected)", nestedFullPath) return nil } @@ -573,16 +565,10 @@ func parseImportSpecsFromArray(items []any) ([]ImportSpec, error) { return nil, errors.New("import 'inputs'/'with' must be an object") } } - // Extract optional "if" condition - var ifCond string - if ifVal, hasIf := importItem["if"]; hasIf { - if ifStr, ok := ifVal.(string); ok { - ifCond = ifStr - } else { - return nil, errors.New("import 'if' must be a string") - } + if _, hasIf := importItem["if"]; hasIf { + return nil, errors.New("import 'if' is no longer supported; use {{#if ...}}{{#runtime-import ...}}{{/if}} for experiment-specific prompt imports") } - specs = append(specs, ImportSpec{Path: pathStr, Inputs: inputs, If: ifCond}) + specs = append(specs, ImportSpec{Path: pathStr, Inputs: inputs}) default: return nil, errors.New("import item must be a string or an object with 'path'/'uses' field") } @@ -608,36 +594,6 @@ func checkImportInputsConsistency(importPath string, existingInputs, newInputs m ) } -func checkImportIfConditionConsistency(importPath, existingCondition, newCondition string) error { - if strings.TrimSpace(existingCondition) == strings.TrimSpace(newCondition) { - return nil - } - return fmt.Errorf( - "import conflict: '%s' is imported more than once with different 'if' conditions.\n"+ - "An imported workflow can only be imported once per workflow with one consistent condition.\n"+ - " Previous 'if': %q\n"+ - " New 'if': %q", - importPath, - existingCondition, - newCondition, - ) -} - -// combineImportIfConditions merges a parent import condition with a nested import -// condition. Empty conditions are ignored; when both are non-empty, both must hold. -func combineImportIfConditions(parentCondition, childCondition string) string { - parent := strings.TrimSpace(parentCondition) - child := strings.TrimSpace(childCondition) - switch { - case parent == "": - return child - case child == "": - return parent - default: - return fmt.Sprintf("(%s) && (%s)", parent, child) - } -} - // importInputsEqual reports whether two import input maps are deeply equal. // Both nil and empty maps are considered equal (both represent "no inputs"). // Map key ordering does not affect the result. diff --git a/pkg/parser/import_bfs_test.go b/pkg/parser/import_bfs_test.go index d626002d432..1d38f0e4e64 100644 --- a/pkg/parser/import_bfs_test.go +++ b/pkg/parser/import_bfs_test.go @@ -3,8 +3,6 @@ package parser import ( - "os" - "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -30,72 +28,13 @@ func TestParseNestedImportEntries_LenientArrayParsing(t *testing.T) { require.Equal(t, map[string]any{"env": "prod"}, entries[1].inputs) } -func TestParseImportSpecsFromArray_InvalidIfType(t *testing.T) { +func TestParseImportSpecsFromArray_RejectsIf(t *testing.T) { _, err := parseImportSpecsFromArray([]any{ map[string]any{ "uses": "shared/workflow.md", - "if": true, + "if": "experiments.variant == 'a'", }, }) require.Error(t, err) - assert.Contains(t, err.Error(), "import 'if' must be a string") -} - -func TestProcessImportsFromFrontmatter_NestedImportInheritsIfCondition(t *testing.T) { - tmpDir := t.TempDir() - sharedDir := filepath.Join(tmpDir, "shared") - require.NoError(t, os.MkdirAll(sharedDir, 0o755)) - - require.NoError(t, os.WriteFile(filepath.Join(sharedDir, "leaf.md"), []byte(`--- -steps: - - name: Leaf - run: echo leaf ---- -`), 0o644)) - - require.NoError(t, os.WriteFile(filepath.Join(sharedDir, "parent.md"), []byte(`--- -imports: - - uses: shared/leaf.md ---- -`), 0o644)) - - mainContent := `--- -imports: - - uses: shared/parent.md - if: "experiments.variant == 'a'" ---- -` - result, err := ExtractFrontmatterFromContent(mainContent) - require.NoError(t, err) - - importsResult, err := ProcessImportsFromFrontmatterWithSource(result.Frontmatter, tmpDir, nil, "", "") - require.NoError(t, err) - assert.Contains(t, importsResult.MergedSteps, "needs.activation.outputs.variant == 'a'") -} - -func TestProcessImportsFromFrontmatter_DuplicateImportWithDifferentIfErrors(t *testing.T) { - tmpDir := t.TempDir() - sharedDir := filepath.Join(tmpDir, "shared") - require.NoError(t, os.MkdirAll(sharedDir, 0o755)) - require.NoError(t, os.WriteFile(filepath.Join(sharedDir, "a.md"), []byte(`--- -steps: - - name: A - run: echo a ---- -`), 0o644)) - - mainContent := `--- -imports: - - uses: shared/a.md - if: "experiments.variant == 'a'" - - uses: shared/a.md - if: "experiments.variant == 'b'" ---- -` - result, err := ExtractFrontmatterFromContent(mainContent) - require.NoError(t, err) - - _, err = ProcessImportsFromFrontmatterWithSource(result.Frontmatter, tmpDir, nil, "", "") - require.Error(t, err) - assert.Contains(t, err.Error(), "different 'if' conditions") + assert.Contains(t, err.Error(), "import 'if' is no longer supported") } diff --git a/pkg/parser/import_field_extractor.go b/pkg/parser/import_field_extractor.go index 27172d83b4a..df991a48218 100644 --- a/pkg/parser/import_field_extractor.go +++ b/pkg/parser/import_field_extractor.go @@ -13,7 +13,6 @@ import ( "strings" "github.com/github/gh-aw/pkg/importinpututil" - "github.com/goccy/go-yaml" ) // importAccumulator centralizes the builder/slice/set variables used during @@ -127,13 +126,13 @@ func (acc *importAccumulator) extractAllImportFields(content []byte, item import acc.extractEngineConfig(fm, item.fullPath) // Phase 4: Extract scalar and builder-based configuration fields. - acc.extractConfigFields(fm, item.fullPath, item.ifCondition) + acc.extractConfigFields(fm, item.fullPath) // Phase 5: Extract activation, authentication, and access-control fields. acc.extractActivationFields(fm, item) // Phase 6: Extract step, job, and environment fields. - if err := acc.extractStepAndJobFields(fm, item.importPath, item.ifCondition); err != nil { + if err := acc.extractStepAndJobFields(fm, item.importPath); err != nil { return err } @@ -167,7 +166,7 @@ func (acc *importAccumulator) prepareFrontmatter(content []byte, item importQueu } acc.toolsBuilder.WriteString(toolsContent + "\n") importRelPath := computeImportRelPath(item.fullPath, item.importPath) - if err := acc.trackRuntimeOrInlineImport(item.fullPath, importRelPath, rawContent, wasSubstituted, item.ifCondition); err != nil { + if err := acc.trackRuntimeOrInlineImport(item.fullPath, importRelPath, rawContent, wasSubstituted); err != nil { return nil, nil, err } @@ -234,10 +233,10 @@ func (acc *importAccumulator) extractToolsContent(rawContent string, item import return toolsContent, nil } -func (acc *importAccumulator) trackRuntimeOrInlineImport(fullPath, importRelPath, rawContent string, wasSubstituted bool, ifCondition string) error { +func (acc *importAccumulator) trackRuntimeOrInlineImport(fullPath, importRelPath, rawContent string, wasSubstituted bool) error { if !wasSubstituted && !strings.HasPrefix(importRelPath, BuiltinPathPrefix) { acc.importPaths = append(acc.importPaths, importRelPath) - acc.promptImports = append(acc.promptImports, PromptImportEntry{ImportPath: importRelPath, If: ifCondition}) + acc.promptImports = append(acc.promptImports, PromptImportEntry{ImportPath: importRelPath}) parserLog.Printf("Added import path for runtime-import: %s", importRelPath) return nil } @@ -250,7 +249,7 @@ func (acc *importAccumulator) trackRuntimeOrInlineImport(fullPath, importRelPath return fmt.Errorf("failed to extract markdown from imported file '%s': %w", fullPath, err) } appendMarkdownWithSeparator(&acc.markdownBuilder, markdownContent) - acc.promptImports = append(acc.promptImports, PromptImportEntry{Markdown: markdownContent, If: ifCondition}) + acc.promptImports = append(acc.promptImports, PromptImportEntry{Markdown: markdownContent}) return nil } @@ -357,7 +356,7 @@ func (acc *importAccumulator) extractEngineConfig(fm map[string]any, fullPath st // acc.safeOutputs, acc.mcpScripts, acc.stepsBuilder, acc.runtimesBuilder, // acc.servicesBuilder, acc.networkBuilder, acc.permissionsBuilder, // acc.secretMaskingBuilder. -func (acc *importAccumulator) extractConfigFields(fm map[string]any, fullPath string, ifCondition string) { +func (acc *importAccumulator) extractConfigFields(fm map[string]any, fullPath string) { acc.extractFirstWinsJSONField(fm, fullPath, "max-turns", &acc.mergedMaxTurns) acc.extractFirstWinsJSONField(fm, fullPath, "max-tool-denials", &acc.mergedMaxToolDenials) acc.extractFirstWinsJSONField(fm, fullPath, "max-runs", &acc.mergedMaxRuns) @@ -367,7 +366,7 @@ func (acc *importAccumulator) extractConfigFields(fm map[string]any, fullPath st acc.appendJSONBuilderField(fm, "mcp-servers", "{}", &acc.mcpServersBuilder) acc.appendJSONSliceField(fm, "safe-outputs", "{}", &acc.safeOutputs) acc.appendJSONSliceField(fm, "mcp-scripts", "{}", &acc.mcpScripts) - acc.appendConditionalYAMLStepsField(fm, "steps", &acc.stepsBuilder, ifCondition) + acc.appendYAMLBuilderField(fm, "steps", &acc.stepsBuilder) acc.appendJSONBuilderField(fm, "runtimes", "{}", &acc.runtimesBuilder) acc.appendYAMLBuilderField(fm, "services", &acc.servicesBuilder) acc.appendJSONBuilderField(fm, "network", "{}", &acc.networkBuilder) @@ -411,70 +410,6 @@ func (acc *importAccumulator) appendYAMLBuilderField(fm map[string]any, field st builder.WriteString(content + "\n") } -// appendConditionalYAMLStepsField extracts a YAML steps field and appends it to the builder. -// When ifCondition is non-empty, each step that lacks an "if:" guard has one added using -// the transformed condition (see transformImportConditionForStep). -func (acc *importAccumulator) appendConditionalYAMLStepsField(fm map[string]any, field string, builder *strings.Builder, ifCondition string) { - content, err := extractYAMLFieldFromMap(fm, field) - if err != nil || content == "" { - return - } - builder.WriteString(applyIfConditionToStepsYAML(content, ifCondition) + "\n") -} - -// experimentConditionRegex matches "experiments." sub-expressions in an import -// if: condition and rewrites them to "needs.activation.outputs." for use in -// GitHub Actions step-level "if:" guards. -var experimentConditionRegex = regexp.MustCompile(`experiments\.(\w+)`) - -// transformImportConditionForStep converts an import-spec condition expression into the -// equivalent expression for a GitHub Actions step "if:" field. -// -// Example: -// -// experiments.log_fetch_strategy == 'eager' -// → needs.activation.outputs.log_fetch_strategy == 'eager' -func transformImportConditionForStep(condition string) string { - return experimentConditionRegex.ReplaceAllString(condition, "needs.activation.outputs.$1") -} - -// applyIfConditionToStepsYAML applies an import-level "if:" guard to each step in a YAML -// steps list. When a step already has an "if:" expression, the import condition is -// combined with the existing expression using logical AND. -// When ifCondition is empty the original content is returned -// unchanged. The YAML is parsed using round-trip semantics: on any parse or marshal error -// the original content is returned unchanged so that the caller's behaviour degrades -// gracefully rather than silently discarding steps. -func applyIfConditionToStepsYAML(stepsYAML string, ifCondition string) string { - if ifCondition == "" { - return stepsYAML - } - stepCondition := transformImportConditionForStep(ifCondition) - var steps []map[string]any - if err := yaml.UnmarshalWithOptions([]byte(stepsYAML), &steps, yaml.UseOrderedMap()); err != nil { - parserLog.Printf("Warning: failed to parse steps YAML for if-condition injection (condition=%q), using original: %v", ifCondition, err) - return stepsYAML - } - for i := range steps { - if existing, hasIf := steps[i]["if"]; hasIf { - existingCondition := strings.TrimSpace(fmt.Sprint(existing)) - if existingCondition == "" { - steps[i]["if"] = stepCondition - continue - } - steps[i]["if"] = fmt.Sprintf("(%s) && (%s)", stepCondition, existingCondition) - } else { - steps[i]["if"] = stepCondition - } - } - out, err := yaml.Marshal(steps) - if err != nil { - parserLog.Printf("Warning: failed to marshal steps YAML after if-condition injection (condition=%q), using original: %v", ifCondition, err) - return stepsYAML - } - return strings.TrimSpace(string(out)) -} - // extractActivationFields extracts activation and authentication-related fields from // the frontmatter map: bots, skip-roles, skip-bots, skip-if-match, skip-if-no-match, // on.github-token, on.github-app, top-level github-app, and checkout. @@ -591,27 +526,22 @@ func (acc *importAccumulator) extractCheckoutField(fm map[string]any, fullPath s // map. Environment variable conflict detection is performed: if the same env var is // defined in two different imports, an error is returned. // -// When ifCondition is non-empty, each extracted step receives an "if:" guard that evaluates -// the condition at runtime. The expression is transformed from the import-spec form -// (e.g. "experiments.foo == 'bar'") to the GitHub Actions form used in step conditions -// (e.g. "needs.activation.outputs.foo == 'bar'"). -// // Side effects: acc.preStepsBuilder, acc.preAgentStepsBuilder, acc.postStepsBuilder, // acc.jobsBuilder, acc.envBuilder, acc.envSources. -func (acc *importAccumulator) extractStepAndJobFields(fm map[string]any, importPath string, ifCondition string) error { +func (acc *importAccumulator) extractStepAndJobFields(fm map[string]any, importPath string) error { // Extract pre-steps (prepend in order). if preStepsContent, err := extractYAMLFieldFromMap(fm, "pre-steps"); err == nil && preStepsContent != "" { - acc.preStepsBuilder.WriteString(applyIfConditionToStepsYAML(preStepsContent, ifCondition) + "\n") + acc.preStepsBuilder.WriteString(preStepsContent + "\n") } // Extract pre-agent-steps (prepend in order). if preAgentStepsContent, err := extractYAMLFieldFromMap(fm, "pre-agent-steps"); err == nil && preAgentStepsContent != "" { - acc.preAgentStepsBuilder.WriteString(applyIfConditionToStepsYAML(preAgentStepsContent, ifCondition) + "\n") + acc.preAgentStepsBuilder.WriteString(preAgentStepsContent + "\n") } // Extract post-steps (append in order). if postStepsContent, err := extractYAMLFieldFromMap(fm, "post-steps"); err == nil && postStepsContent != "" { - acc.postStepsBuilder.WriteString(applyIfConditionToStepsYAML(postStepsContent, ifCondition) + "\n") + acc.postStepsBuilder.WriteString(postStepsContent + "\n") } // Extract jobs (append in order; merged into custom jobs map). diff --git a/pkg/parser/import_field_extractor_test.go b/pkg/parser/import_field_extractor_test.go index 034cc871dd6..f19fe3cef01 100644 --- a/pkg/parser/import_field_extractor_test.go +++ b/pkg/parser/import_field_extractor_test.go @@ -674,8 +674,8 @@ func TestExtractConfigFields_FirstWinsAndAccumulates(t *testing.T) { "secret-masking": map[string]any{"log-mask": true}, } - acc.extractConfigFields(first, "first.md", "") - acc.extractConfigFields(second, "second.md", "") + acc.extractConfigFields(first, "first.md") + acc.extractConfigFields(second, "second.md") assert.Equal(t, "10", acc.mergedMaxTurns, "max-turns should be first-wins") assert.Equal(t, "5", acc.mergedMaxToolDenials, "max-tool-denials should be first-wins") @@ -696,24 +696,3 @@ func TestExtractConfigFields_FirstWinsAndAccumulates(t *testing.T) { assert.Contains(t, acc.secretMaskingBuilder.String(), "enabled") assert.Contains(t, acc.secretMaskingBuilder.String(), "log-mask") } - -func TestApplyIfConditionToStepsYAML_InsertsAndRewritesImportCondition(t *testing.T) { - stepsYAML := `- name: No guard - run: echo hello` - - got := applyIfConditionToStepsYAML(stepsYAML, "experiments.log_fetch_strategy == 'eager'") - - assert.Contains(t, got, "needs.activation.outputs.log_fetch_strategy == 'eager'") - assert.Contains(t, got, "name: No guard") -} - -func TestApplyIfConditionToStepsYAML_CombinesExistingCondition(t *testing.T) { - stepsYAML := `- name: Guarded - if: success() - run: echo hello` - - got := applyIfConditionToStepsYAML(stepsYAML, "experiments.log_fetch_strategy == 'eager'") - - assert.Contains(t, got, "(needs.activation.outputs.log_fetch_strategy == 'eager') && (success())") - assert.Contains(t, got, "name: Guarded") -} diff --git a/pkg/parser/import_processor.go b/pkg/parser/import_processor.go index a9b1e57a0b0..e408bfbce7a 100644 --- a/pkg/parser/import_processor.go +++ b/pkg/parser/import_processor.go @@ -20,7 +20,6 @@ var importLog = logger.New("parser:import_processor") type PromptImportEntry struct { ImportPath string // Non-empty when this import should be emitted as {{#runtime-import ...}} Markdown string // Non-empty when this import should be inlined into the prompt at compile time - If string // Optional condition expression that guards this import (e.g., "experiments.foo == 'bar'") } // ImportsResult holds the result of processing imports from frontmatter @@ -97,7 +96,6 @@ type ImportSpec struct { // This is parsed from YAML frontmatter and validated against the imported workflow's input definitions. // This is an appropriate use of 'any' for dynamic YAML data. See scratchpad/go-type-patterns.md. Inputs map[string]any // Optional input values to pass to the imported workflow (values are string, number, or boolean) - If string // Optional condition expression (e.g., "experiments.foo == 'bar'"); conditional imports generate conditional steps and prompt blocks } // ProcessImportsFromFrontmatterWithSource processes imports field from frontmatter with source tracking diff --git a/pkg/parser/import_remote.go b/pkg/parser/import_remote.go index e8b3ba70586..e6f9f890a74 100644 --- a/pkg/parser/import_remote.go +++ b/pkg/parser/import_remote.go @@ -29,7 +29,6 @@ type importQueueItem struct { sectionName string // Optional section name (from file.md#Section syntax) baseDir string // Base directory for resolving nested imports inputs map[string]any // Optional input values from parent import - ifCondition string // Optional condition expression from import spec 'if:' field (e.g., "experiments.foo == 'bar'") remoteOrigin *remoteImportOrigin // Remote origin context (non-nil when imported from a remote repo) } diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index c13d4749a16..cd155d572cf 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -97,10 +97,6 @@ "type": "string", "description": "Import path. Use 'shared/file.md' for paths relative to the workflow directory, '.github/agents/my-agent.md' for repo-root-relative paths, or 'owner/repo/path@ref' for cross-repo imports. Markdown files under .github/agents/ are treated as agent configuration files." }, - "if": { - "type": "string", - "description": "Condition expression that guards this import at runtime. Use 'experiments.' form (e.g. \"experiments.strategy == 'eager'\") to make the import conditional on an experiment variant. Steps and prompt content from the import are only active when the condition is true." - }, "inputs": { "type": "object", "description": "Input values to pass to the imported workflow. Keys are input names declared in the imported workflow's inputs section, values can be strings or expressions.", @@ -147,10 +143,6 @@ "type": "string", "description": "Import path (alias for 'path'). Use 'shared/file.md' for paths relative to the workflow directory, '.github/agents/my-agent.md' for repo-root-relative paths, or 'owner/repo/path@ref' for cross-repo imports." }, - "if": { - "type": "string", - "description": "Condition expression that guards this import at runtime. Use 'experiments.' form (e.g. \"experiments.strategy == 'eager'\") to make the import conditional on an experiment variant. Steps and prompt content from the import are only active when the condition is true." - }, "with": { "type": "object", "description": "Input values to pass to the imported workflow, validated against the imported workflow's 'import-schema'. Alias for 'inputs'.", @@ -236,10 +228,6 @@ "type": "string", "description": "Workflow specification in format owner/repo/path@ref." }, - "if": { - "type": "string", - "description": "Condition expression that guards this import at runtime. Use 'experiments.' form (e.g. \"experiments.strategy == 'eager'\") to make the import conditional on an experiment variant." - }, "inputs": { "type": "object", "description": "Input values to pass to the imported workflow.", @@ -286,10 +274,6 @@ "type": "string", "description": "Workflow specification in format owner/repo/path@ref." }, - "if": { - "type": "string", - "description": "Condition expression that guards this import at runtime. Use 'experiments.' form (e.g. \"experiments.strategy == 'eager'\") to make the import conditional on an experiment variant." - }, "with": { "type": "object", "description": "Input values to pass to the imported workflow.", diff --git a/pkg/workflow/compiler_yaml.go b/pkg/workflow/compiler_yaml.go index fcb88a2d7ef..9b9682d4af4 100644 --- a/pkg/workflow/compiler_yaml.go +++ b/pkg/workflow/compiler_yaml.go @@ -5,7 +5,6 @@ import ( "fmt" "os" "path/filepath" - "regexp" "sort" "strings" @@ -18,12 +17,6 @@ import ( var compilerYamlLog = logger.New("workflow:compiler_yaml") -var singleQuotedEqualityConditionRegex = regexp.MustCompile(`==\s*'([^']*)'`) - -func normalizeConditionForPromptIf(condition string) string { - return singleQuotedEqualityConditionRegex.ReplaceAllString(condition, `== "$1"`) -} - // effectiveStrictMode computes the effective strict mode for a workflow. // Priority: CLI flag (c.strictMode) > frontmatter strict field > default (true). // This should be used when emitting metadata/env vars to correctly reflect the @@ -509,15 +502,9 @@ func (c *Compiler) generatePrompt(yaml *strings.Builder, data *WorkflowData, pre if hasImportInputs { cleaned = SubstituteImportInputs(cleaned, data.ImportInputs) } - if entry.If != "" { - userPromptChunks = append(userPromptChunks, fmt.Sprintf("{{#if %s}}", normalizeConditionForPromptIf(entry.If))) - } chunks, exprMaps := extractPromptChunksFromMarkdown(cleaned) userPromptChunks = append(userPromptChunks, chunks...) expressionMappings = append(expressionMappings, exprMaps...) - if entry.If != "" { - userPromptChunks = append(userPromptChunks, "{{/if}}") - } continue } if entry.ImportPath == "" { @@ -528,37 +515,19 @@ func (c *Compiler) generatePrompt(yaml *strings.Builder, data *WorkflowData, pre rawContent, err := os.ReadFile(filepath.Join(workspaceRoot, importPath)) if err != nil { compilerYamlLog.Printf("Warning: failed to read import file %s (%v), falling back to runtime-import", importPath, err) - if entry.If != "" { - userPromptChunks = append(userPromptChunks, fmt.Sprintf("{{#if %s}}", normalizeConditionForPromptIf(entry.If))) - } userPromptChunks = append(userPromptChunks, fmt.Sprintf("{{#runtime-import %s}}", importPath)) - if entry.If != "" { - userPromptChunks = append(userPromptChunks, "{{/if}}") - } continue } importedBody, extractErr := parser.ExtractMarkdownContent(string(rawContent)) if extractErr != nil { importedBody = string(rawContent) } - if entry.If != "" { - userPromptChunks = append(userPromptChunks, fmt.Sprintf("{{#if %s}}", normalizeConditionForPromptIf(entry.If))) - } chunks, exprMaps := extractPromptChunksFromMarkdown(importedBody) userPromptChunks = append(userPromptChunks, chunks...) expressionMappings = append(expressionMappings, exprMaps...) - if entry.If != "" { - userPromptChunks = append(userPromptChunks, "{{/if}}") - } continue } - if entry.If != "" { - userPromptChunks = append(userPromptChunks, fmt.Sprintf("{{#if %s}}", normalizeConditionForPromptIf(entry.If))) - } userPromptChunks = append(userPromptChunks, fmt.Sprintf("{{#runtime-import %s}}", importPath)) - if entry.If != "" { - userPromptChunks = append(userPromptChunks, "{{/if}}") - } } } else { // Step 1a: Process and inline imported markdown with inputs (if any) diff --git a/pkg/workflow/compiler_yaml_test.go b/pkg/workflow/compiler_yaml_test.go index 949d9ab513b..2ddc202bb11 100644 --- a/pkg/workflow/compiler_yaml_test.go +++ b/pkg/workflow/compiler_yaml_test.go @@ -1383,15 +1383,6 @@ func extractRuntimeImportLines(content string) string { return strings.Join(lines, "\n") } -func TestNormalizeConditionForPromptIf_SingleQuotedEquality(t *testing.T) { - input := "experiments.log_fetch_strategy == 'eager'" - got := normalizeConditionForPromptIf(input) - expected := `experiments.log_fetch_strategy == "eager"` - if got != expected { - t.Fatalf("normalizeConditionForPromptIf() = %q, want %q", got, expected) - } -} - func TestLockMetadataVersionInReleaseBuilds(t *testing.T) { // Save and restore original values originalIsRelease := isReleaseBuild From 308f9281836f21ec7731b77b0f067f115b54ab20 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 00:23:49 +0000 Subject: [PATCH 2/5] chore: address review feedback Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/daily-safe-output-optimizer.lock.yml | 2 +- .github/workflows/daily-safe-output-optimizer.md | 2 +- pkg/parser/import_bfs_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/daily-safe-output-optimizer.lock.yml b/.github/workflows/daily-safe-output-optimizer.lock.yml index 08ce620867a..f17e4444b9d 100644 --- a/.github/workflows/daily-safe-output-optimizer.lock.yml +++ b/.github/workflows/daily-safe-output-optimizer.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"bc767879df64de334455a579fd07d25393777ab902651d4a65582196ca262471","body_hash":"96d895ec8c2a4d3c1838d9b15e8ad0887a52c964557708a20fcc9d6e16a95a00","strict":true,"agent_id":"claude","engine_versions":{"claude":"2.1.168"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"d21c08c86d1578ee38b789595105775c904522cce40aa867ab4aeaf5c2b50502","body_hash":"f22a5e8b535fa3d96fdc33c86bba6a348fc488c99eabae653b455e073c94e5aa","strict":true,"agent_id":"claude","engine_versions":{"claude":"2.1.168"}} # gh-aw-manifest: {"version":1,"secrets":["ANTHROPIC_API_KEY","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"docker/build-push-action","sha":"f9f3042f7e2789586610d6e8b85c8f03e5195baf","version":"v7.2.0"},{"repo":"docker/setup-buildx-action","sha":"d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5","version":"v4.1.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.68"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.68"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.68"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) diff --git a/.github/workflows/daily-safe-output-optimizer.md b/.github/workflows/daily-safe-output-optimizer.md index aa985a0cec6..b5b3c3e266f 100644 --- a/.github/workflows/daily-safe-output-optimizer.md +++ b/.github/workflows/daily-safe-output-optimizer.md @@ -29,7 +29,7 @@ imports: - uses: shared/skip-if-issue-open.md with: title-prefix: "[safeoutputs]" - - shared/aw-logs-24h-fetch-setup.md + - uses: shared/aw-logs-24h-fetch-setup.md - shared/activation-app.md - ../skills/jqschema/SKILL.md - uses: shared/daily-audit-base.md diff --git a/pkg/parser/import_bfs_test.go b/pkg/parser/import_bfs_test.go index 1d38f0e4e64..0a01318df01 100644 --- a/pkg/parser/import_bfs_test.go +++ b/pkg/parser/import_bfs_test.go @@ -28,7 +28,7 @@ func TestParseNestedImportEntries_LenientArrayParsing(t *testing.T) { require.Equal(t, map[string]any{"env": "prod"}, entries[1].inputs) } -func TestParseImportSpecsFromArray_RejectsIf(t *testing.T) { +func TestParseImportSpecsFromArray_RejectsIfField(t *testing.T) { _, err := parseImportSpecsFromArray([]any{ map[string]any{ "uses": "shared/workflow.md", From d19f56c8d923e3699204f6bfea0ea497eabd1880 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 01:34:31 +0000 Subject: [PATCH 3/5] fix: use {{#runtime-import? ...}} for experiment-gated prompt imports - Switch daily-safe-output-optimizer.md to use the optional `?` form so the prompt import is not promoted unconditionally to the lock file - Update migration error message in import_bfs.go to point to `?` form - Update docs (imports.md, syntax-tools-imports.md, experiments.md) to recommend `{{#runtime-import? ...}}` for experiment-gated content - Regenerate daily-safe-output-optimizer.lock.yml (removes the unconditional aw-logs-24h-fetch-prompt.md macro) Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/aw/experiments.md | 2 +- .github/aw/syntax-tools-imports.md | 2 +- .../daily-safe-output-optimizer.lock.yml | 19 +++++++++---------- .../workflows/daily-safe-output-optimizer.md | 2 +- docs/src/content/docs/reference/imports.md | 2 +- pkg/parser/import_bfs.go | 2 +- 6 files changed, 14 insertions(+), 15 deletions(-) diff --git a/.github/aw/experiments.md b/.github/aw/experiments.md index 49bae34cb88..44df805b4e1 100644 --- a/.github/aw/experiments.md +++ b/.github/aw/experiments.md @@ -292,6 +292,6 @@ experiments: - ❌ **Interpreting early results** (<~20 runs/variant) — chance variation dominates. - ❌ **Experiments as feature flags** — use `features:` for deterministic switches. - ❌ **Engine experiments in one file** — `engine:` cannot switch mid-run; use two parallel files. -- ❌ **Conditional frontmatter imports** — keep imports security-stable and use `{{#if experiments. }}` with `{{#runtime-import path}}` for prompt experiments instead. +- ❌ **Conditional frontmatter imports** — keep imports security-stable and use `{{#if experiments. }}` with `{{#runtime-import? path}}` (optional form, not promoted to unconditional lock-file macros) for prompt experiments instead. - ❌ **Nesting `{{#if experiments. }}` inside `{{#runtime-import? }}`** — evaluation order not guaranteed across import boundaries. - ❌ **Writing the internal env-var form** `__GH_AW_EXPERIMENTS__*` — implementation detail, may change. diff --git a/.github/aw/syntax-tools-imports.md b/.github/aw/syntax-tools-imports.md index c956aa72b9f..d4c589c5463 100644 --- a/.github/aw/syntax-tools-imports.md +++ b/.github/aw/syntax-tools-imports.md @@ -250,7 +250,7 @@ imports: - `env:` - Environment variables passed into the imported workflow context (object). Use when a shared workflow relies on environment variables that must be supplied by the importing workflow. - `checkout:` - Ref (branch, tag, or SHA) to check out when processing this import (string). Overrides the default checkout for this specific import entry. -Conditional `imports:` entries are not supported. For experiment-specific prompt variants, keep the import unconditional and gate a `{{#runtime-import ...}}` block in the workflow body instead. +Conditional `imports:` entries are not supported. For experiment-specific prompt variants, keep the import unconditional and gate a `{{#runtime-import? ...}}` block (optional form) in the workflow body instead. The optional form is not promoted to unconditional lock-file macros, so the content is only injected when the condition is true at runtime. Inside the imported workflow, access values via `${{ github.aw.import-inputs. }}`. diff --git a/.github/workflows/daily-safe-output-optimizer.lock.yml b/.github/workflows/daily-safe-output-optimizer.lock.yml index f17e4444b9d..63c20486f47 100644 --- a/.github/workflows/daily-safe-output-optimizer.lock.yml +++ b/.github/workflows/daily-safe-output-optimizer.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"d21c08c86d1578ee38b789595105775c904522cce40aa867ab4aeaf5c2b50502","body_hash":"f22a5e8b535fa3d96fdc33c86bba6a348fc488c99eabae653b455e073c94e5aa","strict":true,"agent_id":"claude","engine_versions":{"claude":"2.1.168"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"d21c08c86d1578ee38b789595105775c904522cce40aa867ab4aeaf5c2b50502","body_hash":"17a79d65a6e1affd699d34d4a9cf71318b9ca152a0ba8c1ff5f2521886f2db63","strict":true,"agent_id":"claude","engine_versions":{"claude":"2.1.168"}} # gh-aw-manifest: {"version":1,"secrets":["ANTHROPIC_API_KEY","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"docker/build-push-action","sha":"f9f3042f7e2789586610d6e8b85c8f03e5195baf","version":"v7.2.0"},{"repo":"docker/setup-buildx-action","sha":"d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5","version":"v4.1.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.68"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.68"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.68"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) @@ -280,21 +280,21 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_7d2b309619b81f0f_EOF' + cat << 'GH_AW_PROMPT_898c9a323aeb6ae5_EOF' - GH_AW_PROMPT_7d2b309619b81f0f_EOF + GH_AW_PROMPT_898c9a323aeb6ae5_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_7d2b309619b81f0f_EOF' + cat << 'GH_AW_PROMPT_898c9a323aeb6ae5_EOF' Tools: create_issue, create_discussion, missing_tool, missing_data, noop - GH_AW_PROMPT_7d2b309619b81f0f_EOF + GH_AW_PROMPT_898c9a323aeb6ae5_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_7d2b309619b81f0f_EOF' + cat << 'GH_AW_PROMPT_898c9a323aeb6ae5_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -323,19 +323,18 @@ jobs: {{/if}} - GH_AW_PROMPT_7d2b309619b81f0f_EOF + GH_AW_PROMPT_898c9a323aeb6ae5_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_7d2b309619b81f0f_EOF' + cat << 'GH_AW_PROMPT_898c9a323aeb6ae5_EOF' {{#runtime-import .github/workflows/shared/aw-logs-24h-fetch-setup.md}} {{#runtime-import .github/workflows/shared/activation-app.md}} {{#runtime-import .github/skills/jqschema/SKILL.md}} {{#runtime-import .github/workflows/shared/otlp.md}} {{#runtime-import .github/workflows/shared/reporting.md}} - {{#runtime-import .github/workflows/shared/aw-logs-24h-fetch-prompt.md}} {{#runtime-import .github/workflows/shared/noop-reminder.md}} {{#runtime-import .github/workflows/daily-safe-output-optimizer.md}} - GH_AW_PROMPT_7d2b309619b81f0f_EOF + GH_AW_PROMPT_898c9a323aeb6ae5_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 diff --git a/.github/workflows/daily-safe-output-optimizer.md b/.github/workflows/daily-safe-output-optimizer.md index b5b3c3e266f..003da20c213 100644 --- a/.github/workflows/daily-safe-output-optimizer.md +++ b/.github/workflows/daily-safe-output-optimizer.md @@ -90,7 +90,7 @@ Create issues to improve tool descriptions when the workflow prompt is correct b ### Phase 1: Collect Workflow Logs with Safe Output Errors {{#if experiments.log_fetch_strategy == "eager"}} -{{#runtime-import shared/aw-logs-24h-fetch-prompt.md}} +{{#runtime-import? shared/aw-logs-24h-fetch-prompt.md}} {{else}} The gh-aw binary has been built and configured as an MCP server. Ignore any pre-downloaded log bundle and use the MCP tools directly. diff --git a/docs/src/content/docs/reference/imports.md b/docs/src/content/docs/reference/imports.md index ec90dcf0eac..a029daa449c 100644 --- a/docs/src/content/docs/reference/imports.md +++ b/docs/src/content/docs/reference/imports.md @@ -44,7 +44,7 @@ imports: `uses` is an alias for `path`; `with` is an alias for `inputs`. -Conditional frontmatter imports are not supported. If an experiment should vary shared prompt content, keep imports unconditional and gate `{{#runtime-import ...}}` inside your `{{#if experiments. ...}}` block instead. +Conditional frontmatter imports are not supported. If an experiment should vary shared prompt content, keep imports unconditional and gate `{{#runtime-import? ...}}` (optional form) inside your `{{#if experiments. ...}}` block instead. The optional form is not promoted to unconditional lock-file macros, so the content is only injected when the condition is true at runtime. ### Single-import constraint diff --git a/pkg/parser/import_bfs.go b/pkg/parser/import_bfs.go index 7286f63d2bc..8e3c118e09f 100644 --- a/pkg/parser/import_bfs.go +++ b/pkg/parser/import_bfs.go @@ -566,7 +566,7 @@ func parseImportSpecsFromArray(items []any) ([]ImportSpec, error) { } } if _, hasIf := importItem["if"]; hasIf { - return nil, errors.New("import 'if' is no longer supported; use {{#if ...}}{{#runtime-import ...}}{{/if}} for experiment-specific prompt imports") + return nil, errors.New("import 'if' is no longer supported; use {{#if ...}}{{#runtime-import? ...}}{{/if}} for experiment-specific prompt imports") } specs = append(specs, ImportSpec{Path: pathStr, Inputs: inputs}) default: From 1fccdb412b35336304ee01fc0c7371e7a4a789bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 02:02:32 +0000 Subject: [PATCH 4/5] fix: update TestCompileWorkflowWithConditionalImport to expect imports.if rejection Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../daily-safe-output-optimizer.lock.yml | 2 +- pkg/workflow/imports_test.go | 28 ++++++------------- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/.github/workflows/daily-safe-output-optimizer.lock.yml b/.github/workflows/daily-safe-output-optimizer.lock.yml index 92ef8595892..78e13dbff4b 100644 --- a/.github/workflows/daily-safe-output-optimizer.lock.yml +++ b/.github/workflows/daily-safe-output-optimizer.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"981668a6b9cdde49ed29e740ddbcb16b3318a431fcae537bb4c848fe801ec300","body_hash":"590bf0d6a114134cf770d6249b47e5ea4b734ea124dd5533eedf7645031279db","strict":true,"agent_id":"claude","engine_versions":{"claude":"2.1.168"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"d21c08c86d1578ee38b789595105775c904522cce40aa867ab4aeaf5c2b50502","body_hash":"17a79d65a6e1affd699d34d4a9cf71318b9ca152a0ba8c1ff5f2521886f2db63","strict":true,"agent_id":"claude","engine_versions":{"claude":"2.1.168"}} # gh-aw-manifest: {"version":1,"secrets":["ANTHROPIC_API_KEY","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"docker/build-push-action","sha":"f9f3042f7e2789586610d6e8b85c8f03e5195baf","version":"v7.2.0"},{"repo":"docker/setup-buildx-action","sha":"d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5","version":"v4.1.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.0"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.0"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.0"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) diff --git a/pkg/workflow/imports_test.go b/pkg/workflow/imports_test.go index fc5dbb9c2d4..9a88e092cf0 100644 --- a/pkg/workflow/imports_test.go +++ b/pkg/workflow/imports_test.go @@ -214,26 +214,14 @@ Main workflow body. } compiler := workflow.NewCompiler() - if err := compiler.CompileWorkflow(workflowPath); err != nil { - t.Fatalf("CompileWorkflow failed: %v", err) - } - - lockFilePath := stringutil.MarkdownToLockFile(workflowPath) - lockFileContent, err := os.ReadFile(lockFilePath) - if err != nil { - t.Fatalf("Failed to read lock file: %v", err) - } - - compiled := string(lockFileContent) - assertions := []string{ - `{{#if experiments.strategy == "eager"}}`, - "{{/if}}", - "needs.activation.outputs.strategy == 'eager'", - } - for _, expected := range assertions { - if !strings.Contains(compiled, expected) { - t.Errorf("Expected compiled workflow to contain %q", expected) - } + err := compiler.CompileWorkflow(workflowPath) + if err == nil { + t.Fatal("Expected CompileWorkflow to fail for imports.if, but it succeeded") + } + // imports.if is rejected — either by schema validation or the migration check. + errMsg := err.Error() + if !strings.Contains(errMsg, "if") { + t.Errorf("Expected rejection of imports.if, got unrelated error: %v", err) } } From 9416729d778ca91a5bfe395971dc1ffbf1d95e7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 02:08:59 +0000 Subject: [PATCH 5/5] fix: tighten imports.if rejection assertion in TestCompileWorkflowWithConditionalImport Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/imports_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/workflow/imports_test.go b/pkg/workflow/imports_test.go index 9a88e092cf0..631e5f00585 100644 --- a/pkg/workflow/imports_test.go +++ b/pkg/workflow/imports_test.go @@ -218,9 +218,10 @@ Main workflow body. if err == nil { t.Fatal("Expected CompileWorkflow to fail for imports.if, but it succeeded") } - // imports.if is rejected — either by schema validation or the migration check. + // imports.if is rejected — either by schema validation ("Unknown property: if") + // or by the migration guard ("import 'if' is no longer supported"). errMsg := err.Error() - if !strings.Contains(errMsg, "if") { + if !strings.Contains(errMsg, "Unknown property: if") && !strings.Contains(errMsg, "import 'if' is no longer supported") { t.Errorf("Expected rejection of imports.if, got unrelated error: %v", err) } }