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
4 changes: 2 additions & 2 deletions .github/workflows/release.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

99 changes: 99 additions & 0 deletions pkg/workflow/bundler_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,105 @@ func ValidateEmbeddedResourceRequires(sources map[string]string) error {
return nil
}

// validateNoExecSync checks that GitHub Script mode scripts do not use execSync
// GitHub Script mode should use exec instead for better async/await handling
// Returns an error if execSync is found, otherwise returns nil
func validateNoExecSync(scriptName string, content string, mode RuntimeMode) error {
// Only validate GitHub Script mode
if mode != RuntimeModeGitHubScript {
return nil
}

bundlerValidationLog.Printf("Validating no execSync in GitHub Script: %s (%d bytes)", scriptName, len(content))

// Regular expression to match execSync usage
// Matches: execSync(...) with various patterns
execSyncRegex := regexp.MustCompile(`\bexecSync\s*\(`)

lines := strings.Split(content, "\n")
var foundUsages []string

for lineNum, line := range lines {
trimmed := strings.TrimSpace(line)

// Skip comment lines
if strings.HasPrefix(trimmed, "//") || strings.HasPrefix(trimmed, "/*") || strings.HasPrefix(trimmed, "*") {
continue
}

// Check for execSync usage
if execSyncRegex.MatchString(line) {
foundUsages = append(foundUsages, fmt.Sprintf("line %d: %s", lineNum+1, strings.TrimSpace(line)))
}
}

if len(foundUsages) > 0 {
bundlerValidationLog.Printf("Validation failed: found %d execSync usage(s) in %s", len(foundUsages), scriptName)
return fmt.Errorf("GitHub Script mode script '%s' contains %d execSync usage(s):\n %s\n\nGitHub Script mode should use exec instead of execSync for better async/await handling",
scriptName, len(foundUsages), strings.Join(foundUsages, "\n "))
}

bundlerValidationLog.Printf("Validation successful: no execSync usage found in %s", scriptName)
return nil
}

// validateNoGitHubScriptGlobals checks that Node.js mode scripts do not use GitHub Actions globals
// Node.js scripts should not rely on actions/github-script globals like core.*, exec.*, or github.*
// Returns an error if GitHub Actions globals are found, otherwise returns nil
func validateNoGitHubScriptGlobals(scriptName string, content string, mode RuntimeMode) error {
// Only validate Node.js mode
if mode != RuntimeModeNodeJS {
return nil
}

bundlerValidationLog.Printf("Validating no GitHub Actions globals in Node.js script: %s (%d bytes)", scriptName, len(content))

// Regular expressions to match GitHub Actions globals
// Matches: core.method, exec.method, github.property
coreGlobalRegex := regexp.MustCompile(`\bcore\.\w+`)
execGlobalRegex := regexp.MustCompile(`\bexec\.\w+`)
githubGlobalRegex := regexp.MustCompile(`\bgithub\.\w+`)

lines := strings.Split(content, "\n")
var foundUsages []string

for lineNum, line := range lines {
trimmed := strings.TrimSpace(line)

// Skip comment lines and type references
if strings.HasPrefix(trimmed, "//") || strings.HasPrefix(trimmed, "/*") || strings.HasPrefix(trimmed, "*") {
continue
}
if strings.Contains(trimmed, "/// <reference") {
continue
}

// Check for core.* usage
if coreGlobalRegex.MatchString(line) {
foundUsages = append(foundUsages, fmt.Sprintf("line %d: core.* usage: %s", lineNum+1, strings.TrimSpace(line)))
}

// Check for exec.* usage
if execGlobalRegex.MatchString(line) {
foundUsages = append(foundUsages, fmt.Sprintf("line %d: exec.* usage: %s", lineNum+1, strings.TrimSpace(line)))
}

// Check for github.* usage
if githubGlobalRegex.MatchString(line) {
foundUsages = append(foundUsages, fmt.Sprintf("line %d: github.* usage: %s", lineNum+1, strings.TrimSpace(line)))
}
}

if len(foundUsages) > 0 {
bundlerValidationLog.Printf("Validation failed: found %d GitHub Actions global usage(s) in %s", len(foundUsages), scriptName)
return fmt.Errorf("Node.js mode script '%s' contains %d GitHub Actions global usage(s):\n %s\n\nNode.js scripts should not use GitHub Actions globals (core.*, exec.*, github.*)",
scriptName, len(foundUsages), strings.Join(foundUsages, "\n "))
}

bundlerValidationLog.Printf("Validation successful: no GitHub Actions globals found in %s", scriptName)
return nil
}

// normalizePath normalizes a file path by resolving . and .. components
func normalizePath(path string) string {
// Split path into parts
Expand Down
243 changes: 243 additions & 0 deletions pkg/workflow/bundler_validation_script_registry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
package workflow

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestValidateNoExecSync_GitHubScriptMode(t *testing.T) {
tests := []struct {
name string
scriptName string
content string
mode RuntimeMode
expectError bool
}{
{
name: "GitHub Script mode with execSync should fail",
scriptName: "test_script",
content: `
const { execSync } = require("child_process");
const result = execSync("ls -la");
`,
mode: RuntimeModeGitHubScript,
expectError: true,
},
{
name: "GitHub Script mode with exec should pass",
scriptName: "test_script",
content: `
const { exec } = require("@actions/exec");
await exec.exec("ls -la");
`,
mode: RuntimeModeGitHubScript,
expectError: false,
},
{
name: "GitHub Script mode without exec should pass",
scriptName: "test_script",
content: `
const fs = require("fs");
const data = fs.readFileSync("file.txt");
`,
mode: RuntimeModeGitHubScript,
expectError: false,
},
{
name: "Node.js mode with execSync should pass (not checked)",
scriptName: "test_script",
content: `
const { execSync } = require("child_process");
const result = execSync("ls -la");
`,
mode: RuntimeModeNodeJS,
expectError: false,
},
{
name: "GitHub Script mode with execSync in comment should pass",
scriptName: "test_script",
content: `
// Don't use execSync, use exec instead
const { exec } = require("@actions/exec");
`,
mode: RuntimeModeGitHubScript,
expectError: false,
},
{
name: "GitHub Script mode with multiple execSync calls should fail",
scriptName: "test_script",
content: `
const { execSync } = require("child_process");
execSync("git status");
const output = execSync("git diff");
`,
mode: RuntimeModeGitHubScript,
expectError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateNoExecSync(tt.scriptName, tt.content, tt.mode)
if tt.expectError {
assert.Error(t, err, "Expected validation to fail")
assert.Contains(t, err.Error(), "execSync", "Error should mention execSync")
} else {
assert.NoError(t, err, "Expected validation to pass")
}
})
}
}

func TestValidateNoGitHubScriptGlobals_NodeJSMode(t *testing.T) {
tests := []struct {
name string
scriptName string
content string
mode RuntimeMode
expectError bool
}{
{
name: "Node.js mode with core.* should fail",
scriptName: "test_script",
content: `
const fs = require("fs");
core.info("This is a message");
`,
mode: RuntimeModeNodeJS,
expectError: true,
},
{
name: "Node.js mode with exec.* should fail",
scriptName: "test_script",
content: `
const fs = require("fs");
await exec.exec("ls -la");
`,
mode: RuntimeModeNodeJS,
expectError: true,
},
{
name: "Node.js mode with github.* should fail",
scriptName: "test_script",
content: `
const fs = require("fs");
const repo = github.context.repo;
`,
mode: RuntimeModeNodeJS,
expectError: true,
},
{
name: "Node.js mode without GitHub Actions globals should pass",
scriptName: "test_script",
content: `
const fs = require("fs");
const data = fs.readFileSync("file.txt");
console.log("Processing data");
`,
mode: RuntimeModeNodeJS,
expectError: false,
},
{
name: "GitHub Script mode with core.* should pass (not checked)",
scriptName: "test_script",
content: `
core.info("This is a message");
core.setOutput("result", "value");
`,
mode: RuntimeModeGitHubScript,
expectError: false,
},
{
name: "Node.js mode with GitHub Actions globals in comment should pass",
scriptName: "test_script",
content: `
// Don't use core.info in Node.js scripts
console.log("Use console.log instead");
`,
mode: RuntimeModeNodeJS,
expectError: false,
},
{
name: "Node.js mode with type reference should pass",
scriptName: "test_script",
content: `
/// <reference types="@actions/github-script" />
const fs = require("fs");
`,
mode: RuntimeModeNodeJS,
expectError: false,
},
{
name: "Node.js mode with multiple GitHub Actions globals should fail",
scriptName: "test_script",
content: `
const fs = require("fs");
core.info("Message");
exec.exec("ls");
const repo = github.context.repo;
`,
mode: RuntimeModeNodeJS,
expectError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateNoGitHubScriptGlobals(tt.scriptName, tt.content, tt.mode)
if tt.expectError {
assert.Error(t, err, "Expected validation to fail")
} else {
assert.NoError(t, err, "Expected validation to pass")
}
})
}
}

func TestScriptRegistry_RegisterWithMode_Validation(t *testing.T) {
t.Run("GitHub Script mode with execSync should panic", func(t *testing.T) {
registry := NewScriptRegistry()
invalidScript := `
const { execSync } = require("child_process");
execSync("ls -la");
`
assert.Panics(t, func() {
registry.RegisterWithMode("invalid_script", invalidScript, RuntimeModeGitHubScript)
}, "Should panic when registering GitHub Script with execSync")
})

t.Run("Node.js mode with GitHub Actions globals should panic", func(t *testing.T) {
registry := NewScriptRegistry()
invalidScript := `
const fs = require("fs");
core.info("This should not be here");
`
assert.Panics(t, func() {
registry.RegisterWithMode("invalid_script", invalidScript, RuntimeModeNodeJS)
}, "Should panic when registering Node.js script with GitHub Actions globals")
})

t.Run("Valid GitHub Script mode should not panic", func(t *testing.T) {
registry := NewScriptRegistry()
validScript := `
const { exec } = require("@actions/exec");
core.info("This is valid for GitHub Script mode");
`
assert.NotPanics(t, func() {
registry.RegisterWithMode("valid_script", validScript, RuntimeModeGitHubScript)
}, "Should not panic with valid GitHub Script")
})

t.Run("Valid Node.js mode should not panic", func(t *testing.T) {
registry := NewScriptRegistry()
validScript := `
const fs = require("fs");
const { execSync } = require("child_process");
console.log("This is valid for Node.js mode");
`
assert.NotPanics(t, func() {
registry.RegisterWithMode("valid_script", validScript, RuntimeModeNodeJS)
}, "Should not panic with valid Node.js script")
})
}
Loading