Skip to content

feat(workflow): cache repository owner-type API call once per compilation run#40258

Merged
pelikhan merged 5 commits into
mainfrom
copilot/cache-repository-owner-type-query
Jun 19, 2026
Merged

feat(workflow): cache repository owner-type API call once per compilation run#40258
pelikhan merged 5 commits into
mainfrom
copilot/cache-repository-owner-type-query

Conversation

Copilot AI commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

The gh api /users/<owner> call ("Checking repository owner type...") was invoked once per compiled workflow file when determining whether to suppress the copilot-requests: write tip for individual-user repos. With N Copilot workflows in a repo, this meant N identical API round-trips per gh aw compile run.

Changes

  • compiler_types.go — Initialize ownerTypeCache map[string]string eagerly in NewCompiler() alongside actionPinWarnings and priorManifests, making the intent explicit and consistent.
  • permissions_compiler_validator.go — Remove the now-redundant nil-guard from repositoryOwnerIsIndividualUser; the map is always initialized.
  • compiler_owner_type_cache_test.go (new) — Unit tests covering:
    • Cache-hit path for "User", "Organization", and "" (prior API error) — pre-populating the cache and asserting the correct result without a network call
    • Malformed/empty repositorySlug returns false immediately
    • Cache persists across multiple workflow compilations on the same Compiler instance (the multi-file case)
    • NewCompiler() always produces an initialized (non-nil) map

Copilot AI and others added 2 commits June 19, 2026 06:22
Initialize ownerTypeCache in NewCompiler so the owner-type API call
("Check repository owner type...") is made at most once per repo during
a single compilation run. Add unit tests that verify the cache is
consulted before making a network call and that it persists across
multiple workflow compilations on the same Compiler instance.

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

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title feat(workflow): cache repository owner type per compilation run feat(workflow): cache repository owner-type API call once per compilation run Jun 19, 2026
Copilot AI requested a review from pelikhan June 19, 2026 06:23
@pelikhan pelikhan marked this pull request as ready for review June 19, 2026 13:17
Copilot AI review requested due to automatic review settings June 19, 2026 13:17

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 reduces redundant gh api /users/<owner> lookups during gh aw compile by ensuring the repository owner-type cache is initialized once per Compiler instance and then reused across workflow compilations in the same run.

Changes:

  • Initialize ownerTypeCache eagerly in NewCompiler() to make the cache lifecycle explicit.
  • Remove the redundant nil-guard in repositoryOwnerIsIndividualUser() now that the cache is always initialized by NewCompiler().
  • Add focused unit tests to cover cache-hit behavior, malformed slugs, cache persistence across multiple compilations, and non-nil initialization.
Show a summary per file
File Description
pkg/workflow/compiler_types.go Eagerly initializes ownerTypeCache in NewCompiler() to support per-run caching.
pkg/workflow/permissions_compiler_validator.go Removes now-unnecessary nil initialization inside repositoryOwnerIsIndividualUser().
pkg/workflow/compiler_owner_type_cache_test.go Adds unit tests validating cache hits, malformed slug handling, cross-compilation reuse, and initialization.

Copilot's findings

Tip

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

  • Files reviewed: 3/3 changed files
  • Comments generated: 0

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

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

📄 Draft ADR committed: docs/adr/40258-cache-repository-owner-type-per-compilation-run.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. It captures the caching decision: cache the gh api /users/<owner> owner-type lookup once per owner per compilation run, with eager ownerTypeCache initialization in NewCompiler().
  2. Complete the missing sections — confirm the context, refine the decision rationale, and adjust the alternatives/consequences to match your intent.
  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-40258: Cache Repository Owner-Type Lookup Per Compilation Run

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

❓ Why ADRs Matter

ADRs create a searchable, permanent record of why the codebase looks the way it does. Even a small performance/caching change — like deduplicating per-owner API round-trips across a multi-workflow compile — is worth recording so future contributors understand the trade-off (cache lifetime, cached negative results) without re-deriving it from the diff.

📋 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., 0042-use-postgresql.md for PR #42).

🔒 Blocking: this PR cannot merge until an ADR is linked in the PR body.

References: §27828047748

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 66.2 AIC · ⌖ 10.5 AIC · ⊞ 13.6K ·

@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 /zoom-out, /tdd, and /improve-codebase-architecture — commenting with observations, no blocking issues.

📋 Key Themes & Highlights

Key Themes

  • Nil-guard removal is load-bearing — the codebase has ~15 tests using &Compiler{} struct literals that bypass NewCompiler(). None reach repositoryOwnerIsIndividualUser() today, but removing the guard silently transfers that safety contract to every future test author. Consider restoring it or documenting the invariant on the field.
  • Sticky-false-on-error — a transient API failure caches "" and suppresses the copilot-requests tip for the entire run. This is the correct fail-safe default but is undocumented. A one-line comment on the function would help future readers.
  • Goroutine-safety assumption — other API-backed caches in the package use sync.Map; a note that this map is intentionally not goroutine-safe (single-threaded compilation) prevents misreads.
  • Commit type — the PR is tagged feat but the user-facing behaviour is unchanged; perf or refactor is more accurate for changelog generation.

Positive Highlights

  • ✅ Excellent test-to-production ratio: 120 lines of tests for a 3-line production change.
  • ✅ Tests cover all four key axes: cache-hit, malformed slug, cross-compilation persistence, and NewCompiler nil-safety.
  • ✅ The test for TestRepositoryOwnerIsIndividualUser_CacheInitializedByNewCompiler is a good invariant test that will catch future NewCompiler() regressions.
  • ✅ Eager initialization in NewCompiler() makes the intent explicit and consistent with actionPinWarnings and priorManifests.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 81.3 AIC · ⌖ 9.32 AIC · ⊞ 6.9K

if c.ownerTypeCache == nil {
c.ownerTypeCache = make(map[string]string)
}
ownerType, cached := c.ownerTypeCache[owner]

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] The nil-guard removal is safe today because no existing caller reaches this path with a bare &Compiler{}, but the codebase has 15+ tests that construct &Compiler{} struct literals directly (e.g. frontmatter_extraction_security_test.go, unified_prompt_creation_test.go). If any future test covering copilot-permissions validation uses that pattern, the write at c.ownerTypeCache[owner] = "" (line 195 of the post-PR file) will panic.

💡 Suggested mitigation

The simplest fix is to restore the nil-guard as cheap defensive belt-and-suspenders:

if c.ownerTypeCache == nil {
    c.ownerTypeCache = make(map[string]string)
}

Alternatively, document the invariant on the struct field in compiler_types.go:

// ownerTypeCache is always initialized by NewCompiler().
// Bare &Compiler{} struct literals will panic on first write.
ownerTypeCache map[string]string

The nil-guard was inexpensive; removing it transfers an implicit contract to every future test author.

expectedResult: false,
},
{
name: "cached empty string (API error) returns false",

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 "cached empty string (API error) returns false" case correctly tests the cache-hit read path, but there is no coverage for the write side: when RunGH fails, "" is stored in the cache, and the copilot-requests tip is silently suppressed for the entire compilation run — even if the failure was transient (e.g. a momentary network blip).

This is likely the intended fail-safe behaviour (uncertain owner type → show tip), but the "sticky" aspect (no retry within a run) is easy to miss. A brief doc comment on the test or on repositoryOwnerIsIndividualUser noting this tradeoff explicitly would save a future reader from having to re-derive it.

💡 Suggested doc comment (in permissions_compiler_validator.go)
// Note: a failed API lookup caches "" so subsequent calls for the same owner
// also return false without retrying. This is intentional: fail-safe means
// "show the tip when uncertain" and avoids N retry round-trips per run.

actionPinWarnings: make(map[string]bool), // Initialize warning cache
priorManifests: make(map[string]*GHAWManifest),
gitRoot: gitRoot, // Auto-detected git root
ownerTypeCache: make(map[string]string), // Initialize owner-type cache (keyed by owner login)

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.

[/improve-codebase-architecture] The rest of the package's API-backed caches (repositoryFeaturesCache, unquoteYAMLKeyCache) use sync.Map with explicit goroutine-safety documentation in repository_features_validation.go. This map is per-Compiler instance and compilation is currently sequential, so a plain map is correct today — but there is no comment stating that assumption.

Adding a brief note keeps future readers from having to reverse-engineer why the pattern differs:

ownerTypeCache: make(map[string]string), // Not goroutine-safe; Compiler is used sequentially

Or, if compilation is ever parallelized, upgrade to sync.Map for consistency with the established pattern.

@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 blocking issue, one minor cleanup

The caching goal is correct and the eager-initialization pattern is a genuine improvement. Two issues need addressing before merge.

🔴 Blocking — nil-guard removal creates latent panic path

The removed nil-guard was protecting against assignment to entry in nil map panics on Compiler instances created via &Compiler{} struct literals (two exist in production code; many more in tests). The PR's claim that the map is "always initialized" is only true for the NewCompiler() code path. Fix: restore the nil-guard, or enforce the invariant by converting the two &Compiler{} call sites to NewCompiler().

See inline comment on permissions_compiler_validator.go:189.

🟡 Minor — dead cache seedings in malformed-slug test mislead future readers

All slugs in TestRepositoryOwnerIsIndividualUser_MalformedSlug return before reaching the cache, so the ownerTypeCache pre-seedings at lines 77–78 are never read and the accompanying comment about bypass risk is misleading. Remove them.

See inline comment on compiler_owner_type_cache_test.go:77.

🔎 Code quality review by PR Code Quality Reviewer · 142.1 AIC · ⌖ 7.46 AIC · ⊞ 5.1K

if c.ownerTypeCache == nil {
c.ownerTypeCache = make(map[string]string)
}
ownerType, cached := c.ownerTypeCache[owner]

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.

Nil-guard removal is based on an incomplete invariant and leaves a latent nil-map write panic.

💡 Details and suggested fix

The PR claims the guard is "now-redundant" because NewCompiler() always initialises ownerTypeCache. That is only true for instances created through NewCompiler(). Two production call sites create &Compiler{} struct literals that bypass the constructor entirely:

safe_jobs.go:328           c := &Compiler{}
safe_outputs_config.go:725 c := &Compiler{}

Reading from a nil map (line 189) is safe in Go — returns zero value + ok=false. But the writes below are not:

c.ownerTypeCache[owner] = ""       // line 195 — panics on nil map
c.ownerTypeCache[owner] = ownerType // line 199 — panics on nil map

Neither current call site reaches repositoryOwnerIsIndividualUser(), so there is no active bug today. But the nil-guard was the only thing standing between a future refactor and a assignment to entry in nil map panic. Restoring it is a one-liner with zero runtime cost:

if c.ownerTypeCache == nil {
    c.ownerTypeCache = make(map[string]string)
}

Alternatively: change the two &Compiler{} call sites to use NewCompiler() and document the invariant on the struct so future contributors can rely on it safely.

c.SetRepositorySlug(tt.slug)
// Populate the cache with "User" for any conceivable owner — if the slug
// parsing were bypassed and a lookup happened, it would incorrectly return true.
c.ownerTypeCache["owneronly"] = "User"

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.

Both cache seedings in this test are dead code — all malformed slugs return before the cache is consulted.

💡 Details

Every slug in this table triggers the early-return guard at line 184:

if !ok || owner == "" || repo == "" {
    return false
}

before execution ever reaches the cache lookup. Specifically:

slug strings.Cut result guard fires on
"" ok=false !ok
"owneronly" ok=false !ok
"/" owner="", repo="" owner==""
"/repo" owner="" owner==""
"owner/" repo="" repo==""

Because the function returns before reaching the cache, the seedings at lines 77–78 are never read. The test comment's claim that the seedings guard against "incorrectly return true" is therefore misleading — there is no code path through which that could happen.

Remove the two dead assignments (or replace them with a comment explaining why they're absent) to keep the test signal clear.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

⚠️ Test Quality Score: 77/100 — Acceptable

Analyzed 10 test scenarios (4 functions): 9 design tests, 1 implementation test, 0 guideline violations.

📊 Metrics & Test Classification (10 scenarios across 4 functions analyzed)
Metric Value
New/modified tests analyzed 10 scenarios (4 functions)
✅ Design tests (behavioral contracts) 9 (90%)
⚠️ Implementation tests (low value) 1 (10%)
Tests with error/edge cases 7 (70%)
Duplicate test clusters 0
Test inflation detected ⚠️ YES (120 test lines vs 2 production lines, 60:1)
🚨 Coding-guideline violations 0
Test File Classification Issues Detected
TestRepositoryOwnerIsIndividualUser_CacheHit (3 rows) pkg/workflow/compiler_owner_type_cache_test.go:14 ✅ Design
TestRepositoryOwnerIsIndividualUser_MalformedSlug (5 rows) pkg/workflow/compiler_owner_type_cache_test.go:57 ✅ Design
TestRepositoryOwnerIsIndividualUser_CacheSharedAcrossCompilations pkg/workflow/compiler_owner_type_cache_test.go:90 ✅ Design
TestRepositoryOwnerIsIndividualUser_CacheInitializedByNewCompiler pkg/workflow/compiler_owner_type_cache_test.go:113 ⚠️ Implementation Inspects internal ownerTypeCache field directly

Go: 1 (*_test.go); JavaScript: 0. Other languages not scored.

Build tag: //go:build !integration present on line 1 ✅
Mock libraries: None ✅
Assertion messages: All descriptive ✅

Inflation context: The test file is new and covers the full caching behavioral contract. The production change was a refactoring (moving lazy initialization into NewCompiler), so a large test-to-production line ratio is expected and appropriate here.

⚠️ Flagged Tests — Low Priority (1 issue)

TestRepositoryOwnerIsIndividualUser_CacheInitializedByNewCompiler (pkg/workflow/compiler_owner_type_cache_test.go:113) — ⚠️ Implementation detail: directly inspects the unexported c.ownerTypeCache field to verify it is non-nil. This tests constructor internals rather than observable behavior; a nil-map panic would already be caught by any of the other three tests in this file. Suggested improvement: rely on the existing behavioral tests to implicitly verify nil-safety, or add a comment explaining this test guards against a specific initialization regression.

Verdict

Check passed. 10% implementation tests (threshold: 30%). Design tests cover the core cache-hit, malformed-slug, and cross-compilation cache-sharing contracts; all assertions carry descriptive messages and the build tag is correctly set.

References: §27828047723

🧪 Test quality analysis by Test Quality Sentinel · 170.2 AIC · ⌖ 5.87 AIC · ⊞ 8.3K ·

@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: 77/100. Test quality is acceptable — 10% of new tests are implementation tests (threshold: 30%).

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill (fix call sites)

…w feedback

- Change &Compiler{} in safe_jobs.go and safe_outputs_config.go to
  NewCompiler() so ownerTypeCache is always initialized, eliminating
  the latent nil-map write panic if those paths ever reach
  repositoryOwnerIsIndividualUser()
- Add doc comment explaining the fail-safe caching behavior for API
  errors (empty string cached, no retry within a run)
- Note that ownerTypeCache is not goroutine-safe (Compiler is sequential)
- Remove dead cache seedings in TestRepositoryOwnerIsIndividualUser_MalformedSlug
  (all malformed slugs return before the cache is consulted)

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

Copilot AI commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill (fix call sites)

Fixed in the latest commit. The two &Compiler{} call sites in safe_jobs.go and safe_outputs_config.go now use NewCompiler(), ensuring ownerTypeCache is always initialized. Also addressed the other open review threads: added a fail-safe caching doc comment, noted goroutine-safety on the field, and removed the dead cache seedings from the malformed-slug test.

Copilot AI requested a review from pelikhan June 19, 2026 14:02
@pelikhan pelikhan merged commit 075b15c into main Jun 19, 2026
26 of 29 checks passed
@pelikhan pelikhan deleted the copilot/cache-repository-owner-type-query branch June 19, 2026 14:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants