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
22 changes: 11 additions & 11 deletions .github/workflows/smoke-copilot-aoai-entra.lock.yml

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

75 changes: 75 additions & 0 deletions actions/setup/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,75 @@ const path = require("path");
const { spawnSync } = require("child_process");
const fs = require("fs");

function isDebugModeEnabled() {
const toBool = (value) => {
const normalized = String(value || "").toLowerCase();
return normalized === "1" || normalized === "true";
};
return toBool(process.env.RUNNER_DEBUG) || toBool(process.env.ACTIONS_STEP_DEBUG);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] listTmpGhAwFiles (and isDebugModeEnabled) have no unit tests — edge-case bugs here will surface as silent debug-output gaps rather than test failures.

💡 Key cases to cover
Scenario Why it matters
tmpDir does not exist guard branch already in code — should be verified
permission-denied on a subdirectory readErrors counter path
exactly maxFiles entries off-by-one on the truncation flag
maxDepth = 0 walk should not recurse at all
RUNNER_DEBUG=1 vs RUNNER_DEBUG=true both should enable debug mode

Because the functions are not exported they would need a light refactor (e.g. module.exports = { listTmpGhAwFiles, isDebugModeEnabled } behind a test guard, or extract into a debug-inventory.js module) before a test file can import them.

function listTmpGhAwFiles(tmpDir, maxDepth, maxFiles) {
if (!fs.existsSync(tmpDir)) {
console.log(`[debug] ${tmpDir} does not exist; skipping file listing`);
return;
}

const files = [];
let readErrors = 0;

const walk = (currentDir, depth) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnose] The truncation signal only fires when maxFiles is hit, not when maxDepth silently caps recursion. A reader of the debug output has no way to know that subdirectories beyond depth 4 were skipped.

💡 Suggested fix

Track whether any directory was skipped due to depth, and emit a companion signal:

const walk = (currentDir, depth) => {
  if (depth >= maxDepth) {
    depthCapped = true;   // new flag
    return;
  }
  if (files.length >= maxFiles) {
    return;
  }
  // ...
};

Then after the walk:

if (depthCapped) {
  console.log(`[debug] some subdirectories beyond depth ${maxDepth} were not listed`);
}

This keeps the two truncation reasons distinct and both observable.

if (depth >= maxDepth || files.length >= maxFiles) {
return;
Comment on lines +33 to +35
}

let entries;
try {
entries = fs.readdirSync(currentDir, { withFileTypes: true });
} catch (err) {
readErrors += 1;
console.log(`[debug] failed to read ${currentDir}: ${err.message}`);
return;
}

entries.sort((a, b) => a.name.localeCompare(b.name));

for (const entry of entries) {
if (files.length >= maxFiles) {
return;
}

const fullPath = path.join(currentDir, entry.name);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/zoom-out] DFS-first traversal means the 200-file limit can be exhausted entirely inside a deep subdirectory, silently omitting all sibling files at shallower levels. For a debug inventory whose purpose is to show what was left before cleanup, breadth-first (or listing files before recursing) would give a more representative snapshot.

💡 Files-before-dirs alternative

A minimal change: process file entries in the current directory before recursing into subdirs.

// separate dirs and files
const dirs = entries.filter(e => e.isDirectory());
const fileEntries = entries.filter(e => !e.isDirectory());

for (const entry of fileEntries) {
  if (files.length >= maxFiles) return;
  files.push(path.relative(tmpDir, path.join(currentDir, entry.name)));
}
for (const dir of dirs) {
  if (files.length >= maxFiles) return;
  walk(path.join(currentDir, dir.name), depth + 1);
}

This ensures the root-level (most actionable) files always appear, regardless of how many nested files exist.

if (entry.isDirectory()) {
walk(fullPath, depth + 1);
continue;
}

files.push(path.relative(tmpDir, fullPath) || ".");
}
};

walk(tmpDir, 0);

const truncated = files.length >= maxFiles;
console.log(
`[debug] listing files under ${tmpDir} (max depth ${maxDepth}, max files ${maxFiles})`,
);
if (files.length === 0) {
console.log("[debug] no files found");
} else {
for (const file of files) {
console.log(`[debug] - ${file}`);
}
}
if (truncated) {
console.log(`[debug] output truncated at ${maxFiles} files`);
}
if (readErrors > 0) {
console.log(`[debug] encountered ${readErrors} directory read error(s)`);
}
}

// Wrap everything in an async IIFE so that the OTLP span is fully sent before
// the cleanup deletes /tmp/gh-aw/ (which contains aw_info.json and otel.jsonl).
(async () => {
Expand All @@ -28,6 +97,12 @@ const fs = require("fs");
}

const tmpDir = "/tmp/gh-aw";
const maxDebugDepth = 4;
const maxDebugFiles = 200;

if (isDebugModeEnabled()) {
listTmpGhAwFiles(tmpDir, maxDebugDepth, maxDebugFiles);
}

console.log(`Cleaning up ${tmpDir}...`);

Expand Down
18 changes: 9 additions & 9 deletions pkg/workflow/action_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,18 +163,18 @@ func ResolveGhAwRef(ctx context.Context, ref string) (string, error) {
return ref, nil
}
resolverLog.Printf("Resolving --gh-aw-ref %q to commit SHA via GitHub API", ref)
apiPath := fmt.Sprintf("/repos/github/gh-aw/commits/%s", ref)
apiPath := "/repos/github/gh-aw/commits/" + ref
callCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
cmd := ExecGHContext(callCtx, "api", apiPath, "--jq", ".sha")
output, err := cmd.CombinedOutput()
if err != nil {
msg := strings.TrimSpace(string(output))
if msg != "" {
return "", fmt.Errorf("failed to resolve gh-aw ref %q to SHA: %s: %w", ref, msg, err)
cmd := ExecGHContext(callCtx, "api", apiPath, "--jq", ".sha")
output, err := cmd.CombinedOutput()
if err != nil {
msg := strings.TrimSpace(string(output))
if msg != "" {
return "", fmt.Errorf("failed to resolve gh-aw ref %q to SHA: %s: %w", ref, msg, err)
}
return "", fmt.Errorf("failed to resolve gh-aw ref %q to SHA: %w", ref, err)
}
return "", fmt.Errorf("failed to resolve gh-aw ref %q to SHA: %w", ref, err)
}
sha := strings.TrimSpace(string(output))
if !gitutil.IsValidFullSHA(sha) {
return "", fmt.Errorf("unexpected response resolving gh-aw ref %q: got %q (expected 40-char hex SHA)", ref, sha)
Expand Down
Loading