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
147 changes: 54 additions & 93 deletions .github/workflows/security-review.lock.yml

Large diffs are not rendered by default.

47 changes: 25 additions & 22 deletions cmd/gh-aw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ Examples:
dependabot, _ := cmd.Flags().GetBool("dependabot")
forceOverwrite, _ := cmd.Flags().GetBool("force")
refreshStopTime, _ := cmd.Flags().GetBool("refresh-stop-time")
forceRefreshActionPins, _ := cmd.Flags().GetBool("force-refresh-action-pins")
zizmor, _ := cmd.Flags().GetBool("zizmor")
poutine, _ := cmd.Flags().GetBool("poutine")
actionlint, _ := cmd.Flags().GetBool("actionlint")
Expand Down Expand Up @@ -248,28 +249,29 @@ Examples:
workflowDir = workflowsDir
}
config := cli.CompileConfig{
MarkdownFiles: args,
Verbose: verbose,
EngineOverride: engineOverride,
ActionMode: actionMode,
ActionTag: actionTag,
Validate: validate,
Watch: watch,
WorkflowDir: workflowDir,
SkipInstructions: false, // Deprecated field, kept for backward compatibility
NoEmit: noEmit,
Purge: purge,
TrialMode: trial,
TrialLogicalRepoSlug: logicalRepo,
Strict: strict,
Dependabot: dependabot,
ForceOverwrite: forceOverwrite,
RefreshStopTime: refreshStopTime,
Zizmor: zizmor,
Poutine: poutine,
Actionlint: actionlint,
JSONOutput: jsonOutput,
Stats: stats,
MarkdownFiles: args,
Verbose: verbose,
EngineOverride: engineOverride,
ActionMode: actionMode,
ActionTag: actionTag,
Validate: validate,
Watch: watch,
WorkflowDir: workflowDir,
SkipInstructions: false, // Deprecated field, kept for backward compatibility
NoEmit: noEmit,
Purge: purge,
TrialMode: trial,
TrialLogicalRepoSlug: logicalRepo,
Strict: strict,
Dependabot: dependabot,
ForceOverwrite: forceOverwrite,
RefreshStopTime: refreshStopTime,
ForceRefreshActionPins: forceRefreshActionPins,
Zizmor: zizmor,
Poutine: poutine,
Actionlint: actionlint,
JSONOutput: jsonOutput,
Stats: stats,
}
if _, err := cli.CompileWorkflows(cmd.Context(), config); err != nil {
errMsg := err.Error()
Expand Down Expand Up @@ -493,6 +495,7 @@ Use "` + string(constants.CLIExtensionPrefix) + ` help all" to show help for all
compileCmd.Flags().Bool("dependabot", false, "Generate dependency manifests (package.json, requirements.txt, go.mod) and Dependabot config when dependencies are detected")
compileCmd.Flags().Bool("force", false, "Force overwrite of existing dependency files (e.g., dependabot.yml)")
compileCmd.Flags().Bool("refresh-stop-time", false, "Force regeneration of stop-after times instead of preserving existing values from lock files")
compileCmd.Flags().Bool("force-refresh-action-pins", false, "Force refresh of action pins by clearing the cache and resolving all action SHAs from GitHub API")
compileCmd.Flags().Bool("zizmor", false, "Run zizmor security scanner on generated .lock.yml files")
compileCmd.Flags().Bool("poutine", false, "Run poutine security scanner on generated .lock.yml files")
compileCmd.Flags().Bool("actionlint", false, "Run actionlint linter on generated .lock.yml files")
Expand Down
53 changes: 53 additions & 0 deletions pkg/cli/compile_compiler_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,67 @@
package cli

import (
"encoding/json"
"fmt"
"os"
"path/filepath"

"github.com/githubnext/gh-aw/pkg/logger"
"github.com/githubnext/gh-aw/pkg/workflow"
)

var compileCompilerSetupLog = logger.New("cli:compile_compiler_setup")

// resetActionPinsFile resets the action_pins.json file to an empty state
func resetActionPinsFile() error {
compileCompilerSetupLog.Print("Resetting action_pins.json to empty state")

// Get the path to action_pins.json relative to the repository root
// This assumes the command is run from the repository root
actionPinsPath := filepath.Join("pkg", "workflow", "data", "action_pins.json")

// Check if file exists
if _, err := os.Stat(actionPinsPath); os.IsNotExist(err) {
compileCompilerSetupLog.Printf("action_pins.json does not exist at %s, skipping reset", actionPinsPath)
return nil
}

// Create empty structure matching the schema
emptyData := map[string]any{
"entries": map[string]any{},
}

// Marshal with pretty printing
data, err := json.MarshalIndent(emptyData, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal empty action pins: %w", err)
}

// Add trailing newline for prettier compliance
data = append(data, '\n')

// Write the file
if err := os.WriteFile(actionPinsPath, data, 0644); err != nil {
return fmt.Errorf("failed to write action_pins.json: %w", err)
}

compileCompilerSetupLog.Printf("Successfully reset %s to empty state", actionPinsPath)
return nil
}

// createAndConfigureCompiler creates a new compiler instance and configures it
// based on the provided configuration
func createAndConfigureCompiler(config CompileConfig) *workflow.Compiler {
compileCompilerSetupLog.Printf("Creating compiler with config: verbose=%v, validate=%v, strict=%v, trialMode=%v",
config.Verbose, config.Validate, config.Strict, config.TrialMode)

// Handle force refresh action pins - reset the source action_pins.json file
if config.ForceRefreshActionPins {
if err := resetActionPinsFile(); err != nil {
compileCompilerSetupLog.Printf("Warning: failed to reset action_pins.json: %v", err)
}
}

// Create compiler with verbose flag and AI engine override
compiler := workflow.NewCompiler(config.Verbose, config.EngineOverride, GetVersion())
compileCompilerSetupLog.Print("Created compiler instance")
Expand Down Expand Up @@ -88,6 +135,12 @@ func configureCompilerFlags(compiler *workflow.Compiler, config CompileConfig) {
if config.RefreshStopTime {
compileCompilerSetupLog.Print("Stop time refresh enabled: will regenerate stop-after times")
}

// Set force refresh action pins flag
compiler.SetForceRefreshActionPins(config.ForceRefreshActionPins)
if config.ForceRefreshActionPins {
compileCompilerSetupLog.Print("Force refresh action pins enabled: will clear cache and resolve all actions from GitHub API")
}
}

// setupActionMode configures the action script inlining mode
Expand Down
45 changes: 23 additions & 22 deletions pkg/cli/compile_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,29 @@ var compileConfigLog = logger.New("cli:compile_config")

// CompileConfig holds configuration options for compiling workflows
type CompileConfig struct {
MarkdownFiles []string // Files to compile (empty for all files)
Verbose bool // Enable verbose output
EngineOverride string // Override AI engine setting
Validate bool // Enable schema validation
Watch bool // Enable watch mode
WorkflowDir string // Custom workflow directory
SkipInstructions bool // Deprecated: Instructions are no longer written during compilation
NoEmit bool // Validate without generating lock files
Purge bool // Remove orphaned lock files
TrialMode bool // Enable trial mode (suppress safe outputs)
TrialLogicalRepoSlug string // Target repository for trial mode
Strict bool // Enable strict mode validation
Dependabot bool // Generate Dependabot manifests for npm dependencies
ForceOverwrite bool // Force overwrite of existing files (dependabot.yml)
Zizmor bool // Run zizmor security scanner on generated .lock.yml files
Poutine bool // Run poutine security scanner on generated .lock.yml files
Actionlint bool // Run actionlint linter on generated .lock.yml files
JSONOutput bool // Output validation results as JSON
RefreshStopTime bool // Force regeneration of stop-after times instead of preserving existing ones
ActionMode string // Action script inlining mode: inline, dev, or release
ActionTag string // Override action SHA or tag for actions/setup (overrides action-mode to release)
Stats bool // Display statistics table sorted by file size
MarkdownFiles []string // Files to compile (empty for all files)
Verbose bool // Enable verbose output
EngineOverride string // Override AI engine setting
Validate bool // Enable schema validation
Watch bool // Enable watch mode
WorkflowDir string // Custom workflow directory
SkipInstructions bool // Deprecated: Instructions are no longer written during compilation
NoEmit bool // Validate without generating lock files
Purge bool // Remove orphaned lock files
TrialMode bool // Enable trial mode (suppress safe outputs)
TrialLogicalRepoSlug string // Target repository for trial mode
Strict bool // Enable strict mode validation
Dependabot bool // Generate Dependabot manifests for npm dependencies
ForceOverwrite bool // Force overwrite of existing files (dependabot.yml)
RefreshStopTime bool // Force regeneration of stop-after times instead of preserving existing ones
ForceRefreshActionPins bool // Force refresh of action pins by clearing cache and resolving from GitHub API
Zizmor bool // Run zizmor security scanner on generated .lock.yml files
Poutine bool // Run poutine security scanner on generated .lock.yml files
Actionlint bool // Run actionlint linter on generated .lock.yml files
JSONOutput bool // Output validation results as JSON
ActionMode string // Action script inlining mode: inline, dev, or release
ActionTag string // Override action SHA or tag for actions/setup (overrides action-mode to release)
Stats bool // Display statistics table sorted by file size
}

// WorkflowFailure represents a failed workflow with its error count
Expand Down
140 changes: 140 additions & 0 deletions pkg/cli/compile_force_refresh_action_pins_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package cli

import (
"encoding/json"
"os"
"path/filepath"
"testing"

"github.com/githubnext/gh-aw/pkg/testutil"
"github.com/githubnext/gh-aw/pkg/workflow"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestForceRefreshActionPins_ClearCache(t *testing.T) {
// Create temporary directory for testing
tmpDir := testutil.TempDir(t, "test-*")

// Change to temp directory to simulate running from repo root
oldCwd, err := os.Getwd()
require.NoError(t, err, "Failed to get current working directory")
defer func() {
_ = os.Chdir(oldCwd)
}()

err = os.Chdir(tmpDir)
require.NoError(t, err, "Failed to change to temp directory")

// Create a cache with some entries
cache := workflow.NewActionCache(tmpDir)
cache.Set("actions/checkout", "v5", "abc123")
cache.Set("actions/setup-node", "v4", "def456")
err = cache.Save()
require.NoError(t, err, "Failed to save initial cache")

// Verify cache file exists and has entries
cachePath := filepath.Join(tmpDir, ".github", "aw", workflow.CacheFileName)
require.FileExists(t, cachePath, "Cache file should exist before test")

// Load the cache to verify it has entries
testCache := workflow.NewActionCache(tmpDir)
err = testCache.Load()
require.NoError(t, err, "Failed to load cache")
assert.Len(t, testCache.Entries, 2, "Cache should have 2 entries before force refresh")

// Create compiler with force refresh enabled
compiler := workflow.NewCompiler(false, "", "test")
compiler.SetForceRefreshActionPins(true)

// Get the shared action resolver - this should skip loading the cache
actionCache, _ := compiler.GetSharedActionResolverForTest()

// Verify cache is empty (not loaded from disk)
assert.Empty(t, actionCache.Entries, "Cache should be empty when force refresh is enabled")
}

func TestForceRefreshActionPins_ResetFile(t *testing.T) {
// Create temporary directory for testing
tmpDir := testutil.TempDir(t, "test-*")

// Change to temp directory to simulate running from repo root
oldCwd, err := os.Getwd()
require.NoError(t, err, "Failed to get current working directory")
defer func() {
_ = os.Chdir(oldCwd)
}()

err = os.Chdir(tmpDir)
require.NoError(t, err, "Failed to change to temp directory")

// Create the expected directory structure
actionPinsDir := filepath.Join(tmpDir, "pkg", "workflow", "data")
err = os.MkdirAll(actionPinsDir, 0755)
require.NoError(t, err, "Failed to create action pins directory")

// Create a mock action_pins.json with some entries
actionPinsPath := filepath.Join(actionPinsDir, "action_pins.json")
mockData := `{
"entries": {
"actions/checkout@v5": {
"repo": "actions/checkout",
"version": "v5",
"sha": "abc123"
}
}
}`
err = os.WriteFile(actionPinsPath, []byte(mockData), 0644)
require.NoError(t, err, "Failed to create mock action_pins.json")

// Call resetActionPinsFile
err = resetActionPinsFile()
require.NoError(t, err, "resetActionPinsFile should not return error")

// Verify the file was reset to empty
data, err := os.ReadFile(actionPinsPath)
require.NoError(t, err, "Failed to read action_pins.json")

// Parse the JSON to verify structure
var result map[string]any
err = json.Unmarshal(data, &result)
require.NoError(t, err, "Failed to parse action_pins.json")

// Verify it has the correct structure with empty entries
assert.Contains(t, result, "entries", "File should contain 'entries' key")
entries, ok := result["entries"].(map[string]any)
require.True(t, ok, "entries should be a map")
assert.Empty(t, entries, "entries should be empty after reset")
}

func TestForceRefreshActionPins_NoFileExists(t *testing.T) {
// Create temporary directory for testing
tmpDir := testutil.TempDir(t, "test-*")

// Change to temp directory to simulate running from repo root
oldCwd, err := os.Getwd()
require.NoError(t, err, "Failed to get current working directory")
defer func() {
_ = os.Chdir(oldCwd)
}()

err = os.Chdir(tmpDir)
require.NoError(t, err, "Failed to change to temp directory")

// Call resetActionPinsFile when file doesn't exist - should not error
err = resetActionPinsFile()
require.NoError(t, err, "resetActionPinsFile should not error when file doesn't exist")
}

func TestForceRefreshActionPins_EnablesValidation(t *testing.T) {
// Test that force refresh automatically enables validation
config := CompileConfig{
ForceRefreshActionPins: true,
Validate: false, // Explicitly disabled
}

// Simulate the logic in compileSpecificFiles
shouldValidate := config.Validate || config.ForceRefreshActionPins

assert.True(t, shouldValidate, "Validation should be enabled when ForceRefreshActionPins is true")
}
Loading