Skip to content

Include grader evaluator scripts in workflow packages - #56268

Open
pelikhan with Copilot wants to merge 14 commits into
mainfrom
copilot/fix-gh-aw-add-omit-evaluator-files
Open

Include grader evaluator scripts in workflow packages#56268
pelikhan with Copilot wants to merge 14 commits into
mainfrom
copilot/fix-gh-aw-add-omit-evaluator-files

Conversation

Copilot AI commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Packaged workflows omitted evaluator scripts referenced by graders.*.run, causing installed workflows to fail compilation in clean repositories.

  • Package resolution

    • Parse grader configuration from packaged workflows.
    • Add each validated run target to package resources at its repository-relative path.
    • Deduplicate evaluator resources across workflows.
  • Package lifecycle

    • Route evaluator scripts through existing package resource installation and ownership handling.
    • Allow gh aw update to update or restore missing evaluators.
graders:
  operational-value:
    run: .github/graders/example-operational-value.sh

Run: https://github.com/github/gh-aw/actions/runs/33081464762

Generated by 👨‍🍳 PR Sous Chef · pi · gpt54 · 18.6 AIC · ⌖ 8.23 AIC · ⊞ 8.7K ·
Comment /souschef to run again

Copilot AI and others added 2 commits August 27, 2026 06:50
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix gh aw add to include grader evaluator files Include grader evaluator scripts in workflow packages Aug 27, 2026
Copilot AI requested a review from pelikhan August 27, 2026 06:58
@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot shell scripts should already be parsed, checked and added to resources

Copilot AI and others added 3 commits August 27, 2026 07:22
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

@copilot shell scripts should already be parsed, checked and added to resources

Addressed in 37675b8. Grader evaluator paths now go through the existing validated workflow resource extraction/fetch pipeline; the package-specific parser was removed. Tests cover deduplication, repository-relative installation, restoration, and clean compilation.

@github-actions

Copy link
Copy Markdown
Contributor

Category: bug | Risk: medium | Score: 54 (impact 28 + urgency 16 + quality 10) | Action: batch_review | Batch: workflow-runtime | Note: Workflow package resource regression fix.

Generated by 🔧 PR Triage Agent · copilot · mai10 · 17.8 AIC · ⌖ 2.49 AIC · ⊞ 16.6K ·

Copilot AI left a comment

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.

Pull request overview

Adds grader evaluator discovery and installation for packaged workflows.

Changes:

  • Extracts validated grader evaluator paths as resources.
  • Installs evaluators repository-relative and deduplicates paths.
  • Adds evaluator installation, restoration, and compilation tests.
Show a summary per file
File Description
pkg/cli/resources.go Discovers and installs grader evaluator resources.
pkg/cli/remote_workflow_test.go Tests evaluator extraction and installation.

Review details

Suppressed comments (1)

pkg/cli/resources.go:226

  • When force is true for an existing evaluator, the file is overwritten before tracker.TrackModified runs at line 268. TrackModified therefore captures the downloaded bytes rather than the original file, so a later package-install failure cannot roll back the user's previous evaluator. Track existing files before os.WriteFile, as the dispatch and call-workflow paths do.
		if fileExists && !force && isGraderEvaluator {
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread pkg/cli/resources.go Outdated
Comment on lines +55 to +58
if graders != nil {
for _, grader := range graders.Graders {
if grader != nil && grader.Run != "" {
paths = append(paths, grader.Run)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 0c7f570. Grader evaluator paths are now added to resolvedRepositoryPackage.ResourceFiles, so package ownership and syncManifestManagedResources reconcile them, including dedupe and update/restore behavior.

Comment thread pkg/cli/resources.go Outdated
continue
}
if rel, relErr := filepath.Rel(absTargetDir, absTargetPath); relErr != nil || strings.HasPrefix(rel, "..") {
if rel, relErr := filepath.Rel(absTargetBase, absTargetPath); relErr != nil || strings.HasPrefix(rel, "..") {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 0c7f570. Resource writes now use fileutil.ValidatePathWithinBase before writing, including package add/update resource paths, so symlinked parents cannot redirect writes outside the repository.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot why is any of these changes needed?

@github-actions

Copy link
Copy Markdown
Contributor

Comment Memory

reviewed_at: 2026-08-27T00:00:00Z
review_event: REQUEST_CHANGES
top_themes:
  - grader evaluator writes are still vulnerable to symlink escape on force updates
  - grader evaluator files are not tracked with package ownership and can be clobbered across packages
files_reviewed:
  - pkg/cli/resources.go
  - pkg/cli/remote_workflow_test.go
comment_count: 2

Note

This comment is managed by comment memory.

It stores persistent context for this thread in the code block at the top of this comment.
Edit only the text inside the backtick fences; workflow metadata and the footer are regenerated automatically.

Learn more about comment memory

🔎 Code quality review by PR Code Quality Reviewer · pi · gpt54 · 24.1 AIC · ⌖ 8.3 AIC · ⊞ 4.6K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

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.

Request changes

The new grader-evaluator packaging path introduces one filesystem-safety regression and one package-ownership regression that can both break downstream installs and updates.

Blocking themes
  • repo-root evaluator writes are guarded only by lexical path checks, so a symlinked .github/graders path can still redirect a forced update outside the repository
  • evaluator files are treated as ad hoc shared assets instead of manifest-managed package resources, so different packages can silently clobber each other's pinned evaluator script on later updates

I left inline comments on the exact sites.

🔎 Code quality review by PR Code Quality Reviewer · pi · gpt54 · 24.1 AIC · ⌖ 8.3 AIC · ⊞ 4.6K
Comment /review to run again

Comment thread pkg/cli/resources.go Outdated
continue
}
if rel, relErr := filepath.Rel(absTargetDir, absTargetPath); relErr != nil || strings.HasPrefix(rel, "..") {
if rel, relErr := filepath.Rel(absTargetBase, absTargetPath); relErr != nil || strings.HasPrefix(rel, "..") {

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.

This path check is not sufficient for a force update: filepath.Abs/filepath.Rel stay purely lexical, so an existing .github/graders symlink can still redirect the write outside the repository root and --force will happily overwrite the symlink target.

💡 Why this blocks merge

The new grader-evaluator path now writes to the repo root specifically so gh aw update can restore missing evaluators, but that also means the safety boundary has to be the real filesystem path, not just a cleaned string path.

Today the code does:

absTargetBase, _ := filepath.Abs(targetBaseDir)
absTargetPath, _ := filepath.Abs(targetPath)
rel, _ := filepath.Rel(absTargetBase, absTargetPath)

That never resolves symlinks. If .github/graders or one of its parents is a symlink, targetPath still looks like it lives under the repo while the eventual os.WriteFile follows the link and writes somewhere else. This is especially bad on the force=true path because the later conflict checks are skipped entirely.

Please resolve both the base and destination with filepath.EvalSymlinks (or reject symlinked parents outright) before writing, and add a test that plants a symlinked .github/graders directory and proves the write is refused.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 0c7f570. The lexical path check was replaced with the repository symlink-aware path validator, and a symlinked-parent regression test now proves the write is refused.

Comment thread pkg/cli/resources.go Outdated
}
continue
}
if fileExists && !force && isGraderEvaluator {

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.

The duplicate-evaluator shortcut only compares file bytes, so two different packages can silently share the same .github/graders/... path and then overwrite each other on the next update without any ownership/drift check.

💡 Why this blocks merge

This change intentionally treats grader scripts as shared repo-root assets:

if fileExists && !force && isGraderEvaluator {
    existingContent, _ := os.ReadFile(targetPath)
    if bytes.Equal(existingContent, fileContent) {
        continue
    }
}

That avoids an add-time conflict, but it also means the file is never tied back to the package that installed it. Once package A and package B both reference the same path with identical initial content, a later gh aw update for either package can replace the script with new content under force=true even if the other package still owns that file. The old package will now compile against a different evaluator than the one it pinned, and there is no ownership record to stop or even detect that drift.

This needs package-level ownership for grader evaluators (the same resolvedRepositoryPackage.ResourceFiles / manifest-managed path used for other package resources), or the feature will produce cross-package clobbering bugs as soon as two packages reuse the same evaluator path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 0c7f570. Evaluators are now package ResourceFiles, so they are installed and reconciled through package ownership instead of ad hoc per-workflow dependency writes.

@github-actions github-actions Bot left a comment

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.

One small simplification seems worth cutting. net: -1 lines possible.

Generated by ✂️ Ponytail Reviewer for #56268 · codex · mai10 · 7.44 AIC · ⌖ 1.99 AIC · ⊞ 16.7K
Comment /ponytail to run again

Comment thread pkg/cli/resources.go
)

// extractResources extracts file paths from the top-level "resources" frontmatter field.
var downloadResourceFileFromGitHub = parser.DownloadFileFromGitHub

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.

pkg/cli/resources.go:L21: yagni: package-level downloader seam for one test. Call parser.DownloadFileFromGitHub directly and use a tiny helper only if you really need a seam.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No code change for this one. I kept the downloader seam because these resource-fetch tests need deterministic no-network content, and this file already follows package-level downloader seam patterns used elsewhere in package resolution tests.

@github-actions github-actions Bot left a comment

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.

Good addition overall — the happy-path coverage is solid and the deduplication logic is clean.

Two non-blocking issues worth addressing:

  1. Fragile grader-path heuristicisGraderEvaluator is re-derived from a path prefix string at download time. A grader pointing outside .github/graders/ silently misbehaves. See inline comment on resources.go.

  2. Disabled graders still install their scripts — paths from enabled: false graders are collected and downloaded. See inline comment.

The existing review comments from the earlier round (ownership gap, symlink traversal) are also worth addressing before merge.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · copilot · sonnet46 · 30.7 AIC · ⌖ 9.2 AIC · ⊞ 6.2K

Comment thread pkg/cli/resources.go
paths = v
}

// Reject entries that contain GitHub Actions expression syntax — macros are not allowed.

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.

The isGraderEvaluator flag is inferred from a path-prefix string (constants.GithubDir+"graders/") rather than from how the path entered the list. A grader whose run: points outside .github/graders/ silently falls through to workflow-relative path resolution, producing a wrong install path with no warning.

Consider tagging origins at collection time — e.g. a small {path string; isGrader bool} struct — so the flag is structurally guaranteed rather than re-derived from the path string.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in a1405fb and 0c7f570. Resource extraction now carries an isGraderEvaluator origin flag from the parsed grader entry, so routing no longer depends on a .github/graders path-prefix convention.

Comment thread pkg/cli/resources.go
}
case []string:
paths = append(paths, v...)
}

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.

Disabled graders (enabled: false) still have their evaluator scripts fetched and installed here. A grader marked disabled signals that it should not run; downloading its script anyway is surprising and may cause unexpected conflicts if another workflow later ships a different version of the same file.

Either skip grader.Run collection when grader.Enabled == false, or add a comment explaining why installing a disabled grader's evaluator is intentional.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 0c7f570. I added an in-code comment explaining that disabled grader evaluator paths are intentionally included so package resources stay complete and gh aw update can restore them if re-enabled later.

Adds a design decision record explaining the decision to extend the
workflow package resource resolver to automatically include grader
evaluator scripts referenced in graders.*.run frontmatter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR exceeds the 100-line threshold for business logic changes (160 new lines in pkg/), triggering ADR enforcement.

No existing Architecture Decision Record was found in the PR body, on the branch, or in any linked issue. A draft ADR has been generated and committed to this branch:

📄 docs/adr/56268-include-grader-evaluator-scripts-in-workflow-packages.md


What was inferred from this PR

Section Evidence
Decision Extend extractResources to parse graders.*.run frontmatter and route evaluator scripts through the existing package resource installation lifecycle
Driver Packaged workflows failed compilation in clean repos because evaluator scripts referenced in graders.*.run were not included in the resource set
Top alternatives (1) Require manual duplication in resources: field — rejected as a footgun; (2) Treat missing evaluators as non-fatal warnings — rejected because it produces silent runtime failures
Consequences ✅ Clean-install workflows now compile; ✅ gh aw update restores evaluators; ⚠️ Dual path-resolution conventions in fetchAndSaveRemoteResources; ⚠️ Content comparison deferred until after download

Next steps for @pelikhan

  1. Review the draft ADR at docs/adr/56268-include-grader-evaluator-scripts-in-workflow-packages.md — verify that the decision, alternatives, and consequences reflect your intent.
  2. Edit any sections that are inaccurate or incomplete (mark any [TODO: verify] items).
  3. Change status from Draft to Accepted (or Proposed if team review is needed) once the content is finalized.
  4. Merge the PR once the ADR is in an accepted state.

This PR is blocked pending ADR review. The draft is already on the branch — no separate commit is needed.

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · claude · sonnet46 · 67.2 AIC · ⌖ 28.1 AIC · ⊞ 8.4K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

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.

Skills-Based Review 🧠

Applied /diagnosing-bugs, /tdd, and /codebase-design — requesting changes for correctness and test coverage gaps.

📋 Key Themes & Highlights

Key Themes

  • Missing conflict test: The content-conflict branch (resources.go:226-235) — the most security-sensitive new code path — has no test covering the error case.
  • Fragile re-classification: isGraderEvaluator is re-derived from the path prefix string inside fetchAndSaveRemoteResources, duplicating knowledge that is already authoritative in extractResources.
  • Variable shadowing risk: The err variable is reused in the content-equality branch without a distinct name, which is easy to accidentally break during refactors.
  • Missing rationale: Disabled graders' evaluators are silently included with no comment explaining why.

Positive Highlights

  • ✅ Clean factoring of deduplication via seen map in extractResources.
  • ✅ Good test coverage for the happy path (install + idempotent re-install + restore).
  • ✅ Repository-root resolution for grader paths (FindGitRootFrom) is correct and safe.
  • ✅ Existing comments from prior review (ResourceFiles registration, symlink safety) are important and should also be addressed.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · copilot · sonnet46 · 59.8 AIC · ⌖ 10.3 AIC · ⊞ 7.6K
Comment /matt to run again

Comment thread pkg/cli/resources.go
return nil, err
}
if graders != nil {
for _, grader := range graders.Graders {

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] Disabled graders' evaluators are silently included with no in-code rationale — this will surprise reviewers and future maintainers.

The test TestExtractResources_IncludesDisabledGraderEvaluator establishes the behaviour, but there is no comment explaining why disabled evaluators are included (e.g. so that gh aw update can restore them if the grader is later re-enabled).

💡 Suggested comment
// Include evaluator paths even for disabled graders so that
// package resources are complete: gh aw update can restore or
// update the script if the grader is later re-enabled.
for _, grader := range graders.Graders {

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 0c7f570. resources.go now documents why disabled grader evaluators are included in the package resource set.

Comment thread pkg/cli/resources.go Outdated
// ordinary resources remain relative to the source workflow directory.
var remoteFilePath string
if rest, ok := strings.CutPrefix(resourcePath, "/"); ok {
isGraderEvaluator := strings.HasPrefix(resourcePath, constants.GithubDir+"graders/")

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.

[/codebase-design] Hardcoded path prefix ".github/graders/" for grader detection is fragile and duplicates knowledge that already lives in GraderDefinition.Run validation.

If the graders config ever allows run paths outside .github/graders/ (or the prefix changes), this check will silently misclassify them. Consider deriving the isGraderEvaluator flag from a set built by extractResources itself when parsing graders, rather than re-inspecting the path string here.

💡 Alternative approach

Build a set of evaluator paths in extractResources and pass it alongside resourcePaths into fetchAndSaveRemoteResources, or return a struct that carries both the paths and their kind. This removes the re-classification by prefix.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in a1405fb and 0c7f570. Grader identity is propagated from extraction via extractedResource.isGraderEvaluator; there is no longer a hardcoded .github/graders detection prefix.

Comment thread pkg/cli/resources.go Outdated
}
continue
}
if fileExists && !force && isGraderEvaluator {

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.

[/diagnosing-bugs] The content-equality conflict check reads the existing file after the download is complete, meaning a download error will not be caught before the expensive I/O. More importantly, the err variable from downloadResourceFileFromGitHub is re-used in scope from an outer declaration (var err error is implicitly captured) — a new := inside the branch shadows it correctly, but this is easy to accidentally break during a refactor.

💡 Suggestion

Consider pre-reading the existing file before downloading (when fileExists && !force && isGraderEvaluator) so you can skip the download entirely when the file is present and a fast hash matches. At minimum, give the err variable here a distinct name to make the scope crystal-clear:

existingContent, readErr := os.ReadFile(targetPath)
if readErr != nil {
    return fmt.Errorf(...)
}

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 0c7f570. The existing-file read now uses a distinct readErr variable; the content comparison still downloads first because it needs the remote bytes to determine whether the local evaluator is identical.

require.NoError(t, compiler.CompileWorkflow(workflowPath))

require.NoError(t, os.Remove(installedPath))
evaluatorContent = []byte("#!/usr/bin/env bash\necho new\n")

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] The test verifies restore (force=true) but never tests the conflict case — when the file exists with different content and force=false. That branch (resources.go:226-235) returns a user-facing error and is the most security-sensitive path added in this PR.

💡 Suggested extra assertion or separate test
// Conflict: existing file differs from remote — should error without force.
require.NoError(t, os.WriteFile(installedPath, []byte("#!/usr/bin/env bash\necho different\n"), 0o644))
evaluatorContent = []byte("#!/usr/bin/env bash\necho new\n")
err := fetchAndSaveRemoteResources(t.Context(), content, spec, workflowsDir, false, false, nil)
require.ErrorContains(t, err, "already exists")

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 0c7f570. TestFetchAndSaveRemoteResources_InstallsAndRestoresGraderEvaluator now covers the existing-file/different-content/force=false conflict path and asserts the --force error.

@github-actions github-actions Bot left a comment

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.

Skills-Based Review 🧠

Applied /codebase-design and /tdd — requesting changes on efficiency and test coverage gaps.

📋 Key Themes & Highlights

Key Themes

  • Efficiency: gitutil.FindGitRootFrom is called per loop iteration; should be hoisted or lazily initialised once.
  • Ordering issue: network download happens before the existing-file read in the grader conflict check — the common idempotent case always pays for a network round-trip.
  • Fragile detection: isGraderEvaluator relies on a hardcoded path prefix rather than information from extractResources, creating a maintenance hazard.
  • Missing test: the conflict path (file exists, different content, force=false → error) has no coverage.

Positive Highlights

  • ✅ Clean separation of grader evaluators from ordinary resources in extractResources.
  • ✅ Deduplication logic is correct and well-placed.
  • ✅ Path-traversal boundary check extended correctly to both targetDir and gitRoot.
  • downloadResourceFileFromGitHub is made injectable for testing — a good testability decision.
  • ✅ Good integration test that exercises install, idempotent re-install, and force-restore end-to-end.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · copilot · sonnet46 · 51.2 AIC · ⌖ 10.5 AIC · ⊞ 7.6K
Comment /matt to run again

Comment thread pkg/cli/resources.go Outdated
targetPath := filepath.Join(targetDir, localRelPath)
targetBaseDir := targetDir
if isGraderEvaluator {
targetBaseDir, err = gitutil.FindGitRootFrom(targetDir)

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.

[/codebase-design] gitutil.FindGitRootFrom is called on every loop iteration for every grader evaluator, even though targetDir doesn't change. Move this call above the loop (or lazily initialise once) to avoid repeated filesystem traversals — especially relevant when a package includes multiple evaluators.

💡 Suggested fix
// Before the loop:
var gitRoot string

// Inside the loop, replacing the current block:
if isGraderEvaluator {
    if gitRoot == "" {
        gitRoot, err = gitutil.FindGitRootFrom(targetDir)
        if err != nil {
            return fmt.Errorf("failed to resolve repository root for grader resource %q: %w", resourcePath, err)
        }
    }
    targetBaseDir = gitRoot
    localRelPath = filepath.FromSlash(resourcePath)
}

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 0c7f570. The repository root lookup for workspace-relative grader resources is now lazily initialized once and reused across resource iterations.

Comment thread pkg/cli/resources.go Outdated
}
continue
}
if fileExists && !force && isGraderEvaluator {

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.

[/codebase-design] The conflict check downloads the file first (line 219) and only then reads the existing file to compare (line 227). This means a network request always fires even when the file exists and is unchanged — the common case for idempotent re-installs. The comment on line 192 says "conflict handling is deferred", but the download isn't deferred alongside it.

💡 Suggested approach

Read the existing file content before downloading, and skip the download entirely when fileExists && !force && isGraderEvaluator. Proceed to download only when you cannot determine staleness without the remote content — or accept the download but at least document why it cannot be avoided.

At minimum, rename or remove the misleading comment on line 192 so it only covers the write-conflict deferral, not the download.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 0c7f570. I clarified the existing-file branch and renamed the read error variable; the remote download remains necessary before equality comparison because the code must compare against the fetched evaluator bytes.

installed, err := os.ReadFile(installedPath)
require.NoError(t, err)
assert.Equal(t, evaluatorContent, installed)
require.NoError(t, fetchAndSaveRemoteResources(t.Context(), content, spec, workflowsDir, false, false, nil))

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] The test covers install, idempotent re-install (same content), and force-restore — but is missing the conflict case: when the file exists with different content and force=false, fetchAndSaveRemoteResources should return an error. That is the key new behaviour added in resources.go lines 226–234, and it has no test coverage.

💡 Suggested test addition

After the idempotent re-install assertion on line 1876, add:

// Conflict: file exists with different content, force=false should error.
evaluatorContent = []byte("#!/usr/bin/env bash\necho conflict\n")
err = fetchAndSaveRemoteResources(t.Context(), content, spec, workflowsDir, false, false, nil)
require.Error(t, err)
require.ErrorContains(t, err, evaluatorPath)
require.ErrorContains(t, err, "--force")

Without this, a future refactor could silently remove the conflict guard.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 0c7f570. The restore test now includes the non-force conflict case and verifies the evaluator path and --force guidance in the error.

Comment thread pkg/cli/resources.go Outdated
// ordinary resources remain relative to the source workflow directory.
var remoteFilePath string
if rest, ok := strings.CutPrefix(resourcePath, "/"); ok {
isGraderEvaluator := strings.HasPrefix(resourcePath, constants.GithubDir+"graders/")

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.

[/codebase-design] isGraderEvaluator is detected by a hardcoded path prefix (".github/graders/"). If workflow.ParseGradersFromFrontmatter ever normalises the run path (strips ./, resolves a symlink, or allows absolute paths), paths that are semantically grader evaluators will silently fall through to ordinary resource resolution.

💡 Suggested alternative

Propagate the distinction from extractResources rather than re-inferring it from the path string. For example, return a struct or two slices (regular resources and grader evaluator paths), so fetchAndSaveRemoteResources can use set-membership rather than a fragile prefix check. This also eliminates the coupling between path convention and routing logic.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in a1405fb and 0c7f570. fetchAndSaveRemoteResources now consumes grader-origin metadata from extractResourceEntries instead of re-inferring grader resources from a folder prefix.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot graders should use a relative to the GitHub workspace root (.github/workflows/graders/...) or a dot starter local file name (./graders/...). Do not enforce and use a folder naming conventions like "graders".

Copilot AI and others added 3 commits August 27, 2026 13:30
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

@copilot graders should use a relative to the GitHub workspace root (.github/workflows/graders/...) or a dot starter local file name (./graders/...). Do not enforce and use a folder naming convent...

Addressed in a1405fb, with follow-ups in 5a4b99d and 5116cab. graders.operational-value.run now accepts safe workspace-relative paths such as .github/workflows/graders/... and ./ workflow-local paths, without enforcing a .github/graders convention. Resource installation now uses the validated grader path mode and the symlink-aware path guard before writing.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot

I also attempted a branch refresh where allowed. After addressing the above, please run the pr-finisher skill.

Generated by 👨‍🍳 PR Sous Chef · pi · gpt54 · 18.6 AIC · ⌖ 8.23 AIC · ⊞ 8.7K ·
Comment /souschef to run again

Copilot AI and others added 2 commits August 27, 2026 14:44
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

@copilot

I also attempted a branch refresh where allowed. After addressing the above, please run the pr-finisher skill....

Ran pr-finisher.

  • ✅ Reviews — in-scope review comments were addressed or justified with direct replies. I could not mark review threads resolved from this environment because no thread-resolution mutation is available here.
  • ✅ Checks (local) — make agent-report-progress, make lint, focused grader/package tests, and prior-failure container-pin unit tests passed locally.
  • ❓ Checks (CI) — stale after agent pushes; no check runs are reported for latest head 8c75bc0. Prior CGO failure at 5be8a9c was in container-pin unit tests and passes locally now.
  • ❌ Mergeable — PR is open and not draft, but GitHub reports mergeable_state: blocked, likely pending human review/thread resolution and fresh CI.

Actions taken: added grader evaluators to package ResourceFiles, added package ownership/symlink-safe resource validation coverage, handled remaining review nits, and pushed commits 0c7f570 and 8c75bc0.

Hand-off: CI must be re-triggered by a maintainer before merge. Still needed: human review/approval and resolving review threads in the GitHub UI.

Copilot AI requested a review from gh-aw-bot August 27, 2026 14:51
@pelikhan

Copy link
Copy Markdown
Collaborator

/matt

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@github-actions

Copy link
Copy Markdown
Contributor

PR #56268 Review Summary

Applied /tdd, /codebase-design, and /diagnosing-bugs skills.

Status: COMMENT — no blocking issues, but 3 issues worth fixing:

  1. pkg/cli/resources.go:21downloadResourceFileFromGitHub package-level var mutation makes tests fragile; injection is the correct pattern.
  2. pkg/cli/resources.go:250 — grader evaluator conflict check triggers a download before comparing bytes; pre-download existence check is more efficient.
  3. pkg/workflow/graders_config.go:459strings.HasPrefix(part, "..") is subtly different from the old part == ".." guard; can reject legitimate ..-prefixed directory names.

Minor: isWorkspaceRelativeGraderEvaluator naming is misleading (line 165), and new tests should document why t.Parallel() is excluded.

@copilot please address the review comments above.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · copilot · sonnet46 · 95.7 AIC · ⌖ 10.8 AIC · ⊞ 7.6K ·
Comment /matt to run again

@github-actions github-actions Bot left a comment

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.

Skills-Based Review 🧠

Applied /tdd, /codebase-design, and /diagnosing-bugs to the highest-impact files: resources.go, add_package_manifest_resolve.go, graders_config.go, and the new test additions.

Overall this is a well-structured fix with good test coverage. A few issues warrant attention before merge: a race condition in test infrastructure, a post-download conflict check that wastes a network round-trip, and a subtle discrepancy between the Go validator and the JSON schema pattern.

📋 Key Themes & Highlights

Key Themes

  • Race condition risk: downloadResourceFileFromGitHub is a package-level var mutated by three tests without t.Parallel() suppression — if any are run in parallel the mutation is unsafe.
  • Post-download conflict check: Grader evaluator conflict detection downloads the file first, then compares bytes. The network round-trip is wasted when the file already exists and --force was not passed.
  • ../foo.sh path bypass: IsValidOperationalValueEvaluatorRunPath uses strings.HasPrefix(part, "..") but does not reject the bare .. component, allowing a path like ../foo.sh.
  • Schema vs validator divergence: The JSON schema regex and Go validator encode subtly different sets of acceptable paths.

Positive Highlights

  • ✅ Disabled graders still trigger resource discovery — correct and well-reasoned
  • packageResourceDestinationKey normalises paths case-insensitively across OS conventions
  • ValidatePathWithinBase replaces the old filepath.Rel boundary check — symlink-aware upgrade is excellent
  • ✅ Conflict error message for grader evaluators is actionable (--force)

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · copilot · sonnet46 · 95.7 AIC · ⌖ 10.8 AIC · ⊞ 7.6K
Comment /matt to run again

Comment thread pkg/cli/resources.go
)

// extractResources extracts file paths from the top-level "resources" frontmatter field.
var downloadResourceFileFromGitHub = parser.DownloadFileFromGitHub

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] downloadResourceFileFromGitHub is a package-level var mutated by three tests; none call t.Parallel(), but this is a fragile contract — any future t.Parallel() addition causes a data race.

💡 Suggestion

Inject the function as a parameter or via a functional option instead of mutating a global:

func fetchAndSaveRemoteResources(ctx context.Context, content string, spec *WorkflowSpec,
    targetDir string, verbose bool, force bool, tracker *FileTracker,
    download func(ctx context.Context, owner, repo, path, ref string) ([]byte, error),
) error {

Tests can then pass a stub directly — no global mutation, fully parallel-safe.

@copilot please address this.

Comment thread pkg/cli/resources.go
}
continue
}
if fileExists && !force && resource.isGraderEvaluator {

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.

[/diagnosing-bugs] Grader evaluator conflict detection downloads the file before checking for a content mismatch. When fileExists && !force, the download has already happened — the network round-trip is wasted when the existing file differs.

💡 Suggestion

Read the existing file content eagerly (before the download) so you can skip the download entirely when the file already exists and --force is false:

if fileExists && !force && resource.isGraderEvaluator {
    existingContent, readErr := os.ReadFile(targetPath)
    if readErr == nil {
        // file readable — download only to compare; but consider returning early
        // with a "use --force" error instead and skipping the download altogether
    }
}

Alternatively, return the conflict error before downloading, matching the behaviour for non-grader resources.

@copilot please address this.

return strings.HasSuffix(pathForValidation, ".sh")
}

// parseOptionalFloat parses an optional float64 field from a map.

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.

[/diagnosing-bugs] IsValidOperationalValueEvaluatorRunPath uses strings.HasPrefix(part, "..") to reject traversal, but a bare .. component satisfies strings.HasPrefix("..", "..") — it is rejected. However, a component like ..foo would also be rejected, which is overly strict (valid directory names can start with ..). More critically, the comment says "components starting with .. are rejected" but the intent seems to be "the component .. exactly". The existing code is safe due to the symlink-aware ValidatePathWithinBase backstop, but the validator comment is misleading.

💡 Suggestion

Use an exact match instead of a prefix check to match the documented intent:

for part := range strings.SplitSeq(pathForValidation, "/") {
    if part == "" || part == "." || part == ".." {
        return false
    }
}

This is the same guard used in the old code and avoids silently rejecting legitimate directory names like ..files/.

@copilot please address this.

}

func appendPackageGraderEvaluatorResources(ctx context.Context, owner, repo, ref, host, packagePath string, resourceFiles []resolvedPackageResource, installationSources []resolvedPackageInstallable) ([]resolvedPackageResource, error) {
seen := make(map[string]string, len(resourceFiles))

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.

[/codebase-design] appendPackageGraderEvaluatorResources eagerly downloads every .md source file in installationSources to discover grader run paths. For a package with many workflows this is O(n) network calls during resolution — before a user has even confirmed gh aw add. The ADR acknowledges this cost but doesn't note the user-visible latency impact.

💡 Suggestion

Consider a two-phase approach: parse grader config from the already-downloaded manifest metadata where possible, and defer evaluator content downloading to the install step (where downloads already happen). If downloading at resolution time is intentional and necessary, add a log line so users see progress during slow resolutions:

addPackageManifestLog.Printf("resolving grader evaluators from %d workflow sources", len(installationSources))

@copilot please address this.

run: .github/workflows/graders/example-operational-value.sh
---

# Workflow

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] The three new TestFetchAndSaveRemoteResources_* tests mutate the package-level downloadResourceFileFromGitHub var. They're not marked t.Parallel() which prevents a data race today, but there's no explicit enforcement. A comment or (nolint/redacted):paralleltest is insufficient — a future contributor adding t.Parallel() to these tests won't know about the global dependency.

💡 Suggestion

Add a (nolint/redacted):paralleltest comment with an explicit reason on each test that mutates the global, or better — refactor to inject the dependency (see comment on resources.go:21). At minimum, add an internal comment in each test:

// NOTE: cannot call t.Parallel() — test mutates the downloadResourceFileFromGitHub package global.

@copilot please address this.

Comment thread pkg/cli/resources.go
// repository-relative run path.
var remoteFilePath string
if rest, ok := strings.CutPrefix(resourcePath, "/"); ok {
isWorkspaceRelativeGraderEvaluator := resource.isGraderEvaluator && !strings.HasPrefix(resourcePath, "./")

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.

[/codebase-design] isWorkspaceRelativeGraderEvaluator is determined by two disjoint conditions: resource.isGraderEvaluator and !strings.HasPrefix(resourcePath, "./"). This naming is misleading — ./graders/foo.sh is workspace-relative semantically, but the boolean is false for it. The intended meaning is "grader evaluator that uses a repo-root-anchored path" (as opposed to workflow-dir-relative). The current name makes the conditional block harder to reason about.

💡 Suggestion

Rename to something that captures the actual semantic:

isRepoRootAnchoredGraderEvaluator := resource.isGraderEvaluator && !strings.HasPrefix(resourcePath, "./")

And update the inline comment to match.

@copilot please address this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gh aw add omits grader evaluator files from packages

4 participants