From ceeabc7ad2ad29cc5450b3b9d61f6af97e200b88 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Jul 2026 16:17:19 +0000 Subject: [PATCH 1/6] Initial plan From 544b5a0b62621f5a7c69c6b60a9583fa5da26764 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Jul 2026 16:34:08 +0000 Subject: [PATCH 2/6] Finalize daily SPDD ADR follow-up Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- ...-cache-memory-working-tree-sanitization.md | 5 +-- docs/adr/26666-add-pre-agent-steps-support.md | 4 +-- ...le-expiration-for-action-failure-issues.md | 8 ++--- ...-union-activation-permission-derivation.md | 4 +-- ...aw-native-action-updates-at-cli-version.md | 5 +-- .../content/docs/reference/cache-memory.md | 2 ++ docs/src/content/docs/reference/ephemerals.md | 2 ++ .../src/content/docs/reference/frontmatter.md | 2 +- docs/src/content/docs/reference/steps-jobs.md | 24 +++++++++++++- .../content/docs/setup/creating-workflows.mdx | 11 +++++++ pkg/workflow/compiler_pre_agent_steps_test.go | 11 ++++++- pkg/workflow/reactions_test.go | 32 +++++++++++++++++++ 12 files changed, 95 insertions(+), 15 deletions(-) diff --git a/docs/adr/26587-pre-agent-cache-memory-working-tree-sanitization.md b/docs/adr/26587-pre-agent-cache-memory-working-tree-sanitization.md index 985a764b2cc..e8c55522c02 100644 --- a/docs/adr/26587-pre-agent-cache-memory-working-tree-sanitization.md +++ b/docs/adr/26587-pre-agent-cache-memory-working-tree-sanitization.md @@ -1,7 +1,7 @@ # ADR-26587: Pre-Agent Cache-Memory Working-Tree Sanitization **Date**: 2026-04-16 -**Status**: Draft +**Status**: Accepted **Deciders**: pelikhan, Copilot --- @@ -77,6 +77,7 @@ The existing integrity and threat-detection pipeline could be trusted to catch m 2. Extensions in `GH_AW_ALLOWED_EXTENSIONS` **MUST** include the leading dot (e.g. `.json`, `.md`); files with no extension are treated as if their extension is an empty string. 3. When `GH_AW_ALLOWED_EXTENSIONS` is absent or empty, implementations **MUST NOT** remove any files based on extension (all extensions are allowed). 4. Implementations **SHOULD** log each removed file path and its extension, and **SHOULD** log a final count of removed files. +5. If `GH_AW_ALLOWED_EXTENSIONS` contains any non-empty entry without a leading dot, implementations **SHOULD** emit a warning identifying the malformed entry before continuing with any fallback or filtering behavior. ### Go-to-Shell Configuration Contract @@ -90,4 +91,4 @@ An implementation is considered conformant with this ADR if it satisfies all **M --- -*This is a DRAFT ADR generated by the [Design Decision Gate](https://github.com/github/gh-aw/actions/runs/24514620900) workflow. The PR author must review, complete, and finalize this document before the PR can merge.* +*Accepted on 2026-07-19 after validating the Go-side env-var contract and the shell-script sanitization tests.* diff --git a/docs/adr/26666-add-pre-agent-steps-support.md b/docs/adr/26666-add-pre-agent-steps-support.md index f235237eb9e..32015e81866 100644 --- a/docs/adr/26666-add-pre-agent-steps-support.md +++ b/docs/adr/26666-add-pre-agent-steps-support.md @@ -1,7 +1,7 @@ # ADR-26666: Introduce `pre-agent-steps` as a Distinct Workflow Extension Point **Date**: 2026-04-16 -**Status**: Draft +**Status**: Accepted **Deciders**: pelikhan, Copilot --- @@ -85,4 +85,4 @@ An implementation is considered conformant with this ADR if it satisfies all **M --- -*This is a DRAFT ADR generated by the [Design Decision Gate](https://github.com/github/gh-aw/actions/runs/24519930482) workflow. The PR author must review, complete, and finalize this document before the PR can merge.* +*Accepted on 2026-07-19 after validating compiler ordering and adding a conformance test for workflows that define only `pre-agent-steps`.* diff --git a/docs/adr/26688-configurable-expiration-for-action-failure-issues.md b/docs/adr/26688-configurable-expiration-for-action-failure-issues.md index b72c9aafe75..880df57b59c 100644 --- a/docs/adr/26688-configurable-expiration-for-action-failure-issues.md +++ b/docs/adr/26688-configurable-expiration-for-action-failure-issues.md @@ -1,7 +1,7 @@ # ADR-26688: Configurable Expiration for Action Failure Issues via aw.json **Date**: 2026-04-16 -**Status**: Draft +**Status**: Accepted **Deciders**: pelikhan, Copilot --- @@ -43,7 +43,7 @@ Setting `GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS` as a GitHub Actions repositor #### Neutral - The `DefaultActionFailureIssueExpiresHours` constant (`24 * 7 = 168`) is now an exported symbol, making it accessible to other packages that may need the canonical default. -- Documentation in `docs/src/content/docs/guides/ephemerals.md` is updated to describe the new field alongside the existing `runs_on` maintenance option. +- Documentation in `docs/src/content/docs/reference/ephemerals.md` is updated to describe the new field alongside the existing `runs_on` maintenance option. --- @@ -60,7 +60,7 @@ Setting `GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS` as a GitHub Actions repositor ### Compilation and Environment Variable Propagation 1. The workflow compiler **MUST** read `action_failure_issue_expires` from `aw.json` (via `LoadRepoConfig`) during `buildConclusionJob` and inject the resolved value as the `GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS` environment variable into the generated conclusion-job YAML. -2. If `aw.json` cannot be loaded or parsed, the compiler **MUST** log a warning and fall back to the default value of `168`; it **MUST NOT** fail the build. +2. If `.github/workflows/aw.json` exists but cannot be loaded or parsed, the compiler **MUST** emit a warning that identifies the file path and the fallback value (`168`) before continuing; it **MUST NOT** fail the build. 3. The injected environment variable value **MUST** be a positive integer string representing hours. ### Runtime Handling @@ -75,4 +75,4 @@ An implementation is considered conformant with this ADR if it satisfies all **M --- -*This is a DRAFT ADR generated by the [Design Decision Gate](https://github.com/github/gh-aw/actions/runs/24522565038) workflow. The PR author must review, complete, and finalize this document before the PR can merge.* +*Accepted on 2026-07-19 after validating the `aw.json`-driven expiration path and documenting the fallback warning behavior.* diff --git a/docs/adr/26693-reaction-target-scoping-and-union-activation-permission-derivation.md b/docs/adr/26693-reaction-target-scoping-and-union-activation-permission-derivation.md index 351c172c793..7d0963a2668 100644 --- a/docs/adr/26693-reaction-target-scoping-and-union-activation-permission-derivation.md +++ b/docs/adr/26693-reaction-target-scoping-and-union-activation-permission-derivation.md @@ -1,7 +1,7 @@ # ADR-26693: Reaction Target Scoping and Union Activation Permission Derivation **Date**: 2026-04-16 -**Status**: Draft +**Status**: Accepted **Deciders**: pelikhan, Copilot --- @@ -88,4 +88,4 @@ An implementation is considered conformant with this ADR if it satisfies all **M --- -*This is a DRAFT ADR generated by the [Design Decision Gate](https://github.com/github/gh-aw/actions/runs/24526383923) workflow. The PR author must review, complete, and finalize this document before the PR can merge.* +*Accepted on 2026-07-19 after validating target-scoped permission derivation and parser defaults for scalar and object reaction forms.* diff --git a/docs/adr/26827-cap-gh-aw-native-action-updates-at-cli-version.md b/docs/adr/26827-cap-gh-aw-native-action-updates-at-cli-version.md index 401e8c7ed06..638d0ce7fc0 100644 --- a/docs/adr/26827-cap-gh-aw-native-action-updates-at-cli-version.md +++ b/docs/adr/26827-cap-gh-aw-native-action-updates-at-cli-version.md @@ -1,7 +1,7 @@ # ADR-26827: Cap gh-aw Native Action Updates at Running CLI Version **Date**: 2026-04-17 -**Status**: Draft +**Status**: Accepted **Deciders**: dsyme, Copilot --- @@ -55,6 +55,7 @@ Introduce a checked-in file (e.g., `compatibility.json`) that explicitly maps CL 1. Implementations **MUST** classify an action as a "gh-aw native action" if and only if its base repository (resolved via `gitutil.ExtractBaseRepo`) is `github/gh-aw` or `github/gh-aw-actions`. 2. Implementations **MUST NOT** classify any other action as a gh-aw native action, including actions in forked or similarly named repositories. +3. Future native action repositories **MAY** be added only by an explicit allowlist expansion in a later release; until then, unknown repositories **MUST** remain non-native. ### Version Cap Enforcement @@ -75,4 +76,4 @@ An implementation is considered conformant with this ADR if it satisfies all **M --- -*This is a DRAFT ADR generated by the [Design Decision Gate](https://github.com/github/gh-aw/actions/runs/24560253328) workflow. The PR author must review, complete, and finalize this document before the PR can merge.* +*Accepted on 2026-07-19 after validating the CLI-version cap behavior and documenting the explicit-allowlist extension model for future native action repositories.* diff --git a/docs/src/content/docs/reference/cache-memory.md b/docs/src/content/docs/reference/cache-memory.md index f005cdb7d77..d1bd77dec5a 100644 --- a/docs/src/content/docs/reference/cache-memory.md +++ b/docs/src/content/docs/reference/cache-memory.md @@ -47,6 +47,8 @@ tools: If files with disallowed extensions are found, the workflow will report validation failures. +When a cache is restored for agent execution, gh-aw also strips execute bits from restored working-tree files and removes disallowed file types before the agent can read them. See [ADR-26587](https://github.com/github/gh-aw/blob/main/docs/adr/26587-pre-agent-cache-memory-working-tree-sanitization.md) for the pre-agent sanitization contract behind `allowed-extensions`. + ## Multiple Configurations ```aw wrap diff --git a/docs/src/content/docs/reference/ephemerals.md b/docs/src/content/docs/reference/ephemerals.md index 85af094b112..41fc524bbb1 100644 --- a/docs/src/content/docs/reference/ephemerals.md +++ b/docs/src/content/docs/reference/ephemerals.md @@ -140,6 +140,8 @@ Customize the runner: `action_failure_issue_expires` sets expiration, in hours, for failure issues opened by the conclusion job, including grouped parent issues when `group-reports: true`. The default is `168` (7 days). +If `.github/workflows/aw.json` is present but contains invalid JSON or fails schema validation, compilation keeps the default `168`-hour expiration and emits a warning that identifies the config path and fallback value. + `disabled_jobs` lets you omit specific maintenance jobs from the generated workflow. Job IDs are case-insensitive, and `_` / `-` are treated equivalently. Supported job IDs: diff --git a/docs/src/content/docs/reference/frontmatter.md b/docs/src/content/docs/reference/frontmatter.md index 1e132d77c28..718ed370e0f 100644 --- a/docs/src/content/docs/reference/frontmatter.md +++ b/docs/src/content/docs/reference/frontmatter.md @@ -107,7 +107,7 @@ imports: See [Imports](/gh-aw/reference/imports/) for complete documentation on syntax, shared components, APM package dependencies, and composition patterns. -### Custom Steps and Jobs (`steps:`, `pre-agent-steps:`, `post-steps:`, `jobs:`) +### Custom Steps and Jobs (`pre-steps:`, `steps:`, `pre-agent-steps:`, `post-steps:`, `jobs:`) Add deterministic steps before or after agentic execution, or define full custom GitHub Actions jobs that run before the agent. See [Custom Steps and Jobs](/gh-aw/reference/steps-jobs/) for complete documentation. diff --git a/docs/src/content/docs/reference/steps-jobs.md b/docs/src/content/docs/reference/steps-jobs.md index cda48b71921..b055f3b2b75 100644 --- a/docs/src/content/docs/reference/steps-jobs.md +++ b/docs/src/content/docs/reference/steps-jobs.md @@ -1,6 +1,6 @@ --- title: Custom Steps and Jobs -description: "Add deterministic pre-processing steps and custom GitHub Actions jobs to agentic workflows using steps:, pre-agent-steps:, post-steps:, and jobs:" +description: "Add deterministic pre-processing steps and custom GitHub Actions jobs to agentic workflows using pre-steps:, steps:, pre-agent-steps:, post-steps:, and jobs:" sidebar: order: 820 --- @@ -9,6 +9,15 @@ Custom steps and jobs let you mix deterministic computation with agentic executi See [DeterministicOps](/gh-aw/patterns/deterministic-ops/) for patterns combining computation with AI reasoning. +## Choosing the Right Step Hook + +Use these top-level step hooks in this order: + +1. `pre-steps:` — runs before checkout and the rest of the built-in agent-job setup. Use this for short-lived token minting or anything that must happen before repository checkout. +2. `steps:` — runs after checkout and the normal runtime/bootstrap steps, but before the final pre-agent phase. Use this for deterministic preprocessing that needs the checked-out repository. +3. `pre-agent-steps:` — runs after framework-owned initialization such as checkout cleanup and base-branch restoration, and before MCP startup and engine execution. Use this for last-moment environment preparation immediately before the agent starts. +4. `post-steps:` — runs after the engine finishes. Use this for cleanup, summaries, uploads, or follow-up automation. + ## Custom Steps (`steps:`) Add custom steps before agentic execution. If unspecified, a default checkout step is added automatically. @@ -21,6 +30,19 @@ steps: Use custom steps to precompute data, filter triggers, or prepare context for AI agents. Steps can also short-circuit the agent by writing a `noop` entry to `$GH_AW_SAFE_OUTPUTS` — the harness detects this at startup and exits cleanly without incurring any AI inference cost. See [Skip the Agent from Steps Using `noop`](/gh-aw/reference/cost-management/#skip-the-agent-from-steps-using-noop) for details. +## Custom Pre-Checkout Steps (`pre-steps:`) + +Add custom steps at the very beginning of the agent job, before checkout and the rest of the compiler-managed setup. + +```yaml wrap +pre-steps: + - name: Mint checkout token + id: checkout_app + uses: actions/create-github-app-token@v2 +``` + +Use pre-steps when later checkout or setup must consume outputs from a step in the same job. + ## Custom Pre-Agent Steps (`pre-agent-steps:`) Add custom steps before MCP gateway startup in the agent job so prerequisite MCP installation/configuration can happen first. diff --git a/docs/src/content/docs/setup/creating-workflows.mdx b/docs/src/content/docs/setup/creating-workflows.mdx index e9a3c6aed28..bfca656f834 100644 --- a/docs/src/content/docs/setup/creating-workflows.mdx +++ b/docs/src/content/docs/setup/creating-workflows.mdx @@ -99,6 +99,17 @@ After merging the pull request, you can run the workflow to see it in action. Ei - trigger runs manually from the Actions tab in GitHub.com, or - use [the `gh aw run` command](/gh-aw/setup/cli/#run) to trigger runs from your terminal. +## Choose the Right Step Lifecycle Hook + +When you add deterministic setup or cleanup to a workflow, use the smallest hook that matches the lifecycle point you need: + +1. `pre-steps:` — before checkout and the rest of the compiler-managed setup. +2. `steps:` — after checkout and normal bootstrap, while the job is still in its general preprocessing phase. +3. `pre-agent-steps:` — after checkout cleanup and base-branch restoration, immediately before MCP startup and engine execution. +4. `post-steps:` — after the engine completes. + +Reach for `pre-agent-steps:` when you need a final preparation phase right before the agent starts, and `pre-steps:` when later checkout or setup needs outputs from an earlier step in the same job. See [Custom Steps and Jobs](/gh-aw/reference/steps-jobs/) for full examples. + ## Initialize the Repository Running `gh aw init` is **required** to enable the authoring experience in the GitHub code agent. This step configures your repository so that you can create and modify agentic workflows directly from [github.com](https://github.com) or the GitHub mobile app, using the Copilot coding agent. diff --git a/pkg/workflow/compiler_pre_agent_steps_test.go b/pkg/workflow/compiler_pre_agent_steps_test.go index a23bb45c6ba..64f4c773dbb 100644 --- a/pkg/workflow/compiler_pre_agent_steps_test.go +++ b/pkg/workflow/compiler_pre_agent_steps_test.go @@ -18,6 +18,8 @@ func TestPreAgentStepsGeneration(t *testing.T) { on: push permissions: contents: read +checkout: + force-clean-git-credentials: true pre-agent-steps: - name: Finalize prompt context run: echo "finalize" @@ -50,17 +52,24 @@ Test pre-agent-steps. } startMCPGatewayIndex := indexInNonCommentLines(lockContent, "- name: Start MCP Gateway") + cleanCheckoutCredentialsIndex := indexInNonCommentLines(lockContent, "- name: Clean git credentials after checkout") preAgentStepIndex := indexInNonCommentLines(lockContent, "- name: Finalize prompt context") aiStepIndex := indexInNonCommentLines(lockContent, "- name: Execute Claude Code CLI") - if startMCPGatewayIndex == -1 || preAgentStepIndex == -1 || aiStepIndex == -1 { + if startMCPGatewayIndex == -1 || cleanCheckoutCredentialsIndex == -1 || preAgentStepIndex == -1 || aiStepIndex == -1 { t.Fatal("Could not find expected steps in generated workflow") } + if cleanCheckoutCredentialsIndex >= preAgentStepIndex { + t.Errorf("Pre-agent-step (%d) should appear after Clean git credentials after checkout (%d)", preAgentStepIndex, cleanCheckoutCredentialsIndex) + } if preAgentStepIndex >= startMCPGatewayIndex { t.Errorf("Pre-agent-step (%d) should appear before Start MCP Gateway (%d)", preAgentStepIndex, startMCPGatewayIndex) } if preAgentStepIndex >= aiStepIndex { t.Errorf("Pre-agent-step (%d) should appear before AI execution step (%d)", preAgentStepIndex, aiStepIndex) } + if strings.Contains(lockContent, "pre-agent-steps: []") || strings.Contains(lockContent, "post-steps: []") || strings.Contains(lockContent, "pre-steps: []") { + t.Error("Expected no empty steps block markers when only pre-agent-steps are configured") + } } func TestPreAgentStepsImportsMergeOrder(t *testing.T) { diff --git a/pkg/workflow/reactions_test.go b/pkg/workflow/reactions_test.go index f08b8984b1f..6adbe9122d5 100644 --- a/pkg/workflow/reactions_test.go +++ b/pkg/workflow/reactions_test.go @@ -210,3 +210,35 @@ func TestParseReactionConfigMapAllTargetsDisabled(t *testing.T) { t.Fatal("Expected parseReactionConfig to fail when all reaction targets are disabled") } } + +func TestParseReactionConfigScalarLeavesTargetPointersNil(t *testing.T) { + reaction, issues, pullRequests, discussions, err := parseReactionConfig("rocket") + if err != nil { + t.Fatalf("parseReactionConfig(scalar) returned unexpected error: %v", err) + } + if reaction != "rocket" { + t.Fatalf("Expected reaction type 'rocket', got %q", reaction) + } + if issues != nil || pullRequests != nil || discussions != nil { + t.Fatalf("Expected scalar reaction config to leave target pointers nil, got issues=%v pullRequests=%v discussions=%v", issues, pullRequests, discussions) + } +} + +func TestParseReactionConfigMapDefaultsTypeAndTargets(t *testing.T) { + reaction, issues, pullRequests, discussions, err := parseReactionConfig(map[string]any{}) + if err != nil { + t.Fatalf("parseReactionConfig(empty map) returned unexpected error: %v", err) + } + if reaction != "eyes" { + t.Fatalf("Expected default reaction type 'eyes', got %q", reaction) + } + if issues == nil || !*issues { + t.Fatalf("Expected issues target to default to true, got %v", issues) + } + if pullRequests == nil || !*pullRequests { + t.Fatalf("Expected pull-requests target to default to true, got %v", pullRequests) + } + if discussions == nil || !*discussions { + t.Fatalf("Expected discussions target to default to true, got %v", discussions) + } +} From 46f9cf3b128aa9a6782c293752c18836b132f250 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Jul 2026 16:40:19 +0000 Subject: [PATCH 3/6] Fix pre-agent step test message Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/compiler_pre_agent_steps_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/workflow/compiler_pre_agent_steps_test.go b/pkg/workflow/compiler_pre_agent_steps_test.go index 64f4c773dbb..a97f587de6b 100644 --- a/pkg/workflow/compiler_pre_agent_steps_test.go +++ b/pkg/workflow/compiler_pre_agent_steps_test.go @@ -59,7 +59,7 @@ Test pre-agent-steps. t.Fatal("Could not find expected steps in generated workflow") } if cleanCheckoutCredentialsIndex >= preAgentStepIndex { - t.Errorf("Pre-agent-step (%d) should appear after Clean git credentials after checkout (%d)", preAgentStepIndex, cleanCheckoutCredentialsIndex) + t.Errorf("Clean git credentials after checkout (%d) should appear before pre-agent-step (%d)", cleanCheckoutCredentialsIndex, preAgentStepIndex) } if preAgentStepIndex >= startMCPGatewayIndex { t.Errorf("Pre-agent-step (%d) should appear before Start MCP Gateway (%d)", preAgentStepIndex, startMCPGatewayIndex) From e9f969596da324c2c401bd354a76e86e33f59c86 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Jul 2026 18:20:09 +0000 Subject: [PATCH 4/6] fix review feedback on spec docs and aw.json warnings Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- docs/adr/26666-add-pre-agent-steps-support.md | 8 ++-- ...le-expiration-for-action-failure-issues.md | 2 +- docs/src/content/docs/reference/ephemerals.md | 2 +- docs/src/content/docs/reference/steps-jobs.md | 28 +++++++------- .../content/docs/setup/creating-workflows.mdx | 9 +---- pkg/workflow/compiler_pre_agent_steps_test.go | 21 +++++++++- pkg/workflow/compiler_repo_config.go | 15 ++++++++ pkg/workflow/compiler_repo_config_test.go | 38 +++++++++++++++++++ 8 files changed, 93 insertions(+), 30 deletions(-) diff --git a/docs/adr/26666-add-pre-agent-steps-support.md b/docs/adr/26666-add-pre-agent-steps-support.md index 32015e81866..a8222f9a27e 100644 --- a/docs/adr/26666-add-pre-agent-steps-support.md +++ b/docs/adr/26666-add-pre-agent-steps-support.md @@ -10,11 +10,11 @@ ### Context -The gh-aw workflow compiler translates markdown workflow files into GitHub Actions YAML. It already supports two custom step extension points: `pre-steps` (injected before the checkout step, very early in the agent job) and `post-steps` (injected after AI engine execution). A gap exists for steps that must run after all framework initialization — including checkout, credential setup, and environment preparation — but *immediately before* the AI engine starts. Use cases include final context preparation scripts, last-moment environment variable injection, and validations that depend on checked-out code being available. +The gh-aw workflow compiler translates markdown workflow files into GitHub Actions YAML. It already supports two custom step extension points: `pre-steps` (injected before the checkout step, very early in the agent job) and `post-steps` (injected after AI engine execution). A gap exists for steps that must run after checkout, checkout cleanup, and PR/base restoration, but before MCP startup and later engine-owned runtime setup. Use cases include final context preparation scripts, last-moment environment variable injection, and validations that depend on checked-out code being available. ### Decision -We will add a new `pre-agent-steps` frontmatter field that is injected into the compiled GitHub Actions YAML immediately before the engine execution step, after all setup steps (including the CLI proxy start step). Like `pre-steps`, `pre-agent-steps` participates in the import merge system: imported `pre-agent-steps` are prepended before the main workflow's `pre-agent-steps`, preserving a deterministic, layered execution order. The existing `validateStepsSecrets` infrastructure is extended to cover the new field, applying identical secret expression restrictions. +We will add a new `pre-agent-steps` frontmatter field that is injected into the compiled GitHub Actions YAML after checkout cleanup and trusted base-restore steps, but before MCP startup and the later engine execution sequence. Like `pre-steps`, `pre-agent-steps` participates in the import merge system: imported `pre-agent-steps` are prepended before the main workflow's `pre-agent-steps`, preserving a deterministic, layered execution order. The existing `validateStepsSecrets` infrastructure is extended to cover the new field, applying identical secret expression restrictions. ### Alternatives Considered @@ -61,7 +61,7 @@ Users could approximate "run before engine" behaviour by placing steps in `post- ### Compilation and Placement 1. Implementations **MUST** inject all resolved `pre-agent-steps` into the compiled GitHub Actions YAML immediately before the engine execution step. -2. `pre-agent-steps` **MUST** be placed after all framework setup steps (including the CLI proxy start step) and **MUST NOT** be placed before any framework-owned initialization steps. +2. `pre-agent-steps` **MUST** be placed after checkout cleanup and any trusted PR/base restoration steps, and **MUST** be placed before MCP startup and the later engine execution sequence. 3. If no `pre-agent-steps` are defined (neither in the main workflow nor in any imports), the compiler **MUST NOT** emit any placeholder or empty steps block. ### Import Merge Ordering @@ -81,7 +81,7 @@ Users could approximate "run before engine" behaviour by placing steps in `post- ### Conformance -An implementation is considered conformant with this ADR if it satisfies all **MUST** and **MUST NOT** requirements above. Specifically: `pre-agent-steps` are injected immediately before the engine execution step (not before setup steps), imports are merged in prepend order, secret validation is applied, and action pinning is applied. Failure to meet any **MUST** or **MUST NOT** requirement constitutes non-conformance. +An implementation is considered conformant with this ADR if it satisfies all **MUST** and **MUST NOT** requirements above. Specifically: `pre-agent-steps` are injected after checkout cleanup / trusted restore steps and before MCP startup and engine execution, imports are merged in prepend order, secret validation is applied, and action pinning is applied. Failure to meet any **MUST** or **MUST NOT** requirement constitutes non-conformance. --- diff --git a/docs/adr/26688-configurable-expiration-for-action-failure-issues.md b/docs/adr/26688-configurable-expiration-for-action-failure-issues.md index 880df57b59c..e472c687bc9 100644 --- a/docs/adr/26688-configurable-expiration-for-action-failure-issues.md +++ b/docs/adr/26688-configurable-expiration-for-action-failure-issues.md @@ -60,7 +60,7 @@ Setting `GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS` as a GitHub Actions repositor ### Compilation and Environment Variable Propagation 1. The workflow compiler **MUST** read `action_failure_issue_expires` from `aw.json` (via `LoadRepoConfig`) during `buildConclusionJob` and inject the resolved value as the `GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS` environment variable into the generated conclusion-job YAML. -2. If `.github/workflows/aw.json` exists but cannot be loaded or parsed, the compiler **MUST** emit a warning that identifies the file path and the fallback value (`168`) before continuing; it **MUST NOT** fail the build. +2. If `.github/workflows/aw.json` exists but cannot be loaded, parsed, or validated, the compiler **MUST** emit a warning that identifies the file path and the fallback value (`168`) before continuing; it **MUST NOT** fail the build. 3. The injected environment variable value **MUST** be a positive integer string representing hours. ### Runtime Handling diff --git a/docs/src/content/docs/reference/ephemerals.md b/docs/src/content/docs/reference/ephemerals.md index 41fc524bbb1..0dc49f058c1 100644 --- a/docs/src/content/docs/reference/ephemerals.md +++ b/docs/src/content/docs/reference/ephemerals.md @@ -140,7 +140,7 @@ Customize the runner: `action_failure_issue_expires` sets expiration, in hours, for failure issues opened by the conclusion job, including grouped parent issues when `group-reports: true`. The default is `168` (7 days). -If `.github/workflows/aw.json` is present but contains invalid JSON or fails schema validation, compilation keeps the default `168`-hour expiration and emits a warning that identifies the config path and fallback value. +If `.github/workflows/aw.json` is present but cannot be loaded, parsed, or validated, compilation keeps the default `168`-hour expiration and emits a warning that identifies the config path and fallback value. `disabled_jobs` lets you omit specific maintenance jobs from the generated workflow. Job IDs are case-insensitive, and `_` / `-` are treated equivalently. diff --git a/docs/src/content/docs/reference/steps-jobs.md b/docs/src/content/docs/reference/steps-jobs.md index b055f3b2b75..0c8e0853a6d 100644 --- a/docs/src/content/docs/reference/steps-jobs.md +++ b/docs/src/content/docs/reference/steps-jobs.md @@ -13,26 +13,14 @@ See [DeterministicOps](/gh-aw/patterns/deterministic-ops/) for patterns combinin Use these top-level step hooks in this order: -1. `pre-steps:` — runs before checkout and the rest of the built-in agent-job setup. Use this for short-lived token minting or anything that must happen before repository checkout. +1. `pre-steps:` — runs before checkout and most of the built-in agent-job setup, though compiler-injected OTLP masking steps may run earlier. Use this for short-lived token minting or anything that must happen before repository checkout. 2. `steps:` — runs after checkout and the normal runtime/bootstrap steps, but before the final pre-agent phase. Use this for deterministic preprocessing that needs the checked-out repository. 3. `pre-agent-steps:` — runs after framework-owned initialization such as checkout cleanup and base-branch restoration, and before MCP startup and engine execution. Use this for last-moment environment preparation immediately before the agent starts. 4. `post-steps:` — runs after the engine finishes. Use this for cleanup, summaries, uploads, or follow-up automation. -## Custom Steps (`steps:`) - -Add custom steps before agentic execution. If unspecified, a default checkout step is added automatically. - -```yaml wrap -steps: - - name: Install dependencies - run: npm ci -``` - -Use custom steps to precompute data, filter triggers, or prepare context for AI agents. Steps can also short-circuit the agent by writing a `noop` entry to `$GH_AW_SAFE_OUTPUTS` — the harness detects this at startup and exits cleanly without incurring any AI inference cost. See [Skip the Agent from Steps Using `noop`](/gh-aw/reference/cost-management/#skip-the-agent-from-steps-using-noop) for details. - ## Custom Pre-Checkout Steps (`pre-steps:`) -Add custom steps at the very beginning of the agent job, before checkout and the rest of the compiler-managed setup. +Add custom steps before checkout and the rest of the pre-checkout agent-job setup. Compiler-injected OTLP masking steps may still run earlier. ```yaml wrap pre-steps: @@ -43,6 +31,18 @@ pre-steps: Use pre-steps when later checkout or setup must consume outputs from a step in the same job. +## Custom Steps (`steps:`) + +Add custom steps before agentic execution. If unspecified, a default checkout step is added automatically. + +```yaml wrap +steps: + - name: Install dependencies + run: npm ci +``` + +Use custom steps to precompute data, filter triggers, or prepare context for AI agents. Steps can also short-circuit the agent by writing a `noop` entry to `$GH_AW_SAFE_OUTPUTS` — the harness detects this at startup and exits cleanly without incurring any AI inference cost. See [Skip the Agent from Steps Using `noop`](/gh-aw/reference/cost-management/#skip-the-agent-from-steps-using-noop) for details. + ## Custom Pre-Agent Steps (`pre-agent-steps:`) Add custom steps before MCP gateway startup in the agent job so prerequisite MCP installation/configuration can happen first. diff --git a/docs/src/content/docs/setup/creating-workflows.mdx b/docs/src/content/docs/setup/creating-workflows.mdx index bfca656f834..9014a56029c 100644 --- a/docs/src/content/docs/setup/creating-workflows.mdx +++ b/docs/src/content/docs/setup/creating-workflows.mdx @@ -101,14 +101,7 @@ After merging the pull request, you can run the workflow to see it in action. Ei ## Choose the Right Step Lifecycle Hook -When you add deterministic setup or cleanup to a workflow, use the smallest hook that matches the lifecycle point you need: - -1. `pre-steps:` — before checkout and the rest of the compiler-managed setup. -2. `steps:` — after checkout and normal bootstrap, while the job is still in its general preprocessing phase. -3. `pre-agent-steps:` — after checkout cleanup and base-branch restoration, immediately before MCP startup and engine execution. -4. `post-steps:` — after the engine completes. - -Reach for `pre-agent-steps:` when you need a final preparation phase right before the agent starts, and `pre-steps:` when later checkout or setup needs outputs from an earlier step in the same job. See [Custom Steps and Jobs](/gh-aw/reference/steps-jobs/) for full examples. +When you add deterministic setup or cleanup to a workflow, choose the smallest hook that matches the lifecycle point you need. See [Custom Steps and Jobs](/gh-aw/reference/steps-jobs/#choosing-the-right-step-hook) for the canonical ordering guide, including the distinction between pre-checkout `pre-steps:` and the later `pre-agent-steps:` phase. ## Initialize the Repository diff --git a/pkg/workflow/compiler_pre_agent_steps_test.go b/pkg/workflow/compiler_pre_agent_steps_test.go index a97f587de6b..32847375645 100644 --- a/pkg/workflow/compiler_pre_agent_steps_test.go +++ b/pkg/workflow/compiler_pre_agent_steps_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/github/gh-aw/pkg/testutil" + "gopkg.in/yaml.v3" ) func TestPreAgentStepsGeneration(t *testing.T) { @@ -67,8 +68,24 @@ Test pre-agent-steps. if preAgentStepIndex >= aiStepIndex { t.Errorf("Pre-agent-step (%d) should appear before AI execution step (%d)", preAgentStepIndex, aiStepIndex) } - if strings.Contains(lockContent, "pre-agent-steps: []") || strings.Contains(lockContent, "post-steps: []") || strings.Contains(lockContent, "pre-steps: []") { - t.Error("Expected no empty steps block markers when only pre-agent-steps are configured") + var compiled struct { + Jobs map[string]map[string]any `yaml:"jobs"` + } + if err := yaml.Unmarshal(content, &compiled); err != nil { + t.Fatalf("Failed to parse generated lock file as YAML: %v", err) + } + for jobName, job := range compiled.Jobs { + rawSteps, hasSteps := job["steps"] + if !hasSteps { + continue + } + steps, ok := rawSteps.([]any) + if !ok { + t.Fatalf("Expected %s job steps to decode as a YAML sequence, got %T", jobName, rawSteps) + } + if len(steps) == 0 { + t.Fatalf("Expected %s job to omit empty steps blocks instead of emitting steps: []", jobName) + } } } diff --git a/pkg/workflow/compiler_repo_config.go b/pkg/workflow/compiler_repo_config.go index f8375e8b6a7..1a2d53dc581 100644 --- a/pkg/workflow/compiler_repo_config.go +++ b/pkg/workflow/compiler_repo_config.go @@ -1,7 +1,9 @@ package workflow import ( + "fmt" "maps" + "os" "strings" ) @@ -17,6 +19,19 @@ func (c *Compiler) loadRepoConfig() (*RepoConfig, error) { c.repoConfigLoaded = true if c.repoConfigErr != nil { repoConfigLog.Printf("loadRepoConfig: failed to load repo config: %v", c.repoConfigErr) + fmt.Fprintln( + os.Stderr, + formatCompilerMessage( + RepoConfigFileName, + "warning", + fmt.Sprintf( + "failed to load aw.json; compilation will continue with defaults, and action_failure_issue_expires will fall back to %d hours where applicable: %v", + DefaultActionFailureIssueExpiresHours, + c.repoConfigErr, + ), + ), + ) + c.IncrementWarningCount() } else { repoConfigLog.Print("loadRepoConfig: repo config loaded successfully") } diff --git a/pkg/workflow/compiler_repo_config_test.go b/pkg/workflow/compiler_repo_config_test.go index 674f7b7eb0b..9b2ee71f687 100644 --- a/pkg/workflow/compiler_repo_config_test.go +++ b/pkg/workflow/compiler_repo_config_test.go @@ -5,6 +5,8 @@ package workflow import ( "os" "path/filepath" + "strconv" + "strings" "testing" ) @@ -91,6 +93,42 @@ func TestCompilerLoadRepoConfig_CachesError(t *testing.T) { } } +func TestCompilerLoadRepoConfig_InvalidAWJSONWarnsWithPathAndFallback(t *testing.T) { + gitRoot := t.TempDir() + workflowsDir := filepath.Join(gitRoot, ".github", "workflows") + if err := os.MkdirAll(workflowsDir, 0o755); err != nil { + t.Fatalf("Failed to create workflows directory: %v", err) + } + + configPath := filepath.Join(workflowsDir, "aw.json") + if err := os.WriteFile(configPath, []byte(`{"maintenance":{"action_failure_issue_expires":0}}`), 0o600); err != nil { + t.Fatalf("Failed to write invalid aw.json: %v", err) + } + + compiler := NewCompiler() + compiler.gitRoot = gitRoot + + stderr := captureStderr(func() { + cfg, err := compiler.loadRepoConfig() + if err == nil { + t.Fatal("Expected loadRepoConfig to fail for invalid config") + } + if cfg != nil { + t.Fatal("Expected nil config on loadRepoConfig error") + } + }) + + if !strings.Contains(stderr, RepoConfigFileName) { + t.Fatalf("Expected warning to mention %s, got: %s", RepoConfigFileName, stderr) + } + if !strings.Contains(stderr, strconv.Itoa(DefaultActionFailureIssueExpiresHours)) { + t.Fatalf("Expected warning to mention fallback %d, got: %s", DefaultActionFailureIssueExpiresHours, stderr) + } + if compiler.GetWarningCount() != 1 { + t.Fatalf("Expected warning count 1, got %d", compiler.GetWarningCount()) + } +} + func TestCompilerLoadRepoConfig_EmptyGitRoot(t *testing.T) { cwd, err := os.Getwd() if err != nil { From ff35943867d06c6318f0b32817f567d4702b43f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Jul 2026 18:32:43 +0000 Subject: [PATCH 5/6] test invalid aw.json parse warning path Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/workflow/compiler_pre_agent_steps_test.go | 8 ++- pkg/workflow/compiler_repo_config_test.go | 72 ++++++++++--------- 2 files changed, 45 insertions(+), 35 deletions(-) diff --git a/pkg/workflow/compiler_pre_agent_steps_test.go b/pkg/workflow/compiler_pre_agent_steps_test.go index 32847375645..0fb77915c43 100644 --- a/pkg/workflow/compiler_pre_agent_steps_test.go +++ b/pkg/workflow/compiler_pre_agent_steps_test.go @@ -12,6 +12,10 @@ import ( "gopkg.in/yaml.v3" ) +type compiledWorkflowJobs struct { + Jobs map[string]map[string]any `yaml:"jobs"` +} + func TestPreAgentStepsGeneration(t *testing.T) { tmpDir := testutil.TempDir(t, "pre-agent-steps-test") @@ -68,9 +72,7 @@ Test pre-agent-steps. if preAgentStepIndex >= aiStepIndex { t.Errorf("Pre-agent-step (%d) should appear before AI execution step (%d)", preAgentStepIndex, aiStepIndex) } - var compiled struct { - Jobs map[string]map[string]any `yaml:"jobs"` - } + var compiled compiledWorkflowJobs if err := yaml.Unmarshal(content, &compiled); err != nil { t.Fatalf("Failed to parse generated lock file as YAML: %v", err) } diff --git a/pkg/workflow/compiler_repo_config_test.go b/pkg/workflow/compiler_repo_config_test.go index 9b2ee71f687..805e80bef11 100644 --- a/pkg/workflow/compiler_repo_config_test.go +++ b/pkg/workflow/compiler_repo_config_test.go @@ -10,6 +10,42 @@ import ( "testing" ) +func assertInvalidAWJSONWarning(t *testing.T, content string) { + t.Helper() + + gitRoot := t.TempDir() + workflowsDir := filepath.Join(gitRoot, ".github", "workflows") + if err := os.MkdirAll(workflowsDir, 0o755); err != nil { + t.Fatalf("Failed to create workflows directory: %v", err) + } + if err := os.WriteFile(filepath.Join(workflowsDir, "aw.json"), []byte(content), 0o600); err != nil { + t.Fatalf("Failed to write invalid aw.json: %v", err) + } + + compiler := NewCompiler() + compiler.gitRoot = gitRoot + + stderr := captureStderr(func() { + cfg, err := compiler.loadRepoConfig() + if err == nil { + t.Fatal("Expected loadRepoConfig to fail for invalid config") + } + if cfg != nil { + t.Fatal("Expected nil config on loadRepoConfig error") + } + }) + + if !strings.Contains(stderr, RepoConfigFileName) { + t.Fatalf("Expected warning to mention %s, got: %s", RepoConfigFileName, stderr) + } + if !strings.Contains(stderr, strconv.Itoa(DefaultActionFailureIssueExpiresHours)) { + t.Fatalf("Expected warning to mention fallback %d, got: %s", DefaultActionFailureIssueExpiresHours, stderr) + } + if compiler.GetWarningCount() != 1 { + t.Fatalf("Expected warning count 1, got %d", compiler.GetWarningCount()) + } +} + func TestCompilerLoadRepoConfig_CachesResult(t *testing.T) { gitRoot := t.TempDir() workflowsDir := filepath.Join(gitRoot, ".github", "workflows") @@ -94,39 +130,11 @@ func TestCompilerLoadRepoConfig_CachesError(t *testing.T) { } func TestCompilerLoadRepoConfig_InvalidAWJSONWarnsWithPathAndFallback(t *testing.T) { - gitRoot := t.TempDir() - workflowsDir := filepath.Join(gitRoot, ".github", "workflows") - if err := os.MkdirAll(workflowsDir, 0o755); err != nil { - t.Fatalf("Failed to create workflows directory: %v", err) - } - - configPath := filepath.Join(workflowsDir, "aw.json") - if err := os.WriteFile(configPath, []byte(`{"maintenance":{"action_failure_issue_expires":0}}`), 0o600); err != nil { - t.Fatalf("Failed to write invalid aw.json: %v", err) - } - - compiler := NewCompiler() - compiler.gitRoot = gitRoot - - stderr := captureStderr(func() { - cfg, err := compiler.loadRepoConfig() - if err == nil { - t.Fatal("Expected loadRepoConfig to fail for invalid config") - } - if cfg != nil { - t.Fatal("Expected nil config on loadRepoConfig error") - } - }) + assertInvalidAWJSONWarning(t, `{"maintenance":{"action_failure_issue_expires":0}}`) +} - if !strings.Contains(stderr, RepoConfigFileName) { - t.Fatalf("Expected warning to mention %s, got: %s", RepoConfigFileName, stderr) - } - if !strings.Contains(stderr, strconv.Itoa(DefaultActionFailureIssueExpiresHours)) { - t.Fatalf("Expected warning to mention fallback %d, got: %s", DefaultActionFailureIssueExpiresHours, stderr) - } - if compiler.GetWarningCount() != 1 { - t.Fatalf("Expected warning count 1, got %d", compiler.GetWarningCount()) - } +func TestCompilerLoadRepoConfig_MalformedAWJSONWarnsWithPathAndFallback(t *testing.T) { + assertInvalidAWJSONWarning(t, `{"maintenance":`) } func TestCompilerLoadRepoConfig_EmptyGitRoot(t *testing.T) { From d9aaf07f9035089e55940e149de03465298b4962 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Jul 2026 18:45:10 +0000 Subject: [PATCH 6/6] polish review follow-up validation notes Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- docs/src/content/docs/reference/steps-jobs.md | 4 ++-- pkg/workflow/compiler_pre_agent_steps_test.go | 6 +++--- pkg/workflow/compiler_repo_config_test.go | 18 +++++++++++------- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/src/content/docs/reference/steps-jobs.md b/docs/src/content/docs/reference/steps-jobs.md index 0c8e0853a6d..f299efb0f6e 100644 --- a/docs/src/content/docs/reference/steps-jobs.md +++ b/docs/src/content/docs/reference/steps-jobs.md @@ -13,14 +13,14 @@ See [DeterministicOps](/gh-aw/patterns/deterministic-ops/) for patterns combinin Use these top-level step hooks in this order: -1. `pre-steps:` — runs before checkout and most of the built-in agent-job setup, though compiler-injected OTLP masking steps may run earlier. Use this for short-lived token minting or anything that must happen before repository checkout. +1. `pre-steps:` — runs before checkout and all later built-in agent-job setup; only compiler-injected OTLP masking steps may run earlier. Use this for short-lived token minting or anything that must happen before repository checkout. 2. `steps:` — runs after checkout and the normal runtime/bootstrap steps, but before the final pre-agent phase. Use this for deterministic preprocessing that needs the checked-out repository. 3. `pre-agent-steps:` — runs after framework-owned initialization such as checkout cleanup and base-branch restoration, and before MCP startup and engine execution. Use this for last-moment environment preparation immediately before the agent starts. 4. `post-steps:` — runs after the engine finishes. Use this for cleanup, summaries, uploads, or follow-up automation. ## Custom Pre-Checkout Steps (`pre-steps:`) -Add custom steps before checkout and the rest of the pre-checkout agent-job setup. Compiler-injected OTLP masking steps may still run earlier. +Add custom steps before checkout and the later pre-checkout agent-job setup. Compiler-injected OTLP masking steps may still run earlier. ```yaml wrap pre-steps: diff --git a/pkg/workflow/compiler_pre_agent_steps_test.go b/pkg/workflow/compiler_pre_agent_steps_test.go index 0fb77915c43..bc5f9f2c0d6 100644 --- a/pkg/workflow/compiler_pre_agent_steps_test.go +++ b/pkg/workflow/compiler_pre_agent_steps_test.go @@ -46,11 +46,11 @@ Test pre-agent-steps. } lockFile := filepath.Join(tmpDir, "test-pre-agent-steps.lock.yml") - content, err := os.ReadFile(lockFile) + lockBytes, err := os.ReadFile(lockFile) if err != nil { t.Fatalf("Failed to read generated lock file: %v", err) } - lockContent := string(content) + lockContent := string(lockBytes) if !strings.Contains(lockContent, "- name: Finalize prompt context") { t.Error("Expected pre-agent-step to be in generated workflow") @@ -73,7 +73,7 @@ Test pre-agent-steps. t.Errorf("Pre-agent-step (%d) should appear before AI execution step (%d)", preAgentStepIndex, aiStepIndex) } var compiled compiledWorkflowJobs - if err := yaml.Unmarshal(content, &compiled); err != nil { + if err := yaml.Unmarshal(lockBytes, &compiled); err != nil { t.Fatalf("Failed to parse generated lock file as YAML: %v", err) } for jobName, job := range compiled.Jobs { diff --git a/pkg/workflow/compiler_repo_config_test.go b/pkg/workflow/compiler_repo_config_test.go index 805e80bef11..6198064befa 100644 --- a/pkg/workflow/compiler_repo_config_test.go +++ b/pkg/workflow/compiler_repo_config_test.go @@ -25,16 +25,20 @@ func assertInvalidAWJSONWarning(t *testing.T, content string) { compiler := NewCompiler() compiler.gitRoot = gitRoot + var ( + cfg *RepoConfig + err error + ) stderr := captureStderr(func() { - cfg, err := compiler.loadRepoConfig() - if err == nil { - t.Fatal("Expected loadRepoConfig to fail for invalid config") - } - if cfg != nil { - t.Fatal("Expected nil config on loadRepoConfig error") - } + cfg, err = compiler.loadRepoConfig() }) + if err == nil { + t.Fatal("Expected loadRepoConfig to fail for invalid config") + } + if cfg != nil { + t.Fatal("Expected nil config on loadRepoConfig error") + } if !strings.Contains(stderr, RepoConfigFileName) { t.Fatalf("Expected warning to mention %s, got: %s", RepoConfigFileName, stderr) }