diff --git a/.github/workflows/issue-classifier.lock.yml b/.github/workflows/issue-classifier.lock.yml index 44eafee6c7..02134fb672 100644 --- a/.github/workflows/issue-classifier.lock.yml +++ b/.github/workflows/issue-classifier.lock.yml @@ -2204,7 +2204,7 @@ jobs: path: /tmp/gh-aw/aw_info.json if-no-files-found: warn - name: Run AI Inference - uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4 + uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v1 env: GH_AW_MCP_CONFIG: /tmp/gh-aw/mcp-config/mcp-servers.json GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt diff --git a/.github/workflows/release.lock.yml b/.github/workflows/release.lock.yml index 658d3b9a1d..0be3614def 100644 --- a/.github/workflows/release.lock.yml +++ b/.github/workflows/release.lock.yml @@ -6017,13 +6017,13 @@ jobs: - name: Download Go modules run: go mod download - name: Generate SBOM (SPDX format) - uses: anchore/sbom-action@43a17d6e7add2b5535efe4dcae9952337c479a93 # v0.20.11 + uses: anchore/sbom-action@43a17d6e7add2b5535efe4dcae9952337c479a93 # v0.20.10 with: artifact-name: sbom.spdx.json format: spdx-json output-file: sbom.spdx.json - name: Generate SBOM (CycloneDX format) - uses: anchore/sbom-action@43a17d6e7add2b5535efe4dcae9952337c479a93 # v0.20.11 + uses: anchore/sbom-action@43a17d6e7add2b5535efe4dcae9952337c479a93 # v0.20.10 with: artifact-name: sbom.cdx.json format: cyclonedx-json @@ -6227,7 +6227,7 @@ jobs: fetch-depth: 0 persist-credentials: false - name: Release with gh-extension-precompile - uses: cli/gh-extension-precompile@9e2237c30f869ad3bcaed6a4be2cd43564dd421b # v2.1.0 + uses: cli/gh-extension-precompile@9e2237c30f869ad3bcaed6a4be2cd43564dd421b # v2 with: build_script_override: scripts/build-release.sh go_version_file: go.mod diff --git a/.github/workflows/stale-repo-identifier.lock.yml b/.github/workflows/stale-repo-identifier.lock.yml index 144161022a..eee179e885 100644 --- a/.github/workflows/stale-repo-identifier.lock.yml +++ b/.github/workflows/stale-repo-identifier.lock.yml @@ -174,7 +174,7 @@ jobs: ORGANIZATION: ${{ env.ORGANIZATION }} id: stale-repos name: Run stale_repos tool - uses: github/stale-repos@a21e55567b83cf3c3f3f9085d3038dc6cee02598 # v3.0.2 + uses: github/stale-repos@a21e55567b83cf3c3f3f9085d3038dc6cee02598 # v3 - env: INACTIVE_REPOS: ${{ steps.stale-repos.outputs.inactiveRepos }} name: Save stale repos output diff --git a/.github/workflows/super-linter.lock.yml b/.github/workflows/super-linter.lock.yml index 5207834200..4d0e7e71fe 100644 --- a/.github/workflows/super-linter.lock.yml +++ b/.github/workflows/super-linter.lock.yml @@ -6146,7 +6146,7 @@ jobs: persist-credentials: false - name: Super-linter id: super-linter - uses: super-linter/super-linter@47984f49b4e87383eed97890fe2dca6063bbd9c3 # v8.3.1 + uses: super-linter/super-linter@47984f49b4e87383eed97890fe2dca6063bbd9c3 # v8.2.1 env: CREATE_LOG_FILE: "true" DEFAULT_BRANCH: main diff --git a/pkg/workflow/main_export_validation_test.go b/pkg/workflow/main_export_validation_test.go new file mode 100644 index 0000000000..6f569bc65e --- /dev/null +++ b/pkg/workflow/main_export_validation_test.go @@ -0,0 +1,102 @@ +package workflow + +import ( + "os" + "path/filepath" + "regexp" + "strings" + "testing" +) + +// TestScriptsExportMain validates that all JavaScript files that are required +// with `const { main } = require(...)` actually export a main function. +// This prevents runtime errors where code tries to destructure main from +// a module that doesn't export it. +func TestScriptsExportMain(t *testing.T) { + // List of scripts that should export main based on compiler usage + // These are extracted from places where Go code generates: + // const { main } = require('...') + requiredMainExports := []string{ + "check_stop_time.cjs", + "check_skip_if_match.cjs", + "check_command_position.cjs", + "check_workflow_timestamp_api.cjs", + "compute_text.cjs", + "add_reaction_and_edit_comment.cjs", + "lock-issue.cjs", + "unlock-issue.cjs", + "checkout_pr_branch.cjs", + } + + jsDir := "js" + + // Pattern to match: module.exports = { main }; or module.exports = { main, ... }; + mainExportPattern := regexp.MustCompile(`module\.exports\s*=\s*\{[^}]*\bmain\b[^}]*\}`) + + for _, scriptName := range requiredMainExports { + t.Run(scriptName, func(t *testing.T) { + scriptPath := filepath.Join(jsDir, scriptName) + content, err := os.ReadFile(scriptPath) + if err != nil { + t.Fatalf("Failed to read %s: %v", scriptPath, err) + } + + scriptContent := string(content) + + // Check if the script exports main + if !mainExportPattern.MatchString(scriptContent) { + t.Errorf("Script %s is required with 'const { main } = require(...)' but does not export main.\n"+ + "Add 'module.exports = { main };' to the script.", scriptName) + } + + // Also verify that an async function main exists + if !strings.Contains(scriptContent, "async function main()") && + !strings.Contains(scriptContent, "async function main ()") && + !strings.Contains(scriptContent, "function main()") && + !strings.Contains(scriptContent, "function main ()") { + t.Errorf("Script %s exports main but does not define a main function", scriptName) + } + }) + } +} + +// TestScriptsWithMainExportPattern checks that scripts exporting main +// follow the correct pattern and include the require.main check for direct execution +func TestScriptsWithMainExportPattern(t *testing.T) { + jsDir := "js" + + // Pattern to match: module.exports = { main } + mainExportPattern := regexp.MustCompile(`module\.exports\s*=\s*\{[^}]*\bmain\b[^}]*\}`) + + // Pattern to check for require.main === module check + requireMainPattern := regexp.MustCompile(`require\.main\s*===\s*module`) + + entries, err := os.ReadDir(jsDir) + if err != nil { + t.Fatalf("Failed to read js directory: %v", err) + } + + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".cjs") { + continue + } + + scriptPath := filepath.Join(jsDir, entry.Name()) + content, err := os.ReadFile(scriptPath) + if err != nil { + continue // Skip files we can't read + } + + scriptContent := string(content) + + // If script exports main, it should have proper execution guard + if mainExportPattern.MatchString(scriptContent) { + t.Run(entry.Name()+"_has_execution_guard", func(t *testing.T) { + if !requireMainPattern.MatchString(scriptContent) { + t.Logf("Script %s exports main but lacks 'if (require.main === module)' guard for direct execution.\n"+ + "This is acceptable if the script is only meant to be used as a module.", entry.Name()) + } + }) + } + } +} diff --git a/scripts/changeset.js b/scripts/changeset.js index 5461c02914..5e6d541d52 100755 --- a/scripts/changeset.js +++ b/scripts/changeset.js @@ -732,4 +732,4 @@ async function main() { } } -main(); +module.exports = { main }; diff --git a/scripts/changeset.test.js b/scripts/changeset.test.js index a39febd857..27c6fe4307 100755 --- a/scripts/changeset.test.js +++ b/scripts/changeset.test.js @@ -77,8 +77,9 @@ function runChangesetVersion() { execSync('git config user.email "test@example.com"', { cwd: TEST_DIR, stdio: 'ignore' }); execSync('git config user.name "Test User"', { cwd: TEST_DIR, stdio: 'ignore' }); - // Run version command - const output = execSync(`node "${CHANGESET_SCRIPT}" version`, { + // Run version command by requiring and calling main + // Need to set up process.argv properly: [node, scriptname, command, ...] + const output = execSync(`node -e "process.argv.splice(1, 0, 'changeset.js', 'version'); const {main} = require('${CHANGESET_SCRIPT}'); main().catch(console.error);"`, { cwd: TEST_DIR, encoding: 'utf8', env: { ...process.env, GH_AW_CURRENT_VERSION: 'v0.1.0' }