Skip to content

fix: add actions: write to update_cache_memory and conclusion jobs to unblock cache saves#47046

Merged
pelikhan merged 10 commits into
mainfrom
copilot/update-cache-memory-job-permissions
Jul 21, 2026
Merged

fix: add actions: write to update_cache_memory and conclusion jobs to unblock cache saves#47046
pelikhan merged 10 commits into
mainfrom
copilot/update-cache-memory-job-permissions

Conversation

Copilot AI commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

GitHub's cache-reservation backend requires a token with at least one writable scope. Both update_cache_memory and conclusion (daily-AIC) cache-save jobs lacked this, causing silent failures: cache write denied: token has no writable scopes. Because cache steps are continue-on-error: true, runs stayed green while cache-memory and daily-AIC lineage were never persisted.

Changes

pkg/workflow/cache.gobuildUpdateCacheMemoryJob

  • Replaces NewPermissionsEmpty() (renders permissions: {}) with NewPermissionsActionsWrite()
  • Dev mode (local action checkout) additionally sets contents: read
// Before
permissions := NewPermissionsEmpty().RenderToYAML() // → permissions: {}

// After
perms := NewPermissionsActionsWrite()               // → actions: write
if setupActionRef != "" && len(c.generateCheckoutActionsFolder(data)) > 0 {
    perms.Set(PermissionContents, PermissionRead)   // dev mode: + contents: read
}
permissions := perms.RenderToYAML()

pkg/workflow/notify_comment.gobuildConclusionJob

  • Adds actions: write to conclusionPerms when hasMaxDailyAICGuardrail && WorkflowID != "" — i.e. exactly when buildDailyAICUsageCacheSteps injects the actions/cache/save step

Tests

  • TestUpdateCacheMemoryJobHasActionsWritePermission — unit test on the job struct
  • TestConclusionJobActionsWritePermissionForDailyAICCache — table test covering both with/without WorkflowID
  • Integration assertion added to cache_memory_threat_detection_test.go

Lock files

All 260 compiled lock files regenerated; each affected update_cache_memory and conclusion job now carries actions: write.

…r cache saves

Cache saves (actions/cache/save) require a token with at least one writable scope.
The update_cache_memory job had permissions: {} (empty), and the conclusion job
relied only on safe-outputs permissions (issues/pull-requests: write), neither of
which satisfies GitHub's cache-reservation backend requirement.

Both jobs now emit actions: write:
- buildUpdateCacheMemoryJob (pkg/workflow/cache.go): replaces NewPermissionsEmpty()
  with NewPermissionsActionsWrite(), additionally setting contents: read in dev mode.
- buildConclusionJob (pkg/workflow/notify_comment.go): sets PermissionActions: write
  when hasMaxDailyAICGuardrail && WorkflowID != "" (i.e. when the daily-AIC cache
  save steps are included).

Recompiled all 260 lock files to reflect the new permissions.

Fixes: cache write denied: token has no writable scopes

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix cache memory job permissions for writable scopes fix: add actions: write to update_cache_memory and conclusion jobs to unblock cache saves Jul 21, 2026
Copilot AI requested a review from pelikhan July 21, 2026 09:36
@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot validate this fix against GitHub actions docs

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

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

No test files were added or modified in this PR. Test Quality Sentinel skipped. PR #47046 only modifies .lock.yml workflow files to add actions: write permissions.

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 the required actions: write permission to compiler-generated cache-save jobs, fixing silent cache persistence failures.

Changes:

  • Grants cache-write permissions to update_cache_memory and applicable conclusion jobs.
  • Adds unit and integration coverage.
  • Regenerates affected workflow lock files.
Show a summary per file
File Description
pkg/workflow/cache.go Grants cache-update jobs actions: write.
pkg/workflow/notify_comment.go Grants daily-AIC cache saves actions: write.
pkg/workflow/compiler_jobs_test.go Tests cache-memory permissions.
pkg/workflow/notify_comment_test.go Tests conclusion-job permissions.
pkg/workflow/cache_memory_threat_detection_test.go Adds compiled-workflow assertion.
.github/workflows/*.lock.yml (260 files) Regenerates affected conclusion and cache-update permissions.

Review details

Tip

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

  • Files reviewed: 264/264 changed files
  • Comments generated: 2
  • Review effort level: Medium

"- name: Save cache-memory to cache (default)",
"uses: actions/cache/save@",
// update_cache_memory job must have actions: write for cache reservation
"actions: write",
Comment on lines +1543 to +1546
t.Run("no actions: write when WorkflowID is empty", func(t *testing.T) {
workflowData := &WorkflowData{
Name: "Test Workflow",
WorkflowID: "", // no WorkflowID → no daily-AIC cache steps
@github-actions github-actions Bot mentioned this pull request Jul 21, 2026

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

The fix is correct and well-scoped.

  • cache.go: update_cache_memory now starts with NewPermissionsActionsWrite() (always actions: write) and conditionally adds contents: read in dev mode — previously it started with empty permissions, breaking cache saves.
  • notify_comment.go: conclusion adds actions: write only when hasMaxDailyAICGuardrail && WorkflowID != "", precisely targeting the daily-AIC cache save path.

All referenced permission helpers exist and are correctly used. The 260 recompiled lock files reflect the change consistently.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 23.8 AIC · ⌖ 5.23 AIC · ⊞ 4.9K

Copilot AI commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@copilot validate this fix against GitHub actions docs

Validated against the GitHub Actions workflow syntax docs and the June 2026 changelog on read-only cache for untrusted triggers.

The fix is correct. Here's why:

What the docs say

The actions permission scope covers all interactions with the GitHub Actions service, including the cache backend. From the official table:

actions — Work with GitHub Actions. For example, actions: write permits an action to cancel a workflow run.

The cache service is part of the Actions platform, so actions: write is the correct (and minimal) scope to satisfy the reservation backend requirement.

Why permissions: {} caused silent failures

When a job has empty permissions ({}), the GITHUB_TOKEN has no writable scopes at all. The cache-reservation API enforces that the token must have at least one writable scope before it will issue a cache-write lease. With continue-on-error: true on the save step, the failure is swallowed — runs stay green but cache is never persisted.

Key nuance: "untrusted trigger" read-only behavior (June 2026 change)

GitHub's recent hardening means cache writes are also blocked for untrusted triggers (e.g. PRs from forks) regardless of permissions. However, update_cache_memory and conclusion run on trusted triggers (workflow_dispatch, schedule, issue_comment from collaborators), so this restriction doesn't apply here — actions: write is sufficient.

Scope is minimal ✅

The fix adds only actions: write (plus contents: read in dev mode for the checkout step). It does not use write-all or any over-broad permission set.

…save jobs

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 (110 new lines in pkg/ — above the 100-line threshold) but does not have a linked Architecture Decision Record (ADR).

Draft ADR committed: docs/adr/47046-add-actions-write-permission-to-cache-save-jobs.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 confirm the alternatives you considered
  3. Commit the finalized ADR to docs/adr/ on your branch (it is already there as a draft)
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-47046: Add actions write Permission to Cache-Save Jobs

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. 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., 47046-add-actions-write-permission-to-cache-save-jobs.md for PR #47046).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 67.9 AIC · ⌖ 13.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 and /tdd — one minor test coverage gap noted; no blocking issues.

📋 Key Themes & Highlights

Key Themes

  • Root cause properly fixed: NewPermissionsEmpty()NewPermissionsActionsWrite() correctly addresses the cause, not just the symptom. The dev-mode branch now uses perms.Set(...) instead of reassigning the whole block, so both permissions coexist.
  • Conclusion job fix is precisely guarded: hasMaxDailyAICGuardrail(data) && data.WorkflowID != "" matches exactly the condition under which buildDailyAICUsageCacheSteps injects the actions/cache/save step — tight and correct.
  • Tests land with the fix: TestUpdateCacheMemoryJobHasActionsWritePermission, TestConclusionJobActionsWritePermissionForDailyAICCache, and the integration assertion in cache_memory_threat_detection_test.go all ship in this PR.

Test Coverage Gap

  • The dev-mode branch (setupActionRef != "") is not exercised by the new tests. This is the exact branch the original bug inhabited (permissions were fully replaced, dropping actions: write). A dedicated test that asserts both actions: write and contents: read appear when setupActionRef != "" would lock in the regression.

Positive Highlights

  • ✅ Clean, minimal diff in the Go source — no unrelated changes.
  • ✅ Comment explaining why actions: write is needed is clear and actionable for future readers.
  • ✅ 264 lock files regenerated consistently — confirms make recompile was run.
  • COMMENT event rather than requesting changes: the gap is low risk since the default path (no dev mode) is covered, and the existing integration test in cache_memory_threat_detection_test.go exercises the full compiled output.

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

t.Fatal("Expected update_cache_memory job to be created")
}

// Must have actions: write so cache saves are not rejected with "token has no writable scopes".

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 dev mode branch (setupActionRef != "") is not covered — this is the exact code path where the original bug lived (the old code replaced the entire permissions block, losing actions: write). No regression test verifies that dev mode still emits both actions: write and contents: read.

💡 Suggested addition

Add a test (or sub-test) that exercises the dev-mode path and asserts both permissions are present:

func TestUpdateCacheMemoryJobHasActionsWritePermissionInDevMode(t *testing.T) {
    // ... set up compiler with setupActionRef != "" so dev-mode branch executes ...
    if !strings.Contains(job.Permissions, "actions: write") {
        t.Errorf("dev mode must preserve actions: write, got: %q", job.Permissions)
    }
    if !strings.Contains(job.Permissions, "contents: read") {
        t.Errorf("dev mode must have contents: read, got: %q", job.Permissions)
    }
}

@copilot please address this.

@github-actions

This comment has been minimized.

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

Fix is correct; two non-blocking maintainability issues flagged

The core change is sound: replacing NewPermissionsEmpty() with NewPermissionsActionsWrite() in buildUpdateCacheMemoryJob correctly unblocks cache saves, and the dev-mode contents-read path is preserved via perms.Set(...) on the same object (fixing a latent bug where the old code created a new perms object, discarding any prior settings). The conclusion job permission guard mirrors the step-injection condition precisely.

🔍 Findings (non-blocking)

1. Duplicated guard condition — drift risk (notify_comment.go:86)
The hasMaxDailyAICGuardrail(data) && data.WorkflowID != "" expression is copy-pasted in both buildConclusionJob (permissions) and buildConclusionSetupSteps (step injection). If either site drifts, the regression is invisible because cache steps run with continue-on-error: true. A shared needsDailyAICCachePermission(data) predicate would prevent this. See inline comment.

2. Weak integration-test assertion (cache_memory_threat_detection_test.go:65)
The "actions: write" string is matched against the entire compiled lock file, not scoped to the update_cache_memory: block. The dedicated unit test in compiler_jobs_test.go is the real guard; the integration-test assertion provides false confidence. See inline comment.

🔎 Code quality review by PR Code Quality Reviewer · 113.4 AIC · ⌖ 4.77 AIC · ⊞ 5.6K
Comment /review to run again

Comment thread pkg/workflow/notify_comment.go Outdated
}
// actions: write is required for GitHub's cache-reservation backend to allow cache saves.
// The daily-AIC usage cache save step uses actions/cache/save which requires this permission.
if hasMaxDailyAICGuardrail(data) && data.WorkflowID != "" {

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.

Permission and step-inclusion guards are duplicated across two files — future drift will silently break cache saves.

💡 Details and suggested fix

The actions: write permission is gated on hasMaxDailyAICGuardrail(data) && data.WorkflowID != "" here (line 86), and the identical expression appears in notify_comment_conclusion_helpers.go:50 to guard step injection. They are in sync today, but as two independent copy-pasted guards there is no enforcement.

If the helper ever relaxes or changes its guard, the permission block here will silently not be set, producing the original cache write denied: token has no writable scopes regression — invisible because cache steps run with continue-on-error: true.

Extract a single named predicate:

// needsDailyAICCachePermission reports whether the conclusion job
// must have actions: write for daily-AIC cache saves.
func needsDailyAICCachePermission(data *WorkflowData) bool {
    return hasMaxDailyAICGuardrail(data) && data.WorkflowID != ""
}

Both buildConclusionJob and buildConclusionSetupSteps call needsDailyAICCachePermission(data), making them impossible to drift apart.

"- name: Save cache-memory to cache (default)",
"uses: actions/cache/save@",
// update_cache_memory job must have actions: write for cache reservation
"actions: write",

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.

Assertion "actions: write" is a whole-file substring search — it passes even if update_cache_memory specifically is missing the permission.

💡 Details and suggested fix

The integration test checks expectedInLock by scanning the entire compiled lock file for the string "actions: write". If any other job in the output (e.g. conclusion) carries actions: write, this assertion passes even if update_cache_memory silently lost the permission.

The dedicated unit test TestUpdateCacheMemoryJobHasActionsWritePermission in compiler_jobs_test.go does inspect updateCacheMemoryJob.Permissions directly and is the stronger regression guard. But this integration-test assertion provides false confidence — consider scoping it to the update_cache_memory: block, or removing it here and relying solely on the unit test.

Example scoped check:

// Find the update_cache_memory job block and assert actions: write within it
idx := strings.Index(lock, "update_cache_memory:")
if idx == -1 {
    t.Fatal("update_cache_memory job not found in lock")
}
if !strings.Contains(lock[idx:idx+500], "actions: write") {
    t.Error("update_cache_memory job missing actions: write")
}

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot update compiler to only add writeable scope when needed (lazy)

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

Copilot AI commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@copilot update compiler to only add writeable scope when needed (lazy)

Addressed in 6236cfa. The compiler now starts update_cache_memory with empty permissions and adds actions: write only when it actually emits a cache-save step; the restore-only path is covered by a direct unit test.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot recompile

Copilot AI commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@copilot recompile

Ran make recompile on 97d2cdf; it completed cleanly and produced no tracked file changes, so the branch is already fully recompiled.

needs.activation.outputs.secret_verification_result == 'failed' || needs.activation.outputs.daily_ai_credits_exceeded == 'true')
runs-on: ubuntu-slim
permissions:
actions: write

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@copilot there is already issues:write in this scope, why is actions:write needed?

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 269b95a. issues: write already makes the GITHUB_TOKEN writable for the daily-AIC cache-save path, so actions: write was redundant in that scope. The compiler now adds actions: write only when the conclusion job would otherwise be fully read-only.

@pelikhan pelikhan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

...

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

Copy link
Copy Markdown
Contributor

Triage

Field Value
Category bug
Risk medium
Score 67/100 (impact:35 urgency:20 quality:12)
Action fast_track

Rationale: Fixes silent cache-save failures (cache write denied: token has no writable scopes) across 105 files. Permissions change is targeted and low-risk; human approval needed given wide file impact.

Generated by 🔧 PR Triage Agent · sonnet46 45.9 AIC · ⌖ 5.48 AIC · ⊞ 5.6K ·

@github-actions

Copy link
Copy Markdown
Contributor

Great work on this fix! 🚀 The cache persistence issue you are addressing is critical — workflows silently fail to persist cache lineage while staying green, which breaks downstream cache-memory and daily-AIC tracking.

What is solid here:

  • Precise root-cause analysis: NewPermissionsEmpty() left cache-save jobs without actions: write, causing silent failures
  • Smart permission reuse in the conclusion job: checks for existing writable scopes before adding new ones
  • Excellent test coverage: unit tests on the job struct, table tests for both with/without daily-AIC guardrails, and integration assertions
  • All 260 compiled lock files regenerated and updated consistently
  • Clear, detailed PR description with before/after code examples

Ready for merge — this is high-quality agentic output that follows the project development process. The pr-action:fast_track label is well-earned.

Generated by ✅ Contribution Check · sonnet46 54.7 AIC · ⌖ 7.29 AIC · ⊞ 6.2K ·

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

@pelikhan
pelikhan merged commit c5b4281 into main Jul 21, 2026
19 checks passed
@pelikhan
pelikhan deleted the copilot/update-cache-memory-job-permissions branch July 21, 2026 13:34
Copilot stopped work on behalf of pelikhan due to an error July 21, 2026 13:34
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.

cache-memory: update_cache_memory (+ conclusion AIC cache) save fails "token has no writable scopes" — job lacks actions: write

3 participants