Skip to content

compiler: auto-derive and warn on missing path: for cross-repo checkout entries#42257

Merged
pelikhan merged 6 commits into
mainfrom
copilot/compiler-require-path-for-checkout
Jun 30, 2026
Merged

compiler: auto-derive and warn on missing path: for cross-repo checkout entries#42257
pelikhan merged 6 commits into
mainfrom
copilot/compiler-require-path-for-checkout

Conversation

Copilot AI commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

When a checkout: block has a cross-repo entry without an explicit path:, the compiler was emitting GH_AW_CHECKOUT_PATH_N: "", causing safe-outputs handlers (e.g. push_to_pull_request_branch) to fail at runtime with "Repository not found in workspace."

Changes

  • pkg/workflow/checkout_path_validation.go — new validateCrossRepoCheckoutPaths validator that:

    • Auto-derives path: from the repo-name segment of owner/repo (e.g. githubnext/gh-aw-side-repogh-aw-side-repo) and mutates the CheckoutConfig before YAML generation, ensuring both the emitted actions/checkout step and GH_AW_CHECKOUT_PATH_N in the manifest are non-empty
    • Emits a deprecation warning directing authors to add an explicit path: to silence it
    • Skips dynamic ${{…}} expressions — cannot be resolved at compile time; common patterns like repository: ${{ github.repository }} (trusted pull_request_target checkout) must not warn
  • pkg/workflow/compiler_validators.go — wires validateCrossRepoCheckoutPaths into validateCoreToolConfiguration, before checkout step generation

# Before: missing path triggers empty GH_AW_CHECKOUT_PATH_0 → runtime failure
checkout:
  - repository: githubnext/gh-aw-side-repo
    token: ${{ secrets.TEMP_USER_PAT }}
    fetch: ["*"]
    fetch-depth: 0

# After: compiler auto-derives path and emits deprecation warning;
# explicit path: silences the warning
checkout:
  - repository: githubnext/gh-aw-side-repo
    path: gh-aw-side-repo   # ← add this
    token: ${{ secrets.TEMP_USER_PAT }}
    fetch: ["*"]
    fetch-depth: 0

…issing path

Co-authored-by: dsyme <7204669+dsyme@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix compiler to require path for cross-repo checkout entries compiler: auto-derive and warn on missing path: for cross-repo checkout entries Jun 29, 2026
Copilot AI requested a review from dsyme June 29, 2026 14:34
Copilot finished work on behalf of dsyme June 29, 2026 14:34
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR Triage — §28395315609

Field Value
Category bug
Risk medium
Score 47 / 100
Action batch_review
Batch

Score breakdown: impact 25 + urgency 10 + quality 12

Compiler fix: auto-derives path: for cross-repo checkout entries missing it, fixing runtime "Repository not found in workspace" failures. 3 files (+292/-0). Draft, no CI yet. Correctly skips dynamic ${{...}} expressions. Promote from draft when ready.

Generated by 🔧 PR Triage Agent · 99.1 AIC · ⌖ 11.6 AIC · ⊞ 5.4K ·

@github-actions

Copy link
Copy Markdown
Contributor

👋 Nice fix for the missing path: issue in cross-repo checkout entries! The empty GH_AW_CHECKOUT_PATH_N at runtime is a real pain point and auto-deriving the path from the repo-name segment is the right call — paired with a deprecation warning, it gently nudges authors toward explicit config without breaking existing workflows.

The diff is clean and well-scoped across three files, the PR body is thorough (bug context, change summary, before/after YAML), and the 225-line test file in checkout_path_validation_test.go gives solid coverage. This looks ready for maintainer review. 🚀

Generated by ✅ Contribution Check · 138.5 AIC · ⌖ 13.8 AIC · ⊞ 6.3K ·

@pelikhan pelikhan marked this pull request as ready for review June 29, 2026 21:54
Copilot AI review requested due to automatic review settings June 29, 2026 21:54
@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

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

This PR updates the gh-aw workflow compiler to prevent empty checkout paths for cross-repository checkout: entries by auto-deriving a default path: value (and emitting a warning) when repository: is static and path: is omitted—avoiding downstream runtime failures in safe-outputs handlers that rely on workspace path lookup.

Changes:

  • Wire a new compiler validation pass to auto-derive missing path: values for static cross-repo checkout entries and emit a deprecation warning.
  • Add the new validateCrossRepoCheckoutPaths implementation (skipping ${{ ... }} repository expressions).
  • Add unit + light end-to-end validation tests covering derivation, warning counts, and manifest/checkout-step emission.
Show a summary per file
File Description
pkg/workflow/compiler_validators.go Wires the new cross-repo checkout path validation into the core tool validation flow.
pkg/workflow/checkout_path_validation.go Implements auto-derivation + warning emission for missing path: on static cross-repo checkouts.
pkg/workflow/checkout_path_validation_test.go Adds tests for derivation/warnings and verifies derived paths flow into manifest + checkout steps.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 3/3 changed files
  • Comments generated: 3
  • Review effort level: Low

Comment on lines +50 to +52
checkoutPathValidationLog.Printf("auto-deriving path %q for cross-repo checkout %q", repoName, cfg.Repository)
cfg.Path = repoName

Comment on lines +29 to +48
for _, cfg := range checkoutConfigs {
if cfg == nil || cfg.Repository == "" || cfg.Path != "" {
continue
}

// Dynamic expression: skip — cannot determine at compile time whether it
// targets a different repository (e.g. ${{ github.repository }} is the
// same repo checked out at a specific ref, which is a valid pattern for
// pull_request_target and should not trigger a warning).
if strings.Contains(cfg.Repository, "${{") {
continue
}

checkoutPathValidationLog.Printf("cross-repo checkout %q has no explicit path", cfg.Repository)

// Static repository slug: auto-derive path from the repository-name portion.
repoName := cfg.Repository
if slash := strings.LastIndex(cfg.Repository, "/"); slash >= 0 {
repoName = cfg.Repository[slash+1:]
}
Comment on lines +101 to +105
t.Run("warning message for static repo contains suggested path", func(t *testing.T) {
c := NewCompiler()
// Capture stderr by checking warning count and path derivation;
// the actual message text is validated by the path value and warning count.
configs := []*CheckoutConfig{
…paths

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

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (292 new lines in pkg/workflow/) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/42257-auto-derive-checkout-path-for-cross-repo-entries.md — review and complete it before merging.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff
  2. Complete the missing sections — add context the AI could not infer, refine the decision rationale, and list real alternatives you considered
  3. Commit the finalized ADR to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-42257: Auto-Derive and Warn on Missing path for Cross-Repo Checkout Entries

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I will deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 42257-auto-derive-checkout-path-for-cross-repo-entries.md for PR #42257).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 48.1 AIC · ⌖ 12.7 AIC · ⊞ 8.4K ·
Comment /review to run again

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

⚠️ Test Quality Score: 78/100 — Acceptable

Analyzed 18 test scenarios across 5 func Test* functions: 18 design tests, 0 implementation tests, 1 coding-guideline violation (missing //go:build build tag).

📊 Metrics & Test Classification (18 tests analyzed)
Metric Value
New/modified tests analyzed 18 scenarios across 5 func Test* functions
✅ Design tests (behavioral contracts) 18 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 3/5 functions with edge coverage (60%)
Duplicate test clusters 0
Test inflation detected YES — checkout_path_validation_test.go adds 225 lines vs 65 production (ratio 3.46:1)
🚨 Coding-guideline violations 1 (missing //go:build build tag on line 1)
Test File Classification Issues Detected
TestValidateCrossRepoCheckoutPaths (10 subtests) checkout_path_validation_test.go:13 ✅ Design ⚠️ ~6 assertions missing descriptive messages (lines 19, 35, 46, 56, 69, 71)
TestValidateCrossRepoCheckoutPathsViaCompiler (2 subtests) checkout_path_validation_test.go:116 ✅ Design
TestWarnCrossRepoPath_RepoNameExtraction (4 table rows) checkout_path_validation_test.go:167 ✅ Design ⚠️ Table-loop assertions missing messages (lines 179–180)
TestCrossRepoCheckoutPathAppearsInManifestStep (1 subtest) checkout_path_validation_test.go:188 ✅ Design
TestCrossRepoCheckoutPathAppearsInCheckoutStep checkout_path_validation_test.go:211 ✅ Design

Go: 1 (*_test.go); JavaScript: 0. Other languages: none detected.

⚠️ Flagged Issues — Requires Review (1 critical violation, 1 recommendation)

🚨 pkg/workflow/checkout_path_validation_test.go — Coding-guideline violation: missing //go:build tag

Line 1 reads package workflow, but every other test file in pkg/workflow/ begins with //go:build !integration or //go:build integration. This is a mandatory convention that gates which test suites include the file.

Suggested fix: Add //go:build !integration as line 1 (followed by a blank line), since these are fast offline unit tests:

(go/redacted):build !integration

package workflow

TestValidateCrossRepoCheckoutPaths / TestWarnCrossRepoPath_RepoNameExtraction⚠️ Assertion messages missing (guideline recommendation, not a hard failure)

At least 9 assertions lack a descriptive context string. Examples:

assert.Equal(t, 1, c.GetWarningCount())        // line 46 — no message
assert.Equal(t, "my-awesome-tool", configs[0].Path) // line 57 — no message
assert.Equal(t, tt.expectedPath, path)          // line 179 — no message
assert.Equal(t, 1, count)                       // line 180 — no message

The project guideline requires a descriptive string on every assertion so failures are self-explanatory without needing to map back to source lines.

Suggested fix: e.g. assert.Equal(t, 1, c.GetWarningCount(), "expected one warning for missing path field")

Verdict

Check failed. pkg/workflow/checkout_path_validation_test.go is missing the required //go:build !integration build tag on line 1. All peer test files in pkg/workflow/ include this tag. Please add //go:build !integration as the very first line of the file.

🧪 Test quality analysis by Test Quality Sentinel · 77.3 AIC · ⌖ 27.2 AIC · ⊞ 8.1K ·
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.

❌ Test Quality Sentinel: Coding-guideline violation detected. pkg/workflow/checkout_path_validation_test.go is missing the required //go:build !integration build tag on line 1. All peer test files in pkg/workflow/ include this tag. Please review the flagged files in the comment above.

@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.

Review: compiler: auto-derive and warn on missing path: for cross-repo checkout entries

Good fix for the GH_AW_CHECKOUT_PATH_N: "" runtime failure — the approach (auto-derive + deprecation warning + mutation before code generation) is sound, and the test coverage is thorough for the happy paths.

One blocking correctness issue plus two non-blocking suggestions:

Blocking

  • checkout_path_validation.go:46-51 — A trailing-slash slug like "owner/" produces repoName = "" after strings.LastIndex, which then sets cfg.Path = "" — the exact empty-path bug being fixed. A guard if repoName == "" { continue } is needed after the split.

Non-blocking

  • checkout_path_validation.go:28 — The validate* naming convention in this codebase implies read-only checks; the mutation is intentional but surprising. A brief doc note (or rename to fixupCrossRepoCheckoutPaths) would make the side-effect explicit.
  • checkout_path_validation_test.go:249 — The TestWarnCrossRepoPath_RepoNameExtraction table has no entry for "owner/" (trailing slash). Adding that case would lock in the guard and prevent regression.
What's correct ✅
  • Dynamic ${{ expressions skipped correctly — covers ${{ github.repository }} (pull_request_target trusted checkout).
  • Mutation happens before validatePushToPullRequestBranchWarnings, so derived path flows into downstream validators and generated YAML correctly.
  • nil entry guard prevents panics.
  • Integration test via validateWorkflowData verifies warning count + path derivation end-to-end.
  • GH_AW_CHECKOUT_PATH_N test confirms non-empty env var after auto-derivation.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 76.7 AIC · ⌖ 7.31 AIC · ⊞ 4.9K

Comments that could not be inline-anchored

pkg/workflow/checkout_path_validation.go:47

Empty derived path not guarded. When cfg.Repository has a trailing slash (e.g., &quot;owner/&quot;) — malformed but user-provided — strings.LastIndex returns the slash position and cfg.Repository[slash+1:] produces an empty string. The code then sets cfg.Path = &quot;&quot;, perpetuating the exact runtime failure this PR is fixing, and emits the confusing message The compiler has auto-derived path: &quot;&quot; from the repository name.

Add a guard after the slash split:

if slash := strings.LastIndex</details>

<details><summary>pkg/workflow/checkout_path_validation.go:28</summary>

**`validate*` naming convention violated by mutation.** This function is named `validateCrossRepoCheckoutPaths`, following the `validate*` convention for read-only checks, but it mutates `cfg.Path` as a side effect. All other functions in the `validations` table in `validateCoreToolConfiguration` are pure validators with no observable side effects on the input data.

Consider either:
1. Renaming to `fixupCrossRepoCheckoutPaths` (or `deriveCrossRepoCheckoutPaths`) to make the mutation explicit, …

</details>

<details><summary>pkg/workflow/checkout_path_validation_test.go:249</summary>

**Missing test for empty-derived-path edge case.** The table doesn&#39;t cover a trailing-slash repository slug like `&quot;owner/&quot;`. After `strings.LastIndex`, `repoName` would be `&quot;&quot;` and `cfg.Path` would be set to `&quot;&quot;` — the same empty-path bug being fixed. A regression test would ensure any future guard is not accidentally removed:

```go
// In TestWarnCrossRepoPath_RepoNameExtraction:
{&quot;owner/&quot;, &quot;&quot;}, // trailing slash: expect no mutation / skip

(This test would currently fail, confirming the g…

@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, /diagnose, and /zoom-out — requesting changes on test-coverage gaps and wording issues.

📋 Key Themes & Highlights

Key Issues

  • Misleading test (/tdd): "warning message for static repo contains suggested path" does not actually assert any message text — the comment even says "the actual message text is validated by the path value and warning count", which is incorrect. A future change that removes the path: hint from the warning text would pass this test.
  • Wrong semantics for deprecated (/diagnose): Calling brand-new auto-derivation behaviour "deprecated" confuses workflow authors. "Deprecated" implies a prior stable API being removed.
  • No strict-mode escalation (/zoom-out): validateSafeOutputsStepsShellExpansion demonstrates the c.strictMode guard pattern for turning warnings into errors. Without it here, there is no enforcement path and no test coverage for strict-mode behaviour.
  • Misleading helper comment (/tdd): warnCrossRepoPath is described as capturing warning message text, but its signature returns only (path, warnCount).
  • Missing table entry (/tdd): zero-slash repository ("just-repo-no-owner") is an untested fallback path.

Positive Highlights

  • ✅ Correct fix for the root cause — empty GH_AW_CHECKOUT_PATH_N was the real runtime failure
  • ✅ Dynamic ${{...}} expressions correctly skipped to avoid false positives on ${{ github.repository }}
  • ✅ Excellent integration tests: TestCrossRepoCheckoutPathAppearsInManifestStep and TestCrossRepoCheckoutPathAppearsInCheckoutStep confirm the downstream YAML effects
  • ✅ Validator placed correctly before validatePushToPullRequestBranchWarnings in the pipeline
  • ✅ Nil-safety guard in the loop prevents panics on malformed configs

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 80 AIC · ⌖ 8.14 AIC · ⊞ 6.6K
Comment /matt to run again

assert.Empty(t, configs[0].Path)
})

t.Run("warning message for static repo contains suggested path", func(t *testing.T) {

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] Test name promises "contains suggested path" but the body only checks path derivation and warnCount — the actual warning message text is never asserted.

If someone changes the warning to omit the suggested path: YAML snippet, this test would still pass. Consider either renaming the subtest to match what it actually checks, or add a real assertion on the message text.

💡 How to capture and assert stderr in Go

Capture os.Stderr with os.Pipe() before calling the validator:

oldStderr := os.Stderr
r, w, _ := os.Pipe()
os.Stderr = w

c.validateCrossRepoCheckoutPaths(configs, markdownPath)

w.Close()
os.Stderr = oldStderr
var buf bytes.Buffer
buf.ReadFrom(r)
msg := buf.String()

assert.Contains(t, msg, `path: widget-service`, "warning must include the suggested path line")

Alternatively, refactor the validator to return the warning string so the caller controls output — that also makes testing trivial.

@copilot please address this.

msg := strings.Join([]string{
fmt.Sprintf("checkout: repository %q has no explicit path: field.", cfg.Repository),
fmt.Sprintf("The compiler has auto-derived path: %q from the repository name.", repoName),
"This auto-derivation is deprecated. Add an explicit path: to silence this warning:",

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] "This auto-derivation is deprecated" is misleading — this behavior is being introduced here, not removed. "Deprecated" implies a prior stable contract is being withdrawn, which will confuse workflow authors who encounter this for the first time.

Consider rewording to set accurate expectations:

💡 Suggested rewording
"Explicit path: is strongly recommended. Auto-derivation may be enforced as an error in a future version.",

or if strict-mode enforcement is planned:

"Add an explicit path: field to silence this warning. Auto-derivation will become an error in a future release.",

This aligns with how other gh-aw warnings frame future enforcement (e.g. validateSafeOutputsStepsShellExpansion).

@copilot please address this.

" path: " + repoName,
}, "\n")
fmt.Fprintln(os.Stderr, formatCompilerMessage(markdownPath, "warning", msg))
c.IncrementWarningCount()

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] This validator always produces a warning but never an error. The existing validateSafeOutputsStepsShellExpansion validator shows the pattern for escalating to an error in strict mode:

if c.strictMode {
    return formatCompilerError(markdownPath, "error", err.Error(), err)
}

If the roadmap is to eventually require explicit path:, strict mode is the natural enforcement lever — and the warning message already says the behavior "is deprecated" (see comment above on that wording). Without strict-mode gating, there is no enforcement path and no test for it.

💡 Suggested structure
if c.strictMode {
    // In strict mode, missing path: is an error — do not auto-derive.
    errMsg := fmt.Sprintf(
        "checkout: repository %q requires an explicit path: field (strict mode)",
        cfg.Repository,
    )
    return // or accumulate errors
}
// Warning path (non-strict): auto-derive and warn.
cfg.Path = repoName
...
c.IncrementWarningCount()

Even if strict mode enforcement is not planned right now, a comment documenting the intentional omission would help future contributors.

@copilot please address this.

{"owner/repo", "repo"},
{"github/copilot", "copilot"},
{"githubnext/gh-aw-side-repo", "gh-aw-side-repo"},
{"a/b/c", "c"}, // only the last segment

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 table tests cover owner/repo, multi-slash a/b/c, and similar — but not the zero-slash case: Repository: "just-repo-no-owner".

The code falls back to using the full string when strings.LastIndex returns -1, which means path = "just-repo-no-owner". Is that the intended behaviour? It is untested and undocumented. Even if owner/repo is the only valid GitHub slug format, a defensive test documents the fallback contract:

💡 Suggested table entry
{"just-repo-no-owner", "just-repo-no-owner"}, // no slash: full string used as path

@copilot please address this.

}
}

// warnCrossRepoPath is a helper used by tests that need to capture the warning message text.

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 helper comment says it is "for tests that need to capture the warning message text", but its return signature (path string, warnCount int) captures no message text at all. The comment sets a false expectation.

Either update the comment to match the actual return values (path and warnCount), or extend the helper to also return the captured stderr string — which would unblock the message-text assertions noted in the "warning message for static repo..." test above.

💡 Extended helper that captures stderr
func warnCrossRepoPath(t *testing.T, repository string) (path, stderrMsg string, warnCount int) {
    t.Helper()
    r, w, _ := os.Pipe()
    orig := os.Stderr
    os.Stderr = w
    defer func() { os.Stderr = orig }()

    c := NewCompiler()
    cfg := &CheckoutConfig{Repository: repository}
    c.validateCrossRepoCheckoutPaths([]*CheckoutConfig{cfg}, "test.md")
    w.Close()

    var buf bytes.Buffer
    buf.ReadFrom(r)
    return cfg.Path, buf.String(), c.GetWarningCount()
}

@copilot please address this.

@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 — one correctness issue must be addressed before merge

The direction is right: auto-deriving path: before YAML generation is the correct fix for empty GH_AW_CHECKOUT_PATH_N. The overall structure is clean and the test coverage is solid for the happy path.

Blocking issue and two non-blocking concerns

Blocking

  • Empty derived path for malformed slugs (checkout_path_validation.go:51): any Repository slug whose name-segment is empty (e.g. "owner/" or "/") causes repoName = "", so cfg.Path is set to "" — the exact runtime failure this PR is fixing. Add a guard that skips or errors when repoName == "" after derivation.

Non-blocking (but worth addressing)

  • validate* naming convention broken (checkout_path_validation.go:28): every other validate* method is read-only; this one mutates its input. Rename to reflect the dual responsibility.
  • Misleading test name (checkout_path_validation_test.go:111): "warning message for static repo contains suggested path" implies the message text is asserted, but the body only checks count and derived path. Rename or add a stderr capture assertion.

🔎 Code quality review by PR Code Quality Reviewer · 200.8 AIC · ⌖ 7.67 AIC · ⊞ 5.2K
Comment /review to run again

}

checkoutPathValidationLog.Printf("auto-deriving path %q for cross-repo checkout %q", repoName, cfg.Repository)
cfg.Path = repoName

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.

Empty cfg.Path still possible for malformed trailing-slash slugs: strings.LastIndex("owner/", "/") returns 5, making repoName = "", so cfg.Path is set to "" — reproducing the very runtime failure this PR is trying to prevent.

💡 Suggested fix

After computing repoName, guard against the empty result before mutating:

repoName := cfg.Repository
if slash := strings.LastIndex(cfg.Repository, "/"); slash >= 0 {
    repoName = cfg.Repository[slash+1:]
}
if repoName == "" {
    // malformed slug (e.g. "owner/" or "/") — cannot derive a safe path; skip auto-derivation
    // and leave it to the user or a future schema-validation pass.
    checkoutPathValidationLog.Printf("skipping auto-derivation: empty repo-name for %q", cfg.Repository)
    continue
}

Without this guard the downstream GH_AW_CHECKOUT_PATH_N: "" emission and the associated safe-outputs runtime failure (Repository not found in workspace) remain reachable for any slug whose repository-name segment is empty — whether that comes from "owner/", "/", or similar degenerate inputs.

// cannot determine at compile time whether they target a different repository, and
// common patterns such as `repository: ${{ github.repository }}` are valid
// same-repository checkouts that should not receive a warning.
func (c *Compiler) validateCrossRepoCheckoutPaths(checkoutConfigs []*CheckoutConfig, markdownPath string) {

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.

validate* naming convention violated — this function mutates its input: every other validate* method in the codebase is read-only and returns an error. This function silently mutates cfg.Path on caller-owned objects. Future maintainers adding a compile pass before this one, or a validator that checks cfg.Path == "" to mean "not yet set", will be surprised by the mutation.

💡 Suggested approach

Rename to deriveAndWarnCrossRepoCheckoutPaths (or similar) to be honest about the dual responsibility, and add a top-of-function comment making the mutation explicit:

// deriveAndWarnCrossRepoCheckoutPaths auto-derives cfg.Path for cross-repo checkout
// entries that have no explicit path: field, and emits a deprecation warning for each.
//
// NOTE: This function mutates the CheckoutConfig entries in the slice; it must be
// called before any step that reads cfg.Path (e.g. GenerateCheckoutManifestStep).
func (c *Compiler) deriveAndWarnCrossRepoCheckoutPaths(...) {

Alternatively, return the list of modified configs or only mutate inside a clearly named deriveCheckoutPath helper that is invoked explicitly by the compilation step, keeping validation logic purely diagnostic.

c.validateCrossRepoCheckoutPaths(configs, markdownPath)
assert.Equal(t, "widget-service", configs[0].Path)
assert.Equal(t, 1, c.GetWarningCount())
})

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.

Test name promises message content verification but the test body does not deliver it: the comment inside the test even acknowledges this gap: // the actual message text is validated by the path value and warning count. — but the test name is "warning message for static repo contains suggested path", which implies the message text is checked.

💡 Suggested fix

Capture stderr during the call and assert on the message text, e.g.:

var buf bytes.Buffer
origStderr := os.Stderr
r, w, _ := os.Pipe()
os.Stderr = w

c.validateCrossRepoCheckoutPaths(configs, markdownPath)

w.Close()
os.Stderr = origStderr
io.Copy(&buf, r)
out := buf.String()
assert.Contains(t, out, "widget-service", "warning must include the derived path")
assert.Contains(t, out, `path: widget-service`, "warning must include the explicit-path suggestion")

Or rename the test to "cross-repo checkout warning fires once and derives correct path" so the name matches what it actually validates.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

🤖 PR Triage — Run §28413530597

Field Value
Category bug
Risk 🟡 medium
Score 55 / 100
Breakdown impact 30 + urgency 15 + quality 10
Action batch_review

Notes: Carried over (11.1h, was draft). CI all passing (19/19 ✅). Ready for review. Blocked by 4× CHANGES_REQUESTED from github-actions[bot] — address review feedback before merge. Bug fix for compiler emitting empty checkout paths causing runtime failures in push_to_pull_request_branch.

Generated by 🔧 PR Triage Agent · 61.6 AIC · ⌖ 7.7 AIC · ⊞ 1.6K ·

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

Done in aa2521b. I ran the PR-finisher pass, tightened the checkout-path fix around explicit root and malformed repository values, and re-ran local validation (make fmt, make lint, make test-unit, make test, make agent-report-progress). CI on this head is still unverified until a maintainer re-triggers it.

Copilot AI requested a review from pelikhan June 30, 2026 01:33
@pelikhan pelikhan merged commit 64ddeb9 into main Jun 30, 2026
29 checks passed
@pelikhan pelikhan deleted the copilot/compiler-require-path-for-checkout branch June 30, 2026 02:06
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.

4 participants