diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 2de1875bb32..2c7fd072eb8 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -17,6 +17,8 @@ jobs: run: curl -fsSL https://raw.githubusercontent.com/github/gh-aw/refs/heads/main/install-gh-aw.sh | bash - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Set up Node.js uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f with: diff --git a/.github/workflows/copilot-token-audit.lock.yml b/.github/workflows/copilot-token-audit.lock.yml index 7268e4dbf53..6fd7deaa66c 100644 --- a/.github/workflows/copilot-token-audit.lock.yml +++ b/.github/workflows/copilot-token-audit.lock.yml @@ -1,5 +1,5 @@ # gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"30107ff06ddff13a593f175c86fe6ac713e80c7174980a1b05dddccb48fff938","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"668228422ae6a00e4ad889ee87cd7109ec5666a7","version":"v5.0.4"},{"repo":"actions/cache/save","sha":"668228422ae6a00e4ad889ee87cd7109ec5666a7","version":"v5.0.4"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/setup-node","sha":"53b83947a5a98c8d113130e565377fae1a50d02f","version":"v6.3.0"},{"repo":"actions/setup-python","sha":"a309ff8b426b58ec0e2a45f0f869d46889d02405","version":"v6.2.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7"},{"repo":"astral-sh/setup-uv","sha":"eac588ad8def6316056a12d4907a9d4d84ff7a3b","version":"eac588ad8def6316056a12d4907a9d4d84ff7a3b"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.18","digest":"sha256:c77e8c26bab6c39e8568d8e2f8c17015944849a8cbcdfb4bd9725d8893725ca2","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.18@sha256:c77e8c26bab6c39e8568d8e2f8c17015944849a8cbcdfb4bd9725d8893725ca2"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.18","digest":"sha256:d16a40a3ca6e989896d0cef9f31b9412bb1fcc8755bafcafb95012ae1078539b","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.18@sha256:d16a40a3ca6e989896d0cef9f31b9412bb1fcc8755bafcafb95012ae1078539b"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.18","digest":"sha256:eb102afcfbae26ffcec016adebb74d3be7b0a5bf376ba306599cdf3effbe288e","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.18@sha256:eb102afcfbae26ffcec016adebb74d3be7b0a5bf376ba306599cdf3effbe288e"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.17","digest":"sha256:a6dec6ec535a11c565d982afa2f98589805ed0598862b9ea9d3c751fc71afae8","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.2.17@sha256:a6dec6ec535a11c565d982afa2f98589805ed0598862b9ea9d3c751fc71afae8"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0","digest":"sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28","pinned_image":"ghcr.io/github/github-mcp-server:v0.32.0@sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28"},{"image":"node:lts-alpine","digest":"sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b","pinned_image":"node:lts-alpine@sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b"}]} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"668228422ae6a00e4ad889ee87cd7109ec5666a7","version":"v5.0.4"},{"repo":"actions/cache/save","sha":"668228422ae6a00e4ad889ee87cd7109ec5666a7","version":"v5.0.4"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/setup-node","sha":"53b83947a5a98c8d113130e565377fae1a50d02f","version":"v6.3.0"},{"repo":"actions/setup-python","sha":"a309ff8b426b58ec0e2a45f0f869d46889d02405","version":"v6.2.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7"},{"repo":"astral-sh/setup-uv","sha":"eac588ad8def6316056a12d4907a9d4d84ff7a3b","version":"eac588ad8def6316056a12d4907a9d4d84ff7a3b"},{"repo":"docker/build-push-action","sha":"bcafcacb16a39f128d818304e6c9c0c18556b85f","version":"v7.1.0"},{"repo":"docker/setup-buildx-action","sha":"4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd","version":"v4"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.18","digest":"sha256:c77e8c26bab6c39e8568d8e2f8c17015944849a8cbcdfb4bd9725d8893725ca2","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.18@sha256:c77e8c26bab6c39e8568d8e2f8c17015944849a8cbcdfb4bd9725d8893725ca2"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.18","digest":"sha256:d16a40a3ca6e989896d0cef9f31b9412bb1fcc8755bafcafb95012ae1078539b","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.18@sha256:d16a40a3ca6e989896d0cef9f31b9412bb1fcc8755bafcafb95012ae1078539b"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.18","digest":"sha256:eb102afcfbae26ffcec016adebb74d3be7b0a5bf376ba306599cdf3effbe288e","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.18@sha256:eb102afcfbae26ffcec016adebb74d3be7b0a5bf376ba306599cdf3effbe288e"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.17","digest":"sha256:a6dec6ec535a11c565d982afa2f98589805ed0598862b9ea9d3c751fc71afae8","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.2.17@sha256:a6dec6ec535a11c565d982afa2f98589805ed0598862b9ea9d3c751fc71afae8"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0","digest":"sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28","pinned_image":"ghcr.io/github/github-mcp-server:v0.32.0@sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28"},{"image":"node:lts-alpine","digest":"sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b","pinned_image":"node:lts-alpine@sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -50,6 +50,8 @@ # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 # - astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # eac588ad8def6316056a12d4907a9d4d84ff7a3b +# - docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 +# - docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.25.18@sha256:c77e8c26bab6c39e8568d8e2f8c17015944849a8cbcdfb4bd9725d8893725ca2 @@ -358,14 +360,40 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - - name: Create gh-aw temp directory - run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" - - name: Configure gh CLI for GitHub Enterprise - run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" - env: - GH_TOKEN: ${{ github.token }} - - name: Checkout code + - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Setup Go for CLI build + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version-file: go.mod + cache: true + - name: Build gh-aw CLI + run: | + echo "Building gh-aw CLI for linux/amd64..." + mkdir -p dist + VERSION=$(git describe --tags --always --dirty) + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -ldflags "-s -w -X main.version=${VERSION}" \ + -o dist/gh-aw-linux-amd64 \ + ./cmd/gh-aw + # Copy binary to root for direct execution in user-defined steps + cp dist/gh-aw-linux-amd64 ./gh-aw + chmod +x ./gh-aw + echo "✓ Built gh-aw CLI successfully" + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 + - name: Build gh-aw Docker image + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + with: + context: . + platforms: linux/amd64 + push: false + load: true + tags: localhost/gh-aw:dev + build-args: | + BINARY=dist/gh-aw-linux-amd64 - name: Setup Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: @@ -384,6 +412,12 @@ jobs: uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.12' + - name: Create gh-aw temp directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" + - name: Configure gh CLI for GitHub Enterprise + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" + env: + GH_TOKEN: ${{ github.token }} - name: Install gh-aw extension run: curl -fsSL https://raw.githubusercontent.com/github/gh-aw/refs/heads/main/install-gh-aw.sh | bash - name: Install npm dependencies diff --git a/.github/workflows/copilot-token-optimizer.lock.yml b/.github/workflows/copilot-token-optimizer.lock.yml index d7429f693a7..c9b27c76ba5 100644 --- a/.github/workflows/copilot-token-optimizer.lock.yml +++ b/.github/workflows/copilot-token-optimizer.lock.yml @@ -341,14 +341,10 @@ jobs: echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - - name: Create gh-aw temp directory - run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" - - name: Configure gh CLI for GitHub Enterprise - run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" - env: - GH_TOKEN: ${{ github.token }} - - name: Checkout code + - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Setup Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: @@ -363,6 +359,12 @@ jobs: cache: 'npm' cache-dependency-path: 'actions/setup/js/package-lock.json' package-manager-cache: false + - name: Create gh-aw temp directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" + - name: Configure gh CLI for GitHub Enterprise + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" + env: + GH_TOKEN: ${{ github.token }} - name: Install gh-aw extension run: curl -fsSL https://raw.githubusercontent.com/github/gh-aw/refs/heads/main/install-gh-aw.sh | bash - name: Install npm dependencies diff --git a/pkg/parser/yaml_import.go b/pkg/parser/yaml_import.go index 7f1d7af347b..1223170ff71 100644 --- a/pkg/parser/yaml_import.go +++ b/pkg/parser/yaml_import.go @@ -205,8 +205,12 @@ func extractStepsFromCopilotSetup(workflow map[string]any) (string, error) { return "", errors.New("steps field is not a list in copilot-setup-steps job") } - // Ensure checkout step is always included and placed first - stepsSlice = ensureCheckoutStepFirst(stepsSlice) + // Strip checkout steps from the imported copilot-setup-steps. The compiler + // generates its own secure checkout (with persist-credentials: false) via + // CheckoutManager.GenerateDefaultCheckoutStep, so the imported checkout is + // redundant and can introduce artipacked findings when the same job uploads + // artifacts. + stepsSlice = stripCheckoutSteps(stepsSlice) // Marshal steps array directly to YAML format (without "steps:" wrapper) // This matches the format expected by the compiler which unmarshals into []any @@ -215,52 +219,30 @@ func extractStepsFromCopilotSetup(workflow map[string]any) (string, error) { return "", fmt.Errorf("failed to marshal steps to YAML: %w", err) } - yamlImportLog.Printf("Extracted steps from copilot-setup-steps job (YAML array format) with checkout step ensured") + yamlImportLog.Printf("Extracted steps from copilot-setup-steps job (YAML array format) with checkout steps stripped") return string(stepsYAML), nil } -// ensureCheckoutStepFirst ensures a checkout step exists and is placed first in the steps list -// If a checkout step exists, it's moved to the beginning. If not, one is added. -func ensureCheckoutStepFirst(steps []any) []any { - // Find existing checkout step index - checkoutIndex := -1 - for i, step := range steps { +// stripCheckoutSteps removes any actions/checkout steps from the imported +// copilot-setup-steps. The compiler generates its own secure checkout step +// (with persist-credentials: false), so the imported checkout is redundant. +// Stripping it prevents the artipacked finding where checkout + artifact +// upload coexist with persisted credentials, and avoids a duplicate checkout +// in the compiled lock file. +func stripCheckoutSteps(steps []any) []any { + result := make([]any, 0, len(steps)) + for _, step := range steps { if stepMap, ok := step.(map[string]any); ok { if uses, hasUses := stepMap["uses"]; hasUses { if usesStr, ok := uses.(string); ok { - // Check if this is a checkout action (actions/checkout@... or exactly "actions/checkout") if strings.HasPrefix(usesStr, "actions/checkout@") || usesStr == "actions/checkout" { - checkoutIndex = i - break + yamlImportLog.Printf("Stripping checkout step from copilot-setup-steps: %s", usesStr) + continue } } } } + result = append(result, step) } - - // If checkout step exists and is already first, no changes needed - if checkoutIndex == 0 { - yamlImportLog.Print("Checkout step already at beginning of copilot-setup-steps") - return steps - } - - // If checkout step exists but not first, move it to the beginning - if checkoutIndex > 0 { - yamlImportLog.Printf("Moving existing checkout step from position %d to beginning", checkoutIndex) - checkoutStep := steps[checkoutIndex] - // Remove from current position - steps = append(steps[:checkoutIndex], steps[checkoutIndex+1:]...) - // Prepend to beginning - steps = append([]any{checkoutStep}, steps...) - return steps - } - - // No checkout step found, add a default one at the beginning - yamlImportLog.Print("No checkout step found in copilot-setup-steps, adding default checkout step at beginning") - defaultCheckoutStep := map[string]any{ - "name": "Checkout code", - "uses": "actions/checkout@v6", - } - steps = append([]any{defaultCheckoutStep}, steps...) - return steps + return result } diff --git a/pkg/parser/yaml_import_copilot_setup_test.go b/pkg/parser/yaml_import_copilot_setup_test.go index d88e2c98811..09e3de88d4a 100644 --- a/pkg/parser/yaml_import_copilot_setup_test.go +++ b/pkg/parser/yaml_import_copilot_setup_test.go @@ -92,9 +92,9 @@ func TestExtractStepsFromCopilotSetup(t *testing.T) { // Verify the YAML contains the expected content (as a YAML array, not with "steps:" wrapper) assert.NotContains(t, stepsYAML, "steps:", "Should NOT contain steps field wrapper") assert.Contains(t, stepsYAML, "Install gh-aw extension", "Should contain install step") - assert.Contains(t, stepsYAML, "Checkout code", "Should contain checkout step") + assert.NotContains(t, stepsYAML, "Checkout code", "Should NOT contain checkout step (stripped during import)") + assert.NotContains(t, stepsYAML, "actions/checkout@v4", "Should NOT contain checkout action (stripped during import)") assert.Contains(t, stepsYAML, "Set up Node.js", "Should contain Node.js setup step") - assert.Contains(t, stepsYAML, "actions/checkout@v4", "Should contain checkout action") assert.Contains(t, stepsYAML, "actions/setup-node@v4", "Should contain Node.js setup action") // Verify it's formatted as a YAML array (starts with "- name:") @@ -143,8 +143,8 @@ func TestExtractStepsFromCopilotSetup_NoSteps(t *testing.T) { assert.Contains(t, err.Error(), "no steps found", "Error should mention missing steps") } -func TestExtractStepsFromCopilotSetup_EnsuresCheckoutFirst(t *testing.T) { - // Test workflow with checkout step NOT first +func TestExtractStepsFromCopilotSetup_StripsCheckoutStep(t *testing.T) { + // Test workflow with checkout step NOT first — checkout should be stripped workflow := map[string]any{ "name": "Copilot Setup Steps", "on": "workflow_dispatch", @@ -176,7 +176,11 @@ func TestExtractStepsFromCopilotSetup_EnsuresCheckoutFirst(t *testing.T) { require.NoError(t, err, "Should extract steps without error") require.NotEmpty(t, stepsYAML, "Should return non-empty steps YAML") - // Verify checkout step is first + // Verify checkout step is stripped (compiler handles checkout securely) + assert.NotContains(t, stepsYAML, "Checkout code", "Should NOT contain checkout step") + assert.NotContains(t, stepsYAML, "actions/checkout", "Should NOT contain checkout action") + + // Verify first step is now the install step (checkout was removed) lines := strings.Split(stepsYAML, "\n") var firstStepName string for _, line := range lines { @@ -185,23 +189,16 @@ func TestExtractStepsFromCopilotSetup_EnsuresCheckoutFirst(t *testing.T) { break } } - assert.Contains(t, firstStepName, "Checkout code", "First step should be the checkout step") + assert.Contains(t, firstStepName, "Install gh-aw extension", "First step should be the install step after checkout is stripped") - // Verify all steps are present + // Verify non-checkout steps are preserved assert.Contains(t, stepsYAML, "Install gh-aw extension", "Should contain install step") - assert.Contains(t, stepsYAML, "Checkout code", "Should contain checkout step") assert.Contains(t, stepsYAML, "Set up Node.js", "Should contain Node.js setup step") - - // Verify checkout comes before install (order should be: checkout, install, node) - checkoutIndex := strings.Index(stepsYAML, "Checkout code") - installIndex := strings.Index(stepsYAML, "Install gh-aw extension") - nodeIndex := strings.Index(stepsYAML, "Set up Node.js") - assert.Less(t, checkoutIndex, installIndex, "Checkout should come before install") - assert.Less(t, installIndex, nodeIndex, "Install should come before Node.js setup") } -func TestExtractStepsFromCopilotSetup_AddsCheckoutIfMissing(t *testing.T) { - // Test workflow without any checkout step +func TestExtractStepsFromCopilotSetup_NoCheckoutStaysClean(t *testing.T) { + // Test workflow without any checkout step — should remain without checkout + // since the compiler handles checkout generation securely workflow := map[string]any{ "name": "Copilot Setup Steps", "on": "workflow_dispatch", @@ -226,11 +223,15 @@ func TestExtractStepsFromCopilotSetup_AddsCheckoutIfMissing(t *testing.T) { require.NoError(t, err, "Should extract steps without error") require.NotEmpty(t, stepsYAML, "Should return non-empty steps YAML") - // Verify checkout step was added - assert.Contains(t, stepsYAML, "Checkout code", "Should contain added checkout step") - assert.Contains(t, stepsYAML, "actions/checkout@v6", "Should contain checkout action") + // Verify no checkout step was added (compiler handles it) + assert.NotContains(t, stepsYAML, "Checkout code", "Should NOT contain a checkout step") + assert.NotContains(t, stepsYAML, "actions/checkout", "Should NOT contain checkout action") + + // Verify original steps are still present + assert.Contains(t, stepsYAML, "Install dependencies", "Should contain original install step") + assert.Contains(t, stepsYAML, "Run linter", "Should contain original linter step") - // Verify checkout step is first + // Verify first step is Install dependencies lines := strings.Split(stepsYAML, "\n") var firstStepName string for _, line := range lines { @@ -239,22 +240,11 @@ func TestExtractStepsFromCopilotSetup_AddsCheckoutIfMissing(t *testing.T) { break } } - assert.Contains(t, firstStepName, "Checkout code", "First step should be the checkout step") - - // Verify original steps are still present - assert.Contains(t, stepsYAML, "Install dependencies", "Should contain original install step") - assert.Contains(t, stepsYAML, "Run linter", "Should contain original linter step") - - // Verify checkout comes before other steps - checkoutIndex := strings.Index(stepsYAML, "Checkout code") - installIndex := strings.Index(stepsYAML, "Install dependencies") - lintIndex := strings.Index(stepsYAML, "Run linter") - assert.Less(t, checkoutIndex, installIndex, "Checkout should come before install") - assert.Less(t, checkoutIndex, lintIndex, "Checkout should come before lint") + assert.Contains(t, firstStepName, "Install dependencies", "First step should be the install step") } -func TestExtractStepsFromCopilotSetup_CheckoutAlreadyFirst(t *testing.T) { - // Test workflow with checkout step already first +func TestExtractStepsFromCopilotSetup_CheckoutFirstIsStripped(t *testing.T) { + // Test workflow with checkout step already first — it should still be stripped workflow := map[string]any{ "name": "Copilot Setup Steps", "on": "workflow_dispatch", @@ -283,7 +273,15 @@ func TestExtractStepsFromCopilotSetup_CheckoutAlreadyFirst(t *testing.T) { require.NoError(t, err, "Should extract steps without error") require.NotEmpty(t, stepsYAML, "Should return non-empty steps YAML") - // Verify checkout step is first + // Verify checkout step is stripped + assert.NotContains(t, stepsYAML, "Checkout code", "Should NOT contain checkout step") + assert.NotContains(t, stepsYAML, "actions/checkout", "Should NOT contain checkout action") + + // Verify non-checkout steps are present + assert.Contains(t, stepsYAML, "Install dependencies", "Should contain install step") + assert.Contains(t, stepsYAML, "Run tests", "Should contain test step") + + // Verify first step is now Install dependencies lines := strings.Split(stepsYAML, "\n") var firstStepName string for _, line := range lines { @@ -292,18 +290,11 @@ func TestExtractStepsFromCopilotSetup_CheckoutAlreadyFirst(t *testing.T) { break } } - assert.Contains(t, firstStepName, "Checkout code", "First step should be the checkout step") - - // Verify all steps are present - assert.Contains(t, stepsYAML, "Checkout code", "Should contain checkout step") - assert.Contains(t, stepsYAML, "Install dependencies", "Should contain install step") - assert.Contains(t, stepsYAML, "Run tests", "Should contain test step") + assert.Contains(t, firstStepName, "Install dependencies", "First step should be install after checkout is stripped") - // Verify order is maintained - checkoutIndex := strings.Index(stepsYAML, "Checkout code") + // Verify order of remaining steps installIndex := strings.Index(stepsYAML, "Install dependencies") testIndex := strings.Index(stepsYAML, "Run tests") - assert.Less(t, checkoutIndex, installIndex, "Checkout should come before install") assert.Less(t, installIndex, testIndex, "Install should come before tests") } @@ -353,7 +344,7 @@ jobs: // Verify the steps YAML contains expected content (as a YAML array) assert.NotContains(t, stepsYAML, "steps:", "Should NOT contain steps field wrapper") assert.Contains(t, stepsYAML, "Install gh-aw extension", "Should contain install step") - assert.Contains(t, stepsYAML, "Checkout code", "Should contain checkout step") + assert.NotContains(t, stepsYAML, "Checkout code", "Should NOT contain checkout step (stripped)") assert.Contains(t, stepsYAML, "Set up Node.js", "Should contain Node.js setup step") assert.Contains(t, stepsYAML, "Set up Go", "Should contain Go setup step")