diff --git a/.github/workflows/release.lock.yml b/.github/workflows/release.lock.yml index 9840a38941a..b670dfbb32e 100644 --- a/.github/workflows/release.lock.yml +++ b/.github/workflows/release.lock.yml @@ -1,5 +1,6 @@ # gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"2d6e82aec6fea9061e1ef1cbb085324e6ed6c079230d70bbbb96611bb18cf7cf","body_hash":"646353d7bb4e5523bc85349c2cce38188190095a303f83cc95961ff145a47043","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.70"}} -# 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":"b7ad1dad31e06c5925ef5d2fc7ad053ef454303e","version":"b7ad1dad31e06c5925ef5d2fc7ad053ef454303e"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"anchore/sbom-action","sha":"e22c389904149dbc22b58101806040fa8d37a610","version":"v0.24.0"},{"repo":"docker/build-push-action","sha":"53b7df96c91f9c12dcc8a07bcb9ccacbed38856a","version":"v7.3.0"},{"repo":"docker/login-action","sha":"af1e73f918a031802d376d3c8bbc3fe56130a9b0","version":"v4.4.0"},{"repo":"docker/metadata-action","sha":"dc802804100637a589fabce1cb79ff13a1411302","version":"v6.2.0"},{"repo":"docker/setup-buildx-action","sha":"bb05f3f5519dd87d3ba754cc423b652a5edd6d2c","version":"v4.2.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"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.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]}# 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 +# 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":"b7ad1dad31e06c5925ef5d2fc7ad053ef454303e","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"anchore/sbom-action","sha":"e22c389904149dbc22b58101806040fa8d37a610","version":"v0.24.0"},{"repo":"docker/build-push-action","sha":"53b7df96c91f9c12dcc8a07bcb9ccacbed38856a","version":"v7.3.0"},{"repo":"docker/login-action","sha":"af1e73f918a031802d376d3c8bbc3fe56130a9b0","version":"v4.4.0"},{"repo":"docker/metadata-action","sha":"dc802804100637a589fabce1cb79ff13a1411302","version":"v6.2.0"},{"repo":"docker/setup-buildx-action","sha":"bb05f3f5519dd87d3ba754cc423b652a5edd6d2c","version":"v4.2.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"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.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} +# 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 # # ___ _ _ # / _ \ | | (_) diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index bbd7f950502..5d3e121099f 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -436,9 +436,6 @@ jobs: ``` - - - # Smoke Test: Claude Engine Validation. **IMPORTANT: Keep all outputs extremely short and concise. Use single-line responses where possible. No verbose explanations.** @@ -454,11 +451,11 @@ jobs: 5. **Tavily Web Search Testing**: Use the Tavily MCP server to perform a web search for "GitHub Agentic Workflows" and verify that results are returned with at least one item 6. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-claude-__GH_AW_GITHUB_RUN_ID__.txt` with content "Smoke test passed for Claude at $(date)" (create the directory if it doesn't exist) 7. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) - 8. **Discussion Interaction Testing**: + 8. **Discussion Interaction Testing**: - Use `gh api repos/__GH_AW_GITHUB_REPOSITORY__/discussions?per_page=1` to get the latest discussion from __GH_AW_GITHUB_REPOSITORY__ - Extract the discussion number from the result (e.g., if the result is `{"number": 123, "title": "...", ...}`, extract 123) - Use the `add_comment` tool with `discussion_number: ` to add a fun, comic-book style comment stating that the smoke test agent was here - 9. **Agentic Workflows MCP Testing**: + 9. **Agentic Workflows MCP Testing**: - Call the `agentic-workflows` MCP tool using the `status` method with workflow name `smoke-claude` to query workflow status - If the tool returns an error or no results, mark this test as ❌ and note "Tool unavailable or workflow not found" but continue to the Output section - If the tool succeeds, extract key information from the response: total runs, success/failure counts, last run timestamp @@ -496,7 +493,7 @@ jobs: - Note: This will bundle all review comments from test #14 - After submitting, use the GitHub MCP tool to list review threads on the PR and note the thread IDs from review comments you created in test #14 — these will be used in test #16 - 16. **Resolve Review Thread Testing**: + 16. **Resolve Review Thread Testing**: - Use the GitHub MCP tool to list review threads on the PR and filter for threads that are **not yet resolved** (`isResolved: false`) - Prefer resolving a thread created by your own review comments from test #14 in this run - Use `thread_id: ""` from one of those unresolved threads @@ -508,7 +505,7 @@ jobs: - Verify the tool call succeeds - Note: May fail if reviewer is already assigned or doesn't have access - 18. **Push to PR Branch Testing**: + 18. **Push to PR Branch Testing**: - Create a test file at `smoke-test-files/smoke-claude-push-test.md` in the repository workspace with content "Smoke test push — Run __GH_AW_GITHUB_RUN_ID__" - Use the `push_to_pull_request_branch` tool to push this change - Use `pr_number: ` and `commit_message: "test: Add smoke test file"` diff --git a/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/smoke-copilot.golden b/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/smoke-copilot.golden index 3adee4f9cee..dbde9edcdd4 100644 --- a/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/smoke-copilot.golden +++ b/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/smoke-copilot.golden @@ -313,13 +313,13 @@ jobs: 1. **GitHub MCP Testing**: Review the last 2 merged pull requests in __GH_AW_GITHUB_REPOSITORY__ 2. **MCP Scripts GH CLI Testing**: Use the `mcpscripts-gh` tool to query 2 pull requests from __GH_AW_GITHUB_REPOSITORY__ (use args: "pr list --repo __GH_AW_GITHUB_REPOSITORY__ --limit 2 --json number,title,author") - 3. **Serena MCP Testing**: + 3. **Serena MCP Testing**: - Use the Serena MCP server tool `activate_project` to initialize the workspace at `__GH_AW_GITHUB_WORKSPACE__` and verify it succeeds (do NOT use bash to run go commands - use Serena's MCP tools) - After initialization, use the `find_symbol` tool to search for symbols (find which tool to call) and verify that at least 3 symbols are found in the results 4. **Playwright Testing**: Use the playwright tools to navigate to and verify the page title contains "GitHub" (do NOT try to install playwright - use the provided MCP tools) 5. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-copilot-__GH_AW_GITHUB_RUN_ID__.txt` with content "Smoke test passed for Copilot at $(date)" (create the directory if it doesn't exist) 6. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) - 7. **Discussion Interaction Testing**: + 7. **Discussion Interaction Testing**: - Use the `github-discussion-query` mcp-script tool with params: `limit=1, jq=".[0]"` to get the latest discussion from __GH_AW_GITHUB_REPOSITORY__ - Extract the discussion number from the result (e.g., if the result is `{"number": 123, "title": "...", ...}`, extract 123) - Use the `add_comment` tool with `discussion_number: ` to add a fun, playful comment stating that the smoke test agent was here diff --git a/pkg/workflow/unified_prompt_step.go b/pkg/workflow/unified_prompt_step.go index 0e9b79cf695..d33d128d10b 100644 --- a/pkg/workflow/unified_prompt_step.go +++ b/pkg/workflow/unified_prompt_step.go @@ -457,6 +457,16 @@ func (c *Compiler) generateUnifiedPromptCreationStep(yaml *strings.Builder, buil // 2. Write user prompt chunks (appended after built-in sections). // All chunks are written into the same heredoc block (opened above or here) // to minimise the number of delimiter lines in the compiled lock file. + // + // The heredoc payload is a YAML block scalar, so normalizeBlankLines preserves + // it verbatim (it must, since arbitrary block scalars can carry semantically + // significant trailing whitespace and blank runs). Prompt content is markdown + // text the compiler owns, where trailing whitespace is never meaningful and + // long blank runs are noise, so it is cleaned here instead: trailing whitespace + // is trimmed from every line and consecutive blank lines are capped at + // maxConsecutiveBlankLines. userBlankRun is tracked across chunks so a run that + // straddles a chunk boundary is still collapsed. + userBlankRun := 0 for chunkIdx, chunk := range userPromptChunks { unifiedPromptLog.Printf("Writing user prompt chunk %d/%d", chunkIdx+1, len(userPromptChunks)) @@ -472,6 +482,7 @@ func (c *Compiler) generateUnifiedPromptCreationStep(yaml *strings.Builder, buil inHeredoc = true } yaml.WriteString(" " + chunk + "\n") + userBlankRun = 0 continue } @@ -483,8 +494,20 @@ func (c *Compiler) generateUnifiedPromptCreationStep(yaml *strings.Builder, buil lines := strings.SplitSeq(chunk, "\n") for line := range lines { + trimmed := strings.TrimRight(line, " \t") + if trimmed == "" { + // Collapse over-long blank runs; emit truly empty lines (no + // indentation) so they carry no trailing whitespace. + if userBlankRun >= maxConsecutiveBlankLines { + continue + } + userBlankRun++ + yaml.WriteByte('\n') + continue + } + userBlankRun = 0 yaml.WriteString(" ") - yaml.WriteString(line) + yaml.WriteString(trimmed) yaml.WriteByte('\n') } } diff --git a/pkg/workflow/unified_prompt_step_test.go b/pkg/workflow/unified_prompt_step_test.go index a85c7228cf1..d950ba4b674 100644 --- a/pkg/workflow/unified_prompt_step_test.go +++ b/pkg/workflow/unified_prompt_step_test.go @@ -606,3 +606,115 @@ func TestCollectPromptSections_PRCommentPushToPRBranchGuidance(t *testing.T) { } }) } + +// TestGenerateUnifiedPromptCreationStep_TrailingWhitespace verifies that trailing +// whitespace is stripped from every user-prompt content line. +func TestGenerateUnifiedPromptCreationStep_TrailingWhitespace(t *testing.T) { + compiler := &Compiler{} + data := &WorkflowData{ + ParsedTools: NewTools(map[string]any{}), + } + builtinSections := compiler.collectPromptSections(data) + + // Chunk lines with assorted trailing whitespace + userPromptChunks := []string{"line one \nline two\t\nline three \t "} + + var sb strings.Builder + compiler.generateUnifiedPromptCreationStep(&sb, builtinSections, userPromptChunks, nil, data) + output := sb.String() + + assert.NotContains(t, output, "line one ", "trailing spaces should be stripped") + assert.NotContains(t, output, "line two\t", "trailing tab should be stripped") + assert.NotContains(t, output, "line three \t ", "mixed trailing whitespace should be stripped") + assert.Contains(t, output, " line one\n", "content should be present with indentation") + assert.Contains(t, output, " line two\n", "content should be present with indentation") + assert.Contains(t, output, " line three\n", "content should be present with indentation") +} + +// TestGenerateUnifiedPromptCreationStep_BlankRunCapWithinChunk verifies that a run of +// more than maxConsecutiveBlankLines blank lines inside a single chunk is capped. +func TestGenerateUnifiedPromptCreationStep_BlankRunCapWithinChunk(t *testing.T) { + compiler := &Compiler{} + data := &WorkflowData{ + ParsedTools: NewTools(map[string]any{}), + } + builtinSections := compiler.collectPromptSections(data) + + // Four consecutive blank lines between content lines – should be collapsed to two. + userPromptChunks := []string{"# Start\n\n\n\n\n# End"} + + var sb strings.Builder + compiler.generateUnifiedPromptCreationStep(&sb, builtinSections, userPromptChunks, nil, data) + output := sb.String() + + assert.Contains(t, output, "# Start", "start marker should be present") + assert.Contains(t, output, "# End", "end marker should be present") + + // At most maxConsecutiveBlankLines (2) blank lines should appear between them. + startIdx := strings.Index(output, "# Start") + endIdx := strings.Index(output, "# End") + require.Less(t, startIdx, endIdx) + between := output[startIdx:endIdx] + // Three or more consecutive newlines would indicate a blank run of >2. + assert.NotContains(t, between, "\n\n\n\n", "blank run should be capped at 2") +} + +// TestGenerateUnifiedPromptCreationStep_BlankRunCapAcrossChunks verifies that a blank +// run straddling two adjacent chunks is still capped correctly. +func TestGenerateUnifiedPromptCreationStep_BlankRunCapAcrossChunks(t *testing.T) { + compiler := &Compiler{} + data := &WorkflowData{ + ParsedTools: NewTools(map[string]any{}), + } + builtinSections := compiler.collectPromptSections(data) + + // chunk1 ends with two blank lines; chunk2 starts with two blank lines. + // Across the boundary that would be four consecutive blanks; they should be collapsed. + userPromptChunks := []string{ + "# Chunk1\n\n\n", + "\n\n# Chunk2", + } + + var sb strings.Builder + compiler.generateUnifiedPromptCreationStep(&sb, builtinSections, userPromptChunks, nil, data) + output := sb.String() + + startIdx := strings.Index(output, "# Chunk1") + endIdx := strings.Index(output, "# Chunk2") + require.Less(t, startIdx, endIdx) + between := output[startIdx:endIdx] + assert.NotContains(t, between, "\n\n\n\n", "blank run across chunk boundary should be capped at 2") +} + +// TestGenerateUnifiedPromptCreationStep_BlankRunCapAdjacentToRuntimeImport verifies +// that blank-run tracking resets correctly around a runtime-import macro so blank lines +// on both sides of the macro are handled independently. +func TestGenerateUnifiedPromptCreationStep_BlankRunCapAdjacentToRuntimeImport(t *testing.T) { + compiler := &Compiler{} + data := &WorkflowData{ + ParsedTools: NewTools(map[string]any{}), + } + builtinSections := compiler.collectPromptSections(data) + + // chunk[0] ends with a long blank run, chunk[1] is a runtime-import macro, + // chunk[2] starts with a long blank run. + userPromptChunks := []string{ + "# Before\n\n\n\n", + "{{#runtime-import docs/task.md}}", + "\n\n\n\n# After", + } + + var sb strings.Builder + compiler.generateUnifiedPromptCreationStep(&sb, builtinSections, userPromptChunks, nil, data) + output := sb.String() + + // Runtime-import macro must be present verbatim. + assert.Contains(t, output, "{{#runtime-import docs/task.md}}", "runtime-import macro must be emitted verbatim") + + // Content markers must appear. + assert.Contains(t, output, "# Before", "before-marker should be present") + assert.Contains(t, output, "# After", "after-marker should be present") + + // No run of four or more newlines (i.e., 3+ consecutive blank lines) anywhere. + assert.NotContains(t, output, "\n\n\n\n", "blank run should be capped throughout the output") +}