Skip to content

Commit d273c17

Browse files
Copilotgithub-actions[bot]lpcox
authored
fix: add parentheses to JSDoc type cast in copilot_driver.cjs for TypeScript compatibility (#25406)
* feat: increase logging in copilot driver for silent startup failures (#issue) (#25390) * feat(logging): add debug logging to 5 CLI files for improved troubleshooting (#25393) * fix: add parentheses to JSDoc type cast in copilot_driver.cjs for TypeScript compatibility Agent-Logs-Url: https://github.com/github/gh-aw/sessions/34f7e8b3-df09-41bc-b786-8bb4b22ebb7e Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
1 parent 7365f86 commit d273c17

7 files changed

Lines changed: 85 additions & 4 deletions

File tree

actions/setup/js/copilot_driver.cjs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"use strict";
2626

2727
const { spawn } = require("child_process");
28+
const fs = require("fs");
2829

2930
// Maximum number of retry attempts after the initial run
3031
const MAX_RETRIES = 3;
@@ -67,6 +68,29 @@ function sleep(ms) {
6768
return new Promise(resolve => setTimeout(resolve, ms));
6869
}
6970

71+
/**
72+
* Check whether a command path is accessible and executable, logging the result.
73+
* Returns true if the command is usable, false otherwise.
74+
* @param {string} command - Absolute or relative path to the executable
75+
* @returns {Promise<boolean>}
76+
*/
77+
async function checkCommandAccessible(command) {
78+
try {
79+
await fs.promises.access(command, fs.constants.F_OK);
80+
} catch {
81+
log(`pre-flight: command not found: ${command} (F_OK check failed — binary does not exist at this path)`);
82+
return false;
83+
}
84+
try {
85+
await fs.promises.access(command, fs.constants.X_OK);
86+
log(`pre-flight: command is accessible and executable: ${command}`);
87+
return true;
88+
} catch {
89+
log(`pre-flight: command exists but is not executable: ${command} (X_OK check failed — permission denied)`);
90+
return false;
91+
}
92+
}
93+
7094
/**
7195
* Format elapsed milliseconds as a human-readable string (e.g. "3m 12s").
7296
* @param {number} ms
@@ -148,7 +172,11 @@ function runProcess(command, args, attempt) {
148172

149173
child.on("error", err => {
150174
const durationMs = Date.now() - startTime;
151-
log(`attempt ${attempt + 1}: failed to start process '${command}': ${err.message}`);
175+
// prettier-ignore
176+
const errno = /** @type {NodeJS.ErrnoException} */ (err);
177+
const errCode = errno.code ?? "unknown";
178+
const errSyscall = errno.syscall ?? "unknown";
179+
log(`attempt ${attempt + 1}: failed to start process '${command}': ${err.message}` + ` (code=${errCode} syscall=${errSyscall})`);
152180
resolve({
153181
exitCode: 1,
154182
output: collectedOutput,
@@ -170,7 +198,9 @@ async function main() {
170198
process.exit(1);
171199
}
172200

173-
log(`starting: command=${command} maxRetries=${MAX_RETRIES} initialDelayMs=${INITIAL_DELAY_MS} backoffMultiplier=${BACKOFF_MULTIPLIER} maxDelayMs=${MAX_DELAY_MS}`);
201+
log(`starting: command=${command} maxRetries=${MAX_RETRIES} initialDelayMs=${INITIAL_DELAY_MS}` + ` backoffMultiplier=${BACKOFF_MULTIPLIER} maxDelayMs=${MAX_DELAY_MS}` + ` nodeVersion=${process.version} platform=${process.platform}`);
202+
203+
await checkCommandAccessible(command);
174204

175205
let delay = INITIAL_DELAY_MS;
176206
let lastExitCode = 1;
@@ -212,7 +242,7 @@ async function main() {
212242
if (attempt >= MAX_RETRIES) {
213243
log(`all ${MAX_RETRIES} retries exhausted — giving up (exitCode=${lastExitCode})`);
214244
} else {
215-
log(`attempt ${attempt + 1}: no output produced — not retrying (process may have failed to start)`);
245+
log(`attempt ${attempt + 1}: no output produced — not retrying` + ` (possible causes: binary not found, permission denied, auth failure, or silent startup crash)`);
216246
}
217247

218248
// Non-retryable error or retries exhausted — propagate exit code

actions/setup/js/copilot_driver.test.cjs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,4 +153,35 @@ describe("copilot_driver.cjs", () => {
153153
expect(logLine).toMatch(/^\[copilot-driver\] \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
154154
});
155155
});
156+
157+
describe("startup log includes node version and platform", () => {
158+
it("starting log line contains nodeVersion and platform fields", () => {
159+
const command = "/usr/local/bin/copilot";
160+
const startingLine = `starting: command=${command} maxRetries=3 initialDelayMs=5000` + ` backoffMultiplier=2 maxDelayMs=60000` + ` nodeVersion=${process.version} platform=${process.platform}`;
161+
expect(startingLine).toContain("nodeVersion=");
162+
expect(startingLine).toContain("platform=");
163+
expect(startingLine).toMatch(/nodeVersion=v\d+\.\d+/);
164+
});
165+
});
166+
167+
describe("no-output failure message", () => {
168+
it("includes actionable possible causes", () => {
169+
const msg = `attempt 1: no output produced — not retrying` + ` (possible causes: binary not found, permission denied, auth failure, or silent startup crash)`;
170+
expect(msg).toContain("binary not found");
171+
expect(msg).toContain("permission denied");
172+
expect(msg).toContain("auth failure");
173+
expect(msg).toContain("silent startup crash");
174+
});
175+
});
176+
177+
describe("error event message", () => {
178+
it("includes code and syscall fields", () => {
179+
const errMessage = "spawn /usr/local/bin/copilot ENOENT";
180+
const errCode = "ENOENT";
181+
const errSyscall = "spawn";
182+
const logMsg = `attempt 1: failed to start process '/usr/local/bin/copilot': ${errMessage}` + ` (code=${errCode} syscall=${errSyscall})`;
183+
expect(logMsg).toContain("code=ENOENT");
184+
expect(logMsg).toContain("syscall=spawn");
185+
});
186+
});
156187
});

pkg/cli/audit.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,7 @@ func auditJobRun(runID int64, jobID int64, stepNumber int, owner, repo, hostname
636636

637637
// extractStepOutput extracts the output of a specific step from job logs
638638
func extractStepOutput(jobLog string, stepNumber int) (string, error) {
639+
auditLog.Printf("Extracting output for step %d from job logs (%d bytes)", stepNumber, len(jobLog))
639640
lines := strings.Split(jobLog, "\n")
640641
var stepOutput []string
641642
inStep := false
@@ -662,14 +663,17 @@ func extractStepOutput(jobLog string, stepNumber int) (string, error) {
662663
}
663664

664665
if len(stepOutput) == 0 {
666+
auditLog.Printf("Step %d not found in job logs (scanned %d lines)", stepNumber, len(lines))
665667
return "", fmt.Errorf("step %d not found in job logs", stepNumber)
666668
}
667669

670+
auditLog.Printf("Extracted %d lines for step %d", len(stepOutput), stepNumber)
668671
return strings.Join(stepOutput, "\n"), nil
669672
}
670673

671674
// findFirstFailingStep finds the first step that failed in the job logs
672675
func findFirstFailingStep(jobLog string) (int, string) {
676+
auditLog.Printf("Searching for first failing step in job logs (%d bytes)", len(jobLog))
673677
lines := strings.Split(jobLog, "\n")
674678
var stepOutput []string
675679
inStep := false
@@ -700,9 +704,11 @@ func findFirstFailingStep(jobLog string) (int, string) {
700704
}
701705

702706
if foundFailure && len(stepOutput) > 0 {
707+
auditLog.Printf("Found failing step %d with %d lines of output", currentStep, len(stepOutput))
703708
return currentStep, strings.Join(stepOutput, "\n")
704709
}
705710

711+
auditLog.Print("No failing step found in job logs")
706712
return 0, ""
707713
}
708714

pkg/cli/deps_security.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ func querySecurityAdvisories(depVersions map[string]string, verbose bool) ([]Sec
134134
// GitHub Security Advisory API endpoint
135135
url := "https://api.github.com/advisories?ecosystem=go&per_page=100"
136136

137+
depsSecurityLog.Printf("Querying GitHub Security Advisory API: url=%s, dep_count=%d", url, len(depVersions))
137138
client := &http.Client{Timeout: 30 * time.Second}
138139
req, err := http.NewRequest(http.MethodGet, url, nil)
139140
if err != nil {
@@ -190,6 +191,7 @@ func querySecurityAdvisories(depVersions map[string]string, verbose bool) ([]Sec
190191
adv.PatchedVers = []string{vuln.FirstPatchedVersion}
191192
}
192193

194+
depsSecurityLog.Printf("Advisory matched dependency: package=%s, version=%s, severity=%s, id=%s", vuln.Package.Name, currentVersion, apiAdv.Severity, apiAdv.GHSAID)
193195
matchingAdvisories = append(matchingAdvisories, adv)
194196

195197
if verbose {

pkg/cli/firewall_policy.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ func findMatchingRule(entry AuditLogEntry, rules []PolicyRule) *PolicyRule {
271271
if isEntryAllowed(entry) {
272272
expectedAction = "allow"
273273
}
274+
firewallPolicyLog.Printf("Finding matching rule for host=%s, expected_action=%s, rules=%d", entry.Host, expectedAction, len(rules))
274275

275276
for i := range rules {
276277
rule := &rules[i]
@@ -283,6 +284,7 @@ func findMatchingRule(entry AuditLogEntry, rules []PolicyRule) *PolicyRule {
283284
// aclName "all" is a catch-all rule (typically the default deny)
284285
if rule.ACLName == "all" {
285286
if rule.Action == expectedAction {
287+
firewallPolicyLog.Printf("Matched catch-all rule (action=%s) for host=%s", rule.Action, entry.Host)
286288
return rule
287289
}
288290
continue
@@ -291,10 +293,12 @@ func findMatchingRule(entry AuditLogEntry, rules []PolicyRule) *PolicyRule {
291293
// Domain match
292294
if domainMatchesRule(entry.Host, *rule) {
293295
if rule.Action == expectedAction {
296+
firewallPolicyLog.Printf("Matched rule %s (action=%s) for host=%s", rule.ACLName, rule.Action, entry.Host)
294297
return rule
295298
}
296299
}
297300
}
301+
firewallPolicyLog.Printf("No matching rule found for host=%s", entry.Host)
298302
return nil
299303
}
300304

pkg/cli/mcp_safe_update_cache.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ func CollectLockFileManifests(workflowsDir string) map[string]*workflow.GHAWMani
5959
// WritePriorManifestFile serialises the manifest cache to a temporary JSON file and
6060
// returns its path. The caller is responsible for removing the file when done.
6161
func WritePriorManifestFile(cache map[string]*workflow.GHAWManifest) (string, error) {
62+
mcpLog.Printf("Writing prior manifest cache to temp file: %d entries", len(cache))
6263
data, err := json.Marshal(cache)
6364
if err != nil {
6465
return "", fmt.Errorf("marshal manifest cache: %w", err)
@@ -75,5 +76,6 @@ func WritePriorManifestFile(cache map[string]*workflow.GHAWManifest) (string, er
7576
return "", fmt.Errorf("write manifest cache file: %w", err)
7677
}
7778

79+
mcpLog.Printf("Prior manifest cache written to: %s (%d bytes)", f.Name(), len(data))
7880
return f.Name(), nil
7981
}

pkg/cli/workflows.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,11 @@ func suggestWorkflowNames(target string) []string {
242242
// Normalize target: strip .md extension and get basename if it's a path
243243
normalizedTarget := strings.TrimSuffix(filepath.Base(target), ".md")
244244

245+
workflowsLog.Printf("Suggesting workflow names for %q (available: %d)", normalizedTarget, len(availableNames))
245246
// Use the existing FindClosestMatches function from parser package
246-
return parser.FindClosestMatches(normalizedTarget, availableNames, 3)
247+
suggestions := parser.FindClosestMatches(normalizedTarget, availableNames, 3)
248+
workflowsLog.Printf("Found %d suggestion(s) for %q: %v", len(suggestions), normalizedTarget, suggestions)
249+
return suggestions
247250
}
248251

249252
// isWorkflowFile returns true if the file should be treated as a workflow file.
@@ -266,6 +269,8 @@ func getMarkdownWorkflowFiles(workflowDir string) ([]string, error) {
266269
workflowsDir = getWorkflowsDir()
267270
}
268271

272+
workflowsLog.Printf("Scanning for markdown workflow files in: %s", workflowsDir)
273+
269274
if _, err := os.Stat(workflowsDir); os.IsNotExist(err) {
270275
return nil, fmt.Errorf("no %s directory found", workflowsDir)
271276
}
@@ -279,6 +284,7 @@ func getMarkdownWorkflowFiles(workflowDir string) ([]string, error) {
279284
// Filter out README.md files
280285
mdFiles = filterWorkflowFiles(mdFiles)
281286

287+
workflowsLog.Printf("Found %d markdown workflow file(s) in %s", len(mdFiles), workflowsDir)
282288
return mdFiles, nil
283289
}
284290

0 commit comments

Comments
 (0)