Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions docs/src/content/docs/reference/frontmatter-full.md
Original file line number Diff line number Diff line change
Expand Up @@ -985,11 +985,7 @@ tools:
github-token: "${{ secrets.GITHUB_TOKEN }}"

# Array of GitHub MCP server toolset names to enable specific groups of GitHub API
# functionalities. Prefer using toolset over listing individual tools with 'allowed'.
# Available toolsets: context, repos, issues, pull_requests, actions, code_security,
# dependabot, discussions, experiments, gists, labels, notifications, orgs, projects,
# secret_protection, security_advisories, stargazers, users, search
# Use [default] for recommended toolsets or [all] for everything.
# functionalities
# (optional)
toolsets: []
# Array of Toolset name
Expand Down
4 changes: 2 additions & 2 deletions pkg/cli/compile_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ func CompileWorkflows(config CompileConfig) ([]*workflow.WorkflowData, error) {
}
}

if err := runZizmor(absWorkflowDir, verbose); err != nil {
if err := runZizmor(absWorkflowDir, verbose, strict); err != nil {
return workflowDataList, fmt.Errorf("zizmor security scan failed: %w", err)
}
}
Expand Down Expand Up @@ -484,7 +484,7 @@ func CompileWorkflows(config CompileConfig) ([]*workflow.WorkflowData, error) {

// Run zizmor security scanner if requested and compilation was successful
if zizmor && !noEmit {
if err := runZizmor(workflowsDir, verbose); err != nil {
if err := runZizmor(workflowsDir, verbose, strict); err != nil {
return workflowDataList, fmt.Errorf("zizmor security scan failed: %w", err)
}
}
Expand Down
3 changes: 1 addition & 2 deletions pkg/cli/workflows/test-all.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ safe-outputs:
timeout_minutes: 15

# Custom run name
run-name: "Poem Bot triggered by ${{ github.actor }}"
run-name: "Poem Bot triggered by repository workflow"

# Environment variables
env:
Expand All @@ -159,7 +159,6 @@ You are the **Poem Bot**, a creative AI agent that responds to various GitHub ev
### Current Context
- **Repository**: ${{ github.repository }}
- **Triggered by**: GitHub workflow
- **Actor**: ${{ github.actor }}
- **Poem Theme**: ${{ env.POEM_THEME }}
- **Action Type**: ${{ env.ACTION_TYPE }}
- **Content**: "${{ needs.activation.outputs.text }}"
Expand Down
2 changes: 0 additions & 2 deletions pkg/cli/workflows/test-template-github-actions-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ engine:
This workflow tests template rendering with GitHub Actions expressions in conditions.

Repository: ${{ github.repository }}
Actor: ${{ github.actor }}

{{#if true}}
## Standard Analysis
Expand All @@ -31,7 +30,6 @@ This section is hidden and won't be included in the prompt.

## Workflow Information

- Workflow: ${{ github.workflow }}
- Run ID: ${{ github.run_id }}
- Run Number: ${{ github.run_number }}

Expand Down
2 changes: 0 additions & 2 deletions pkg/cli/workflows/test-template-issue-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ tools:

Analyze issue #${{ github.event.issue.number }} in repository ${{ github.repository }}.

Created by: ${{ github.actor }}

{{#if ${{ github.event.issue.number }}}}
## Standard Analysis

Expand Down
2 changes: 0 additions & 2 deletions pkg/cli/workflows/test-template-pr-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ tools:

Review PR #${{ github.event.pull_request.number }} in repository ${{ github.repository }}.

Opened by: ${{ github.actor }}

{{#if true}}
## Standard Review

Expand Down
26 changes: 17 additions & 9 deletions pkg/cli/zizmor.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ type zizmorFinding struct {
}

// runZizmor runs the zizmor security scanner on generated .lock.yml files using Docker
func runZizmor(workflowsDir string, verbose bool) error {
func runZizmor(workflowsDir string, verbose bool, strict bool) error {
compileLog.Print("Running zizmor security scanner")

if verbose {
Expand Down Expand Up @@ -75,9 +75,10 @@ func runZizmor(workflowsDir string, verbose bool) error {
// Run the command
err = cmd.Run()

// Parse and reformat the output
if err := parseAndDisplayZizmorOutput(stdout.String(), stderr.String(), verbose); err != nil {
compileLog.Printf("Failed to parse zizmor output: %v", err)
// Parse and reformat the output, get total warning count
totalWarnings, parseErr := parseAndDisplayZizmorOutput(stdout.String(), stderr.String(), verbose)
if parseErr != nil {
compileLog.Printf("Failed to parse zizmor output: %v", parseErr)
// Fall back to showing raw output
if stdout.Len() > 0 {
fmt.Fprint(os.Stderr, stdout.String())
Expand All @@ -97,9 +98,13 @@ func runZizmor(workflowsDir string, verbose bool) error {
if exitErr, ok := err.(*exec.ExitError); ok {
exitCode := exitErr.ExitCode()
compileLog.Printf("Zizmor exited with code %d", exitCode)
// Exit codes 10-14 indicate findings, not failures
// Treat these as success but log them
// Exit codes 10-14 indicate findings
if exitCode >= 10 && exitCode <= 14 {
// In strict mode, findings are treated as errors
if strict {
return fmt.Errorf("strict mode: zizmor found %d security warnings/errors - workflows must have no zizmor findings in strict mode", totalWarnings)
}
// In non-strict mode, findings are logged but not treated as errors
return nil
}
// Other exit codes are actual errors
Expand All @@ -117,7 +122,8 @@ func runZizmor(workflowsDir string, verbose bool) error {
}

// parseAndDisplayZizmorOutput parses zizmor JSON output and displays it in the desired format
func parseAndDisplayZizmorOutput(stdout, stderr string, verbose bool) error {
// Returns the total number of warnings found
func parseAndDisplayZizmorOutput(stdout, stderr string, verbose bool) (int, error) {
// Count findings per file
fileFindings := make(map[string]int)

Expand All @@ -140,9 +146,10 @@ func parseAndDisplayZizmorOutput(stdout, stderr string, verbose bool) error {

// Parse JSON findings from stdout
var findings []zizmorFinding
totalWarnings := 0
if stdout != "" && strings.HasPrefix(strings.TrimSpace(stdout), "[") {
if err := json.Unmarshal([]byte(stdout), &findings); err != nil {
return fmt.Errorf("failed to parse zizmor JSON output: %w", err)
return 0, fmt.Errorf("failed to parse zizmor JSON output: %w", err)
}

// Count findings per file - each finding counts as 1 regardless of how many locations it has
Expand All @@ -154,6 +161,7 @@ func parseAndDisplayZizmorOutput(stdout, stderr string, verbose bool) error {
if filePath != "" && !affectedFiles[filePath] {
affectedFiles[filePath] = true
fileFindings[filePath]++
totalWarnings++
}
}
}
Expand All @@ -170,5 +178,5 @@ func parseAndDisplayZizmorOutput(stdout, stderr string, verbose bool) error {
fmt.Fprintf(os.Stderr, "🌈 zizmor %d %s in %s\n", count, warningText, filePath)
}

return nil
return totalWarnings, nil
}
7 changes: 6 additions & 1 deletion pkg/cli/zizmor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ func TestParseAndDisplayZizmorOutput(t *testing.T) {
r, w, _ := os.Pipe()
os.Stderr = w

err := parseAndDisplayZizmorOutput(tt.stdout, tt.stderr, tt.verbose)
warningCount, err := parseAndDisplayZizmorOutput(tt.stdout, tt.stderr, tt.verbose)

// Restore stderr
w.Close()
Expand All @@ -205,6 +205,11 @@ func TestParseAndDisplayZizmorOutput(t *testing.T) {
t.Errorf("Unexpected error: %v", err)
}

// Verify warning count is non-negative
if warningCount < 0 {
t.Errorf("Warning count should be non-negative, got: %d", warningCount)
}

// Check expected output
for _, expected := range tt.expectedOutput {
if !strings.Contains(output, expected) {
Expand Down
3 changes: 3 additions & 0 deletions pkg/workflow/strict_mode.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import (
)

// validateStrictMode performs strict mode validations on the workflow
// Note: Strict mode also affects zizmor security scanner behavior (see pkg/cli/zizmor.go)
// When zizmor is enabled with --zizmor flag, strict mode will treat any security
// findings as compilation errors rather than warnings.
func (c *Compiler) validateStrictMode(frontmatter map[string]any, networkPermissions *NetworkPermissions) error {
if !c.strictMode {
return nil
Expand Down
66 changes: 66 additions & 0 deletions pkg/workflow/strict_mode_zizmor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package workflow

import (
"os"
"path/filepath"
"strings"
"testing"
)

func TestStrictModeWithZizmor(t *testing.T) {
// Note: This test documents the expected behavior but doesn't actually run zizmor
// since that would require Docker. The actual zizmor integration is tested in
// pkg/cli/zizmor_test.go
tests := []struct {
name string
content string
expectError bool
errorMsg string
}{
{
name: "strict mode should be compatible with zizmor flag",
content: `---
on: push
strict: true
permissions:
contents: read
engine: copilot
network:
allowed:
- "api.example.com"
---

# Test Workflow`,
expectError: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "strict-zizmor-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)

testFile := filepath.Join(tmpDir, "test-workflow.md")
if err := os.WriteFile(testFile, []byte(tt.content), 0644); err != nil {
t.Fatal(err)
}

compiler := NewCompiler(false, "", "")
compiler.SetStrictMode(true)
err = compiler.CompileWorkflow(testFile)

if tt.expectError && err == nil {
t.Error("Expected compilation to fail but it succeeded")
} else if !tt.expectError && err != nil {
t.Errorf("Expected compilation to succeed but it failed: %v", err)
} else if tt.expectError && err != nil && tt.errorMsg != "" {
if !strings.Contains(err.Error(), tt.errorMsg) {
t.Errorf("Expected error containing '%s', got '%s'", tt.errorMsg, err.Error())
}
}
})
}
}
Loading