diff --git a/.github/workflows/daily-cli-performance.lock.yml b/.github/workflows/daily-cli-performance.lock.yml index 792cfd0d305..f7d1ce419c2 100644 --- a/.github/workflows/daily-cli-performance.lock.yml +++ b/.github/workflows/daily-cli-performance.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"c8064ac6104913f244b3c1f16639c0ae6115b80f1d5852eac188043a1ced1eb3","body_hash":"041fcfe0d8ef38271b062dc73c5f63e55f7045353a54e9fddc4303ce5ecbed64","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.68","copilot-sdk":"1.0.5"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"4ae81dde8c3fcd270ffa4818afede63294c7823ee5708b392793e081ddba341a","body_hash":"041fcfe0d8ef38271b062dc73c5f63e55f7045353a54e9fddc4303ce5ecbed64","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.68","copilot-sdk":"1.0.5"}} # gh-aw-manifest: {"version":1,"secrets":["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":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.26","digest":"sha256:b283b1f037d2e068532fe178a06f2944696c3933ba11604979134e7896ac6f8c","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.26@sha256:b283b1f037d2e068532fe178a06f2944696c3933ba11604979134e7896ac6f8c"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.26","digest":"sha256:1743dbac9bf4225f3acfdcbce4f77f5a3e61e22b2e929305525f00196693c015","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.26@sha256:1743dbac9bf4225f3acfdcbce4f77f5a3e61e22b2e929305525f00196693c015"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.26","digest":"sha256:6006b1c579ca550e023697b5dfd832ae03361328a4c0f2eb49fb181a6b8d7a4b","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.26@sha256:6006b1c579ca550e023697b5dfd832ae03361328a4c0f2eb49fb181a6b8d7a4b"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.26","digest":"sha256:d4647c3bfaf80889eb1dbd3d4e2063340cbf94c0ca6c5747bbfc8507b12f3485","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.26@sha256:d4647c3bfaf80889eb1dbd3d4e2063340cbf94c0ca6c5747bbfc8507b12f3485"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.33","digest":"sha256:f0e1f1d562f01e737f3ef125cb4d7057ff87a68a97927f5ea9fd6d3f5091da06","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.33@sha256:f0e1f1d562f01e737f3ef125cb4d7057ff87a68a97927f5ea9fd6d3f5091da06"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -64,6 +64,7 @@ name: "Daily CLI Performance Agent" on: # permissions: # Permissions applied to pre-activation job # contents: read + # restore-memory: true # Restore-memory enables pre-activation memory restore schedule: - cron: "49 14 * * *" # Friendly format: daily (scattered) @@ -1861,6 +1862,16 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/check_membership.cjs'); await main(); + # Repo memory git-based storage configuration from frontmatter processed below + - name: Clone repo-memory branch (default) + env: + GH_TOKEN: ${{ github.token }} + GITHUB_SERVER_URL: ${{ github.server_url }} + BRANCH_NAME: memory/cli-performance + TARGET_REPO: ${{ github.repository }} + MEMORY_DIR: /tmp/gh-aw/repo-memory/default + CREATE_ORPHAN: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/clone_repo_memory_branch.sh" - name: Detect recent compilation-related changes id: changes uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 diff --git a/.github/workflows/daily-cli-performance.md b/.github/workflows/daily-cli-performance.md index 28c75b829c1..a884a7f8d4a 100644 --- a/.github/workflows/daily-cli-performance.md +++ b/.github/workflows/daily-cli-performance.md @@ -3,6 +3,7 @@ private: true emoji: "⚡" description: Daily CLI Performance - Runs benchmarks, tracks performance trends, and reports regressions on: + restore-memory: true schedule: daily workflow_dispatch: permissions: diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index bf08d059eac..2ec9bd1b6e1 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"f4c25dc4e3ebaa00201eec60464e37b5d059d7c2c14344ad95baab84851c84c8","body_hash":"fae212c4ff39a682e3254fae440084a2152282e383467ff3f0c15fb9d2d69411","strict":true,"agent_id":"codex","engine_versions":{"codex":"0.142.5"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"aa0ed119e72a5a80fca3653db96ef93a846a9fc08a00e2bdaf4d7a9a968e5639","body_hash":"fae212c4ff39a682e3254fae440084a2152282e383467ff3f0c15fb9d2d69411","strict":true,"agent_id":"codex","engine_versions":{"codex":"0.142.5"}} # gh-aw-manifest: {"version":1,"secrets":["CODEX_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","OPENAI_API_KEY"],"actions":[{"repo":"actions-ecosystem/action-add-labels","sha":"c96b68fec76a0987cd93957189e9abd0b9a72ff1","version":"v1.1.3"},{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"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":"924ae3a1cded613372ab5595356fb5720e22ba16","version":"v6.5.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.26","digest":"sha256:b283b1f037d2e068532fe178a06f2944696c3933ba11604979134e7896ac6f8c","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.26@sha256:b283b1f037d2e068532fe178a06f2944696c3933ba11604979134e7896ac6f8c"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.26","digest":"sha256:1743dbac9bf4225f3acfdcbce4f77f5a3e61e22b2e929305525f00196693c015","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.26@sha256:1743dbac9bf4225f3acfdcbce4f77f5a3e61e22b2e929305525f00196693c015"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.26","digest":"sha256:6006b1c579ca550e023697b5dfd832ae03361328a4c0f2eb49fb181a6b8d7a4b","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.26@sha256:6006b1c579ca550e023697b5dfd832ae03361328a4c0f2eb49fb181a6b8d7a4b"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.26","digest":"sha256:d4647c3bfaf80889eb1dbd3d4e2063340cbf94c0ca6c5747bbfc8507b12f3485","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.26@sha256:d4647c3bfaf80889eb1dbd3d4e2063340cbf94c0ca6c5747bbfc8507b12f3485"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.33","digest":"sha256:f0e1f1d562f01e737f3ef125cb4d7057ff87a68a97927f5ea9fd6d3f5091da06","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.33@sha256:f0e1f1d562f01e737f3ef125cb4d7057ff87a68a97927f5ea9fd6d3f5091da06"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"},{"image":"ghcr.io/github/serena-mcp-server:latest","digest":"sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5","pinned_image":"ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -76,6 +76,7 @@ on: # - smoke # Label filtering applied via job conditions types: - labeled + # restore-memory: true # Restore-memory enables pre-activation memory restore workflow_dispatch: inputs: aw_context: diff --git a/.github/workflows/smoke-codex.md b/.github/workflows/smoke-codex.md index e1f094c2846..8a6b028ebe9 100644 --- a/.github/workflows/smoke-codex.md +++ b/.github/workflows/smoke-codex.md @@ -3,6 +3,7 @@ private: true emoji: "🧪" description: Smoke test workflow that validates Codex engine functionality by reviewing recent PRs twice daily on: + restore-memory: true slash_command: name: smoke-codex strategy: centralized diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 1eef3383a43..bfd0973066f 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"cd1db4da83dbeb24e508e618607ddd371bdce795962c9e5d58257d2a911dfc81","body_hash":"ff1e979a13078c84305141cc3861a5a2a120a2c8497fa48e4fc18115ca12a1c3","strict":true,"agent_id":"copilot","agent_model":"gpt-5.4","engine_versions":{"copilot":"1.0.68"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"3bd86338a89e0309c9e8a830f8468327f4360c143b3200bc881f1a41b2dcfe9d","body_hash":"ff1e979a13078c84305141cc3861a5a2a120a2c8497fa48e4fc18115ca12a1c3","strict":true,"agent_id":"copilot","agent_model":"gpt-5.4","engine_versions":{"copilot":"1.0.68"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","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":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"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":"924ae3a1cded613372ab5595356fb5720e22ba16","version":"v6.5.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.26","digest":"sha256:b283b1f037d2e068532fe178a06f2944696c3933ba11604979134e7896ac6f8c","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.26@sha256:b283b1f037d2e068532fe178a06f2944696c3933ba11604979134e7896ac6f8c"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.26","digest":"sha256:1743dbac9bf4225f3acfdcbce4f77f5a3e61e22b2e929305525f00196693c015","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.26@sha256:1743dbac9bf4225f3acfdcbce4f77f5a3e61e22b2e929305525f00196693c015"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.26","digest":"sha256:6006b1c579ca550e023697b5dfd832ae03361328a4c0f2eb49fb181a6b8d7a4b","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.26@sha256:6006b1c579ca550e023697b5dfd832ae03361328a4c0f2eb49fb181a6b8d7a4b"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.26","digest":"sha256:d4647c3bfaf80889eb1dbd3d4e2063340cbf94c0ca6c5747bbfc8507b12f3485","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.26@sha256:d4647c3bfaf80889eb1dbd3d4e2063340cbf94c0ca6c5747bbfc8507b12f3485"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.33","digest":"sha256:f0e1f1d562f01e737f3ef125cb4d7057ff87a68a97927f5ea9fd6d3f5091da06","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.33@sha256:f0e1f1d562f01e737f3ef125cb4d7057ff87a68a97927f5ea9fd6d3f5091da06"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"},{"image":"ghcr.io/github/serena-mcp-server:latest","digest":"sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5","pinned_image":"ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -70,6 +70,7 @@ name: "Smoke Copilot" on: + # restore-memory: true # Restore-memory enables pre-activation memory restore workflow_dispatch: inputs: aw_context: diff --git a/.github/workflows/smoke-copilot.md b/.github/workflows/smoke-copilot.md index 32aa73e6766..72a52f0f37a 100644 --- a/.github/workflows/smoke-copilot.md +++ b/.github/workflows/smoke-copilot.md @@ -2,7 +2,8 @@ private: true emoji: "🧪" description: Smoke Copilot -on: +on: + restore-memory: true slash_command: name: smoke-copilot strategy: centralized diff --git a/docs/adr/44015-expose-memory-stores-to-on-steps-pre-activation.md b/docs/adr/44015-expose-memory-stores-to-on-steps-pre-activation.md new file mode 100644 index 00000000000..9b45fde9d11 --- /dev/null +++ b/docs/adr/44015-expose-memory-stores-to-on-steps-pre-activation.md @@ -0,0 +1,48 @@ +# ADR-44015: Expose Memory Stores to `on.steps` in Pre-Activation + +**Date**: 2026-07-07 +**Status**: Draft +**Deciders**: Unknown + +--- + +### Context + +The gh-aw workflow engine compiles workflows into a `pre_activation` job and an agent job. Custom dispatch-gating steps defined via `on.steps` execute in `pre_activation`, but all three memory stores (cache-memory, repo-memory, comment-memory) were previously only restored in the agent job. This meant that any `on.steps` logic requiring prior memory state — for example, a gate step that reads a cached value to decide whether to proceed — either had to forgo memory access or consume an additional LLM turn in the agent phase to hydrate state. The inability to access memory in pre-activation limited the expressiveness of deterministic, non-LLM dispatch gates. + +### Decision + +We will restore all configured memory stores (cache-memory, repo-memory, comment-memory) in the pre-activation job before `on.steps` gates execute. The restore is **read-only**: it reuses the existing restore/load paths for each memory type and does not add cache-commit or repo-push write-back steps to pre-activation. Hydration is gated on the presence of `on.steps` in the workflow definition so that workflows without custom steps are unaffected. + +### Alternatives Considered + +#### Alternative 1: Execute `on.steps` in the Agent Job Instead of Pre-Activation + +Move the `on.steps` execution point from `pre_activation` to the agent job, where memory is already available. + +This would avoid the need to restore memory in a second location, but it would break the architectural contract that `on.steps` runs before the LLM is invoked. The entire purpose of `on.steps` is to gate activation deterministically and cheaply; running it in the agent job would consume a full LLM turn even when the gate rejects the request, negating the cost benefit. + +#### Alternative 2: Require Workflows to Declare Their Own Memory Restoration Steps + +Leave pre-activation unchanged and let workflow authors manually add memory restore steps before their `on.steps` gates via explicit YAML. + +This avoids centralizing the restore logic in the compiler, but it places the burden on every workflow author to know the correct restore incantation for each memory type. It also creates drift risk: if the restore steps change, all affected workflows must be updated. The compiler already owns the pattern for generating memory steps; extending it is the consistent approach. + +### Consequences + +#### Positive +- `on.steps` gates can now read prior memory state to make deterministic dispatch decisions without spending an LLM turn. +- Reusing the existing restore/load paths ensures memory-type-specific details (branch names, cache keys, token handling) remain in a single authoritative location. +- The read-only constraint prevents pre-activation from accidentally mutating memory, preserving the invariant that only the agent job writes back. + +#### Negative +- Every pre-activation run for a workflow with `on.steps` now executes additional restore steps, adding latency even when the gate does not use memory. +- The pre-activation job compiler gains a new code path (`buildPreActivationMemoryRestoreSteps`), increasing the surface area that must be maintained as memory store implementations evolve. + +#### Neutral +- The change is scoped to workflows that define `on.steps`; workflows without `on.steps` are compiled identically to before. +- The `.github/workflows/daily-cli-performance.lock.yml` lock file is regenerated as a side effect of the compiler change, reflecting the new restore step. + +--- + +*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* diff --git a/docs/src/content/docs/reference/frontmatter.md b/docs/src/content/docs/reference/frontmatter.md index 13a2588d934..79a2c5e11d1 100644 --- a/docs/src/content/docs/reference/frontmatter.md +++ b/docs/src/content/docs/reference/frontmatter.md @@ -79,6 +79,7 @@ The `on:` section uses standard GitHub Actions syntax to define workflow trigger - `skip-if-match:` - Skip execution when a search query has matches (supports `scope: none`; use top-level `on.github-token` / `on.github-app` for custom auth) - `skip-if-no-match:` - Skip execution when a search query has no matches (supports `scope: none`; use top-level `on.github-token` / `on.github-app` for custom auth) - `steps:` - Inject custom deterministic steps into the pre-activation job (saves one workflow job vs. multi-job pattern) +- `restore-memory:` - Opt in to restoring memory stores before `on.steps` in pre-activation (default: `false`) - `permissions:` - Grant additional GitHub token scopes to the pre-activation job (for use with `on.steps:` API calls) - `needs:` - Add custom job dependencies that both `pre_activation` and `activation` must wait for - `github-token:` - Custom token for activation job reactions, status comments, and skip-if search queries diff --git a/docs/src/content/docs/reference/triggers.md b/docs/src/content/docs/reference/triggers.md index 4f4fba77b61..0434c97632c 100644 --- a/docs/src/content/docs/reference/triggers.md +++ b/docs/src/content/docs/reference/triggers.md @@ -682,6 +682,7 @@ Inject custom deterministic steps directly into the pre-activation job. Steps ru on: issues: types: [opened] + restore-memory: true steps: - name: Check issue label id: label_check @@ -708,6 +709,8 @@ if: needs.pre_activation.outputs.has_bug_label == 'true' Explicit outputs in `jobs.pre-activation.outputs` take precedence over auto-wired `_result` outputs on key collision. +Set `on.restore-memory: true` to restore memory stores (`cache-memory`, `repo-memory`, `comment-memory`) before `on.steps` run. The default is `false`. + ### Pre-Activation and Activation Dependencies (`on.needs:`) Add custom jobs that both `pre_activation` and `activation` should depend on. Use this when `on.github-app` credentials come from a job output (for example, a secret-manager fetch job). diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 453c5dd453c..cb11f05bdb2 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -2152,6 +2152,10 @@ "default": [], "examples": [["secrets_fetcher"]] }, + "restore-memory": { + "type": "boolean", + "description": "When true, restore cache-memory, repo-memory, and comment-memory stores before on.steps in pre_activation. Defaults to false." + }, "steps": { "type": "array", "description": "Steps to inject into the pre-activation job. These steps run after all built-in checks (membership, stop-time, skip-if, etc.) and their results are exposed as pre-activation outputs. Use 'id' on steps to reference their results via needs.pre_activation.outputs._result.", diff --git a/pkg/workflow/compiler_orchestrator_workflow.go b/pkg/workflow/compiler_orchestrator_workflow.go index e222a93ea2a..37bbfcf3260 100644 --- a/pkg/workflow/compiler_orchestrator_workflow.go +++ b/pkg/workflow/compiler_orchestrator_workflow.go @@ -710,5 +710,12 @@ func (c *Compiler) processOnSectionAndFilters( } workflowData.OnNeeds = onNeeds + // Extract on.restore-memory to opt in to pre-activation memory restore for on.steps. + onRestoreMemory, err := extractOnRestoreMemory(frontmatter) + if err != nil { + return err + } + workflowData.OnRestoreMemory = onRestoreMemory + return nil } diff --git a/pkg/workflow/compiler_pre_activation_job.go b/pkg/workflow/compiler_pre_activation_job.go index e40c0d14e05..4ce8886d125 100644 --- a/pkg/workflow/compiler_pre_activation_job.go +++ b/pkg/workflow/compiler_pre_activation_job.go @@ -47,6 +47,7 @@ func (c *Compiler) buildPreActivationJob(data *WorkflowData, needsPermissionChec steps = c.buildPreActivationSkipIfQuerySteps(data, steps, skipIfToken) steps = c.buildPreActivationSkipIfCheckFailingStep(data, steps) steps = c.buildPreActivationRolesBotsCmdSteps(data, steps) + steps = c.buildPreActivationMemoryRestoreSteps(data, steps) steps, onStepIDs, err := c.injectPreActivationOnSteps(data, steps, customSteps) if err != nil { return nil, err @@ -230,6 +231,75 @@ func (c *Compiler) buildPreActivationRolesBotsCmdSteps(data *WorkflowData, steps return steps } +// buildPreActivationMemoryRestoreSteps restores memory stores before on.steps run in pre-activation. +// This is a read-only surface: it restores/loads memory data but does not emit write-back or commit steps. +func (c *Compiler) buildPreActivationMemoryRestoreSteps(data *WorkflowData, steps []string) []string { + if len(data.OnSteps) == 0 || !data.OnRestoreMemory { + return steps + } + + var cacheMemorySteps strings.Builder + generatePreActivationCacheMemoryRestoreSteps(&cacheMemorySteps, data) + if cacheMemorySteps.Len() > 0 { + steps = append(steps, cacheMemorySteps.String()) + } + + var repoMemorySteps strings.Builder + generateRepoMemorySteps(&repoMemorySteps, data) + if repoMemorySteps.Len() > 0 { + steps = append(steps, repoMemorySteps.String()) + } + + if data.SafeOutputs != nil && data.SafeOutputs.CommentMemory != nil { + var commentMemorySteps strings.Builder + commentMemorySteps.WriteString(" - name: Prepare comment memory files\n") + fmt.Fprintf(&commentMemorySteps, " uses: %s\n", getCachedActionPin("actions/github-script", data)) + commentMemorySteps.WriteString(" with:\n") + fmt.Fprintf(&commentMemorySteps, " github-token: %s\n", getEffectiveSafeOutputGitHubToken(data.SafeOutputs.CommentMemory.GitHubToken)) + commentMemorySteps.WriteString(" script: |\n") + commentMemorySteps.WriteString(" const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');\n") + commentMemorySteps.WriteString(" setupGlobals(core, github, context, exec, io, getOctokit);\n") + commentMemorySteps.WriteString(" const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_comment_memory_files.cjs');\n") + commentMemorySteps.WriteString(" await main();\n") + steps = append(steps, commentMemorySteps.String()) + } + + return steps +} + +func generatePreActivationCacheMemoryRestoreSteps(builder *strings.Builder, data *WorkflowData) { + if data.CacheMemoryConfig == nil || len(data.CacheMemoryConfig.Caches) == 0 { + return + } + + preActivationData := &WorkflowData{ + ParsedTools: data.ParsedTools, + SafeOutputs: data.SafeOutputs, + CacheMemoryConfig: &CacheMemoryConfig{ + Caches: make([]CacheMemoryEntry, len(data.CacheMemoryConfig.Caches)), + }, + } + for i, cache := range data.CacheMemoryConfig.Caches { + cacheCopy := CacheMemoryEntry{ + ID: cache.ID, + Key: cache.Key, + Description: cache.Description, + RestoreOnly: true, + Scope: cache.Scope, + } + if cache.AllowedExtensions != nil { + cacheCopy.AllowedExtensions = append([]string(nil), cache.AllowedExtensions...) + } + if cache.RetentionDays != nil { + retentionDays := *cache.RetentionDays + cacheCopy.RetentionDays = &retentionDays + } + preActivationData.CacheMemoryConfig.Caches[i] = cacheCopy + } + + generateCacheMemorySteps(builder, preActivationData) +} + func (c *Compiler) appendPreActivationSkipRolesStep(data *WorkflowData, steps []string) []string { steps = append(steps, " - name: Check skip-roles\n") steps = append(steps, fmt.Sprintf(" id: %s\n", constants.CheckSkipRolesStepID)) @@ -783,6 +853,32 @@ func extractOnNeeds(frontmatter map[string]any) ([]string, error) { return parseOnNeedsValues(onMap) } +// extractOnRestoreMemory extracts the optional 'restore-memory' field from the 'on:' section. +// Default is false when unset. +func extractOnRestoreMemory(frontmatter map[string]any) (bool, error) { + onValue, exists := frontmatter["on"] + if !exists || onValue == nil { + return false, nil + } + + onMap, ok := onValue.(map[string]any) + if !ok { + return false, nil + } + + restoreMemoryValue, exists := onMap["restore-memory"] + if !exists || restoreMemoryValue == nil { + return false, nil + } + + restoreMemory, ok := restoreMemoryValue.(bool) + if !ok { + return false, fmt.Errorf("on.restore-memory must be a boolean, got %T", restoreMemoryValue) + } + + return restoreMemory, nil +} + func parseOnNeedsValues(onMap map[string]any) ([]string, error) { if onMap == nil { return nil, nil diff --git a/pkg/workflow/frontmatter_extraction_yaml.go b/pkg/workflow/frontmatter_extraction_yaml.go index 1e9575ad9bc..da2b6494470 100644 --- a/pkg/workflow/frontmatter_extraction_yaml.go +++ b/pkg/workflow/frontmatter_extraction_yaml.go @@ -125,7 +125,7 @@ func (c *Compiler) extractTopLevelYAMLSection(frontmatter map[string]any, key st return yamlStr } -// commentOutProcessedFieldsInOnSection comments out draft, fork, forks, names, labels, manual-approval, stop-after, skip-if-match, skip-if-no-match, skip-roles, reaction, lock-for-agent, steps, permissions, needs, and stale-check fields in the on section +// commentOutProcessedFieldsInOnSection comments out draft, fork, forks, names, labels, manual-approval, stop-after, skip-if-match, skip-if-no-match, skip-roles, reaction, lock-for-agent, steps, permissions, needs, restore-memory, and stale-check fields in the on section // These fields are processed separately and should be commented for documentation // Exception: names fields in sections with __gh_aw_native_label_filter__ marker in frontmatter are NOT commented out func (c *Compiler) commentOutProcessedFieldsInOnSection(yamlStr string, frontmatter map[string]any) string { @@ -648,6 +648,9 @@ func (c *Compiler) commentOutProcessedFieldsInOnSection(yamlStr string, frontmat // Comment out array items in needs shouldComment = true commentReason = " # Needs processed as dependency in pre-activation job" + } else if strings.HasPrefix(trimmedLine, "restore-memory:") { + shouldComment = true + commentReason = " # Restore-memory enables pre-activation memory restore" } else if strings.HasPrefix(trimmedLine, "steps:") { shouldComment = true commentReason = " # Steps injected into pre-activation job" diff --git a/pkg/workflow/on_steps_test.go b/pkg/workflow/on_steps_test.go index 482eb2b9992..bfe5a11eb8c 100644 --- a/pkg/workflow/on_steps_test.go +++ b/pkg/workflow/on_steps_test.go @@ -5,6 +5,7 @@ package workflow import ( "os" "path/filepath" + "regexp" "strings" "testing" @@ -225,6 +226,157 @@ Test on.steps are appended after built-in checks t.Error("Expected membership check step to appear before on.steps gate step in pre_activation job") } }) + + t.Run("on_steps_with_memory_stores_restored_in_pre_activation", func(t *testing.T) { + workflowContent := `--- +on: + workflow_dispatch: null + roles: all + restore-memory: true + steps: + - name: Gate check + id: gate + run: echo "checking..." +tools: + cache-memory: true + repo-memory: true + comment-memory: true +engine: copilot +--- + +Test on.steps with memory stores restored in pre-activation +` + workflowFile := filepath.Join(tmpDir, "test-on-steps-memory-pre-activation.md") + if err := os.WriteFile(workflowFile, []byte(workflowContent), 0644); err != nil { + t.Fatal(err) + } + + err := compiler.CompileWorkflow(workflowFile) + if err != nil { + t.Fatalf("CompileWorkflow() returned error: %v", err) + } + + lockFile := stringutil.MarkdownToLockFile(workflowFile) + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + lockContentStr := string(lockContent) + preActivationStart := strings.Index(lockContentStr, "\n pre_activation:\n") + if preActivationStart == -1 { + preActivationStart = strings.Index(lockContentStr, " pre_activation:\n") + } + if preActivationStart == -1 { + t.Fatalf("Expected pre_activation section in lock file. Lock file:\n%s", lockContentStr) + } + + sectionBodyStart := preActivationStart + if strings.HasPrefix(lockContentStr[preActivationStart:], "\n") { + sectionBodyStart++ + } + jobStartPattern := regexp.MustCompile(`\n [A-Za-z0-9_-]+:\n`) + searchStart := sectionBodyStart + len(" pre_activation:\n") + nextJobLoc := jobStartPattern.FindStringIndex(lockContentStr[searchStart:]) + var preActivationSection string + if nextJobLoc == nil { + preActivationSection = lockContentStr[sectionBodyStart:] + } else { + preActivationSection = lockContentStr[sectionBodyStart : searchStart+nextJobLoc[0]+1] + } + if preActivationSection == "" { + t.Fatalf("Expected pre_activation section in lock file. Lock file:\n%s", lockContentStr) + } + + // Memory stores should be restored before on.steps run in pre-activation. + assert.Contains(t, lockContentStr, "# restore-memory: true # Restore-memory enables pre-activation memory restore") + assert.Contains(t, preActivationSection, "Restore cache-memory file share data") + assert.Contains(t, preActivationSection, "Clone repo-memory branch (default)") + assert.Contains(t, preActivationSection, "Prepare comment memory files") + assert.Contains(t, preActivationSection, " id: gate") + + cacheRestoreIdx := strings.Index(preActivationSection, "Restore cache-memory file share data") + repoRestoreIdx := strings.Index(preActivationSection, "Clone repo-memory branch (default)") + commentMemoryIdx := strings.Index(preActivationSection, "Prepare comment memory files") + gateStepIdx := strings.Index(preActivationSection, " id: gate") + if cacheRestoreIdx == -1 || repoRestoreIdx == -1 || commentMemoryIdx == -1 || gateStepIdx == -1 { + t.Fatalf("Expected memory restore steps and gate step in pre_activation section. Section:\n%s", preActivationSection) + } + assert.Less(t, cacheRestoreIdx, gateStepIdx, "cache-memory should be restored before on.steps") + assert.Less(t, repoRestoreIdx, gateStepIdx, "repo-memory should be restored before on.steps") + assert.Less(t, commentMemoryIdx, gateStepIdx, "comment-memory should be restored before on.steps") + + // Pre-activation must not include write-back/commit steps. + assert.NotContains(t, preActivationSection, "Commit cache-memory changes") + assert.NotContains(t, preActivationSection, "Push repo-memory changes") + assert.Contains(t, preActivationSection, "uses: "+getActionPin("actions/cache/restore")) + assert.NotContains(t, preActivationSection, "uses: actions/cache@") + assert.NotContains(t, preActivationSection, "uses: "+getActionPin("actions/cache")) + }) + + t.Run("on_steps_without_restore_memory_does_not_restore_stores_in_pre_activation", func(t *testing.T) { + workflowContent := `--- +on: + workflow_dispatch: null + roles: all + steps: + - name: Gate check + id: gate + run: echo "checking..." +tools: + cache-memory: true + repo-memory: true + comment-memory: true +engine: copilot +--- + +Test on.steps without on.restore-memory +` + workflowFile := filepath.Join(tmpDir, "test-on-steps-memory-pre-activation-disabled.md") + if err := os.WriteFile(workflowFile, []byte(workflowContent), 0644); err != nil { + t.Fatal(err) + } + + err := compiler.CompileWorkflow(workflowFile) + if err != nil { + t.Fatalf("CompileWorkflow() returned error: %v", err) + } + + lockFile := stringutil.MarkdownToLockFile(workflowFile) + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + lockContentStr := string(lockContent) + preActivationStart := strings.Index(lockContentStr, "\n pre_activation:\n") + if preActivationStart == -1 { + preActivationStart = strings.Index(lockContentStr, " pre_activation:\n") + } + if preActivationStart == -1 { + t.Fatalf("Expected pre_activation section in lock file. Lock file:\n%s", lockContentStr) + } + + sectionBodyStart := preActivationStart + if strings.HasPrefix(lockContentStr[preActivationStart:], "\n") { + sectionBodyStart++ + } + jobStartPattern := regexp.MustCompile(`\n [A-Za-z0-9_-]+:\n`) + searchStart := sectionBodyStart + len(" pre_activation:\n") + nextJobLoc := jobStartPattern.FindStringIndex(lockContentStr[searchStart:]) + var preActivationSection string + if nextJobLoc == nil { + preActivationSection = lockContentStr[sectionBodyStart:] + } else { + preActivationSection = lockContentStr[sectionBodyStart : searchStart+nextJobLoc[0]+1] + } + if preActivationSection == "" { + t.Fatalf("Expected pre_activation section in lock file. Lock file:\n%s", lockContentStr) + } + + assert.Contains(t, preActivationSection, " id: gate") + assert.NotContains(t, preActivationSection, "Restore cache-memory file share data") + assert.NotContains(t, preActivationSection, "Clone repo-memory branch (default)") + assert.NotContains(t, preActivationSection, "Prepare comment memory files") + }) } // TestExtractOnSteps tests the extractOnSteps function directly @@ -485,6 +637,80 @@ func TestExtractOnNeeds(t *testing.T) { } } +func TestExtractOnRestoreMemory(t *testing.T) { + tests := []struct { + name string + frontmatter map[string]any + expected bool + expectError bool + errorContains string + }{ + { + name: "no_on_section", + frontmatter: map[string]any{}, + expected: false, + }, + { + name: "on_section_string", + frontmatter: map[string]any{ + "on": "push", + }, + expected: false, + }, + { + name: "on_without_restore_memory", + frontmatter: map[string]any{ + "on": map[string]any{ + "workflow_dispatch": nil, + }, + }, + expected: false, + }, + { + name: "restore_memory_true", + frontmatter: map[string]any{ + "on": map[string]any{ + "restore-memory": true, + }, + }, + expected: true, + }, + { + name: "restore_memory_false", + frontmatter: map[string]any{ + "on": map[string]any{ + "restore-memory": false, + }, + }, + expected: false, + }, + { + name: "restore_memory_wrong_type", + frontmatter: map[string]any{ + "on": map[string]any{ + "restore-memory": "true", + }, + }, + expectError: true, + errorContains: "on.restore-memory must be a boolean", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + restoreMemory, err := extractOnRestoreMemory(tt.frontmatter) + if tt.expectError { + require.Error(t, err, "expected extraction error") + assert.Contains(t, err.Error(), tt.errorContains, "error should contain expected text") + return + } + + require.NoError(t, err, "expected extraction to succeed") + assert.Equal(t, tt.expected, restoreMemory, "unexpected on.restore-memory result") + }) + } +} + // TestOnPermissionsAppliedToPreActivation tests that on.permissions are applied to the pre-activation job func TestOnPermissionsAppliedToPreActivation(t *testing.T) { tmpDir := testutil.TempDir(t, "on-permissions-test") diff --git a/pkg/workflow/workflow_data.go b/pkg/workflow/workflow_data.go index 488f9c96cb0..36a80786610 100644 --- a/pkg/workflow/workflow_data.go +++ b/pkg/workflow/workflow_data.go @@ -96,6 +96,7 @@ type WorkflowData struct { SkipAuthorAssociations map[string][]string // author associations to skip by event name (on.skip-author-associations) AllowBotAuthoredTriggerComment bool // allow bot-posted-menu / user-checks-box pattern (on.allow-bot-authored-trigger-comment) OnSteps []map[string]any // steps to inject into the pre-activation job from on.steps + OnRestoreMemory bool // enable memory restore in pre-activation for on.steps via on.restore-memory (default false) OnPermissions *Permissions // additional permissions for the pre-activation job from on.permissions OnNeeds []string // custom workflow jobs that pre_activation/activation should depend on from on.needs ManualApproval string // environment name for manual approval from on: section