diff --git a/.github/workflows/test-quality-sentinel.lock.yml b/.github/workflows/test-quality-sentinel.lock.yml index c228072ed92..348e259c854 100644 --- a/.github/workflows/test-quality-sentinel.lock.yml +++ b/.github/workflows/test-quality-sentinel.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"258c6d8dd59cead46ad71f9e4c61c9c022edd3eeff1b7dc5cf6a54ee691ffce4","body_hash":"8e1fff386f1e81cee6ed5549d6410765d51aba641835b13866e2422ac141b86e","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"6610d22ade49c5e3877b8864f93eea885c74ee28265d60065eb787501167aac9","body_hash":"8e1fff386f1e81cee6ed5549d6410765d51aba641835b13866e2422ac141b86e","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}} # 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":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-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.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.2","digest":"sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.2@sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]} # 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 # @@ -259,7 +259,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_WORKFLOW_NAME: "Test Quality Sentinel" - GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿงช *Test quality analysis by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿ”ฌ [{workflow_name}]({run_url}) is analyzing test quality on this {event_type}...\",\"runSuccess\":\"๐Ÿงช [{workflow_name}]({run_url}) completed test quality analysis.\",\"runFailure\":\"โŒ [{workflow_name}]({run_url}) {status} during test quality analysis.\"}" + GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿงช *Test quality analysis by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿ”ฌ [{workflow_name}]({run_url}) is analyzing test quality on this {event_type}...\",\"runSuccess\":\"โœ… [{workflow_name}]({run_url}) completed test quality analysis.\",\"runFailure\":\"โŒ [{workflow_name}]({run_url}) {status} during test quality analysis.\"}" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -1357,7 +1357,7 @@ jobs: GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} - GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿงช *Test quality analysis by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿ”ฌ [{workflow_name}]({run_url}) is analyzing test quality on this {event_type}...\",\"runSuccess\":\"๐Ÿงช [{workflow_name}]({run_url}) completed test quality analysis.\",\"runFailure\":\"โŒ [{workflow_name}]({run_url}) {status} during test quality analysis.\"}" + GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿงช *Test quality analysis by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿ”ฌ [{workflow_name}]({run_url}) is analyzing test quality on this {event_type}...\",\"runSuccess\":\"โœ… [{workflow_name}]({run_url}) completed test quality analysis.\",\"runFailure\":\"โŒ [{workflow_name}]({run_url}) {status} during test quality analysis.\"}" GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" @@ -1383,7 +1383,7 @@ jobs: GH_AW_SAFE_OUTPUTS_RESULT: ${{ needs.safe_outputs.result }} GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} - GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿงช *Test quality analysis by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿ”ฌ [{workflow_name}]({run_url}) is analyzing test quality on this {event_type}...\",\"runSuccess\":\"๐Ÿงช [{workflow_name}]({run_url}) completed test quality analysis.\",\"runFailure\":\"โŒ [{workflow_name}]({run_url}) {status} during test quality analysis.\"}" + GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿงช *Test quality analysis by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿ”ฌ [{workflow_name}]({run_url}) is analyzing test quality on this {event_type}...\",\"runSuccess\":\"โœ… [{workflow_name}]({run_url}) completed test quality analysis.\",\"runFailure\":\"โŒ [{workflow_name}]({run_url}) {status} during test quality analysis.\"}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1724,7 +1724,7 @@ jobs: GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} GH_AW_ENGINE_VERSION: "1.0.60" GH_AW_PROJECT_UTC: "-08:00" - GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿงช *Test quality analysis by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿ”ฌ [{workflow_name}]({run_url}) is analyzing test quality on this {event_type}...\",\"runSuccess\":\"๐Ÿงช [{workflow_name}]({run_url}) completed test quality analysis.\",\"runFailure\":\"โŒ [{workflow_name}]({run_url}) {status} during test quality analysis.\"}" + GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿงช *Test quality analysis by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿ”ฌ [{workflow_name}]({run_url}) is analyzing test quality on this {event_type}...\",\"runSuccess\":\"โœ… [{workflow_name}]({run_url}) completed test quality analysis.\",\"runFailure\":\"โŒ [{workflow_name}]({run_url}) {status} during test quality analysis.\"}" GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_EMOJI: "๐Ÿงช" GH_AW_WORKFLOW_ID: "test-quality-sentinel" diff --git a/pkg/workflow/copilot_requests_tip_test.go b/pkg/workflow/copilot_requests_tip_test.go new file mode 100644 index 00000000000..ccf074f73a3 --- /dev/null +++ b/pkg/workflow/copilot_requests_tip_test.go @@ -0,0 +1,104 @@ +//go:build integration + +package workflow + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/github/gh-aw/pkg/testutil" +) + +func TestCopilotRequestsEnableTip(t *testing.T) { + tests := []struct { + name string + content string + expectTip bool + }{ + { + name: "copilot engine without copilot-requests emits tip", + content: `--- +on: workflow_dispatch +engine: copilot +permissions: + contents: read +--- + +# Test Workflow +`, + expectTip: true, + }, + { + name: "copilot engine with copilot-requests write does not emit tip", + content: `--- +on: workflow_dispatch +engine: copilot +permissions: + contents: read + copilot-requests: write +--- + +# Test Workflow +`, + expectTip: false, + }, + { + name: "copilot engine with copilot-requests none does not emit tip", + content: `--- +on: workflow_dispatch +engine: copilot +permissions: + contents: read + copilot-requests: none +--- + +# Test Workflow +`, + expectTip: false, + }, + { + name: "non-copilot engine does not emit tip", + content: `--- +on: workflow_dispatch +engine: claude +permissions: + contents: read +--- + +# Test Workflow +`, + expectTip: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := testutil.TempDir(t, "copilot-requests-tip-test") + testFile := filepath.Join(tmpDir, "test-workflow.md") + if err := os.WriteFile(testFile, []byte(tt.content), 0600); err != nil { + t.Fatal(err) + } + + var compileErr error + stderrOutput := testutil.CaptureStderr(t, func() { + compiler := NewCompiler() + compiler.SetStrictMode(false) + compileErr = compiler.CompileWorkflow(testFile) + }) + + if compileErr != nil { + t.Fatalf("Expected compilation to succeed but it failed: %v", compileErr) + } + + const tipText = "Tip: set permissions.copilot-requests: write to use GitHub Actions token-based inference" + if tt.expectTip && !strings.Contains(stderrOutput, tipText) { + t.Fatalf("Expected copilot-requests tip in stderr, got:\n%s", stderrOutput) + } + if !tt.expectTip && strings.Contains(stderrOutput, tipText) { + t.Fatalf("Did not expect copilot-requests tip in stderr, got:\n%s", stderrOutput) + } + }) + } +} diff --git a/pkg/workflow/permissions_compiler_validator.go b/pkg/workflow/permissions_compiler_validator.go index 01070df5533..1ba6b3d1e05 100644 --- a/pkg/workflow/permissions_compiler_validator.go +++ b/pkg/workflow/permissions_compiler_validator.go @@ -161,10 +161,28 @@ Ensure proper audience validation and trust policies are configured.` fmt.Fprintln(os.Stderr, formatCompilerMessage(markdownPath, "warning", warningMsg)) c.IncrementWarningCount() } + if shouldEmitCopilotRequestsEnableTip(workflowData, workflowPermissions) { + tipMsg := `Tip: set permissions.copilot-requests: write to use GitHub Actions token-based inference with the Copilot engine instead of a personal access token (COPILOT_GITHUB_TOKEN).` + fmt.Fprintln(os.Stderr, formatCompilerMessage(markdownPath, "info", tipMsg)) + } return workflowPermissions, nil } +func shouldEmitCopilotRequestsEnableTip(workflowData *WorkflowData, workflowPermissions *Permissions) bool { + if workflowData == nil || workflowPermissions == nil { + return false + } + if workflowData.EngineConfig == nil || workflowData.EngineConfig.ID != "copilot" { + return false + } + if level, exists := workflowPermissions.GetExplicit(PermissionCopilotRequests); exists && level == PermissionNone { + return false + } + level, exists := workflowPermissions.Get(PermissionCopilotRequests) + return !exists || level != PermissionWrite +} + func validateOIDCPermissions(workflowData *WorkflowData, workflowPermissions *Permissions) error { if workflowData == nil { return nil