Skip to content

fix: gate push_repo_memory on agent not skipped instead of success#25960

Merged
pelikhan merged 3 commits intomainfrom
copilot/fix-push-repo-memory-job
Apr 13, 2026
Merged

fix: gate push_repo_memory on agent not skipped instead of success#25960
pelikhan merged 3 commits intomainfrom
copilot/fix-push-repo-memory-job

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 13, 2026

push_repo_memory runs on every bot-triggered no-op workflow invocation because always() bypasses skip propagation and the condition only checked needs.agent.result == 'success'. When pre_activation skips (no slash command found), the entire job graph cascades to skipped—but push_repo_memory still fires, wasting runner time and risking spurious commits to the memory branch.

Change

  • pkg/workflow/repo_memory.go: Replace needs.agent.result == 'success' with needs.agent.result != 'skipped' and add a !cancelled() guard to prevent the job from running after workflow cancellation (consistent with compiler_safe_outputs_job.go).
# Before
if: always() && (needs.detection.result == 'success' || needs.detection.result == 'skipped') && needs.agent.result == 'success'

# After
if: always() && (!cancelled()) && (needs.detection.result == 'success' || needs.detection.result == 'skipped') && needs.agent.result != 'skipped'

This correctly distinguishes "agent never ran" (skipped → don't push) from "agent ran but failed" (failure → still push partial memory data), while also preventing spurious runs after workflow cancellation. The detection gate for threat detection workflows is preserved unchanged.

  • pkg/workflow/repo_memory_test.go: Added TestPushRepoMemoryJobConditionGatesOnAgentNotSkipped covering both threat-detection-enabled and disabled paths, asserting the full expected condition string including !cancelled().
  • 27 .lock.yml files: Recompiled.

Change the push_repo_memory job condition from checking
needs.agent.result == 'success' to needs.agent.result != 'skipped'.

This prevents push_repo_memory from running on no-op workflow
invocations (e.g. bot comments where pre_activation is skipped and
the skip cascades through activation → agent → detection).

The != 'skipped' check also allows repo-memory to be pushed when
the agent fails, preserving partial memory data.

Fixes #1556

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/053233b6-7afc-4caf-bf6e-74bbd9f26562

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix push_repo_memory job activation logic fix: gate push_repo_memory on agent not skipped instead of success Apr 13, 2026
Copilot AI requested a review from pelikhan April 13, 2026 01:36
@pelikhan pelikhan marked this pull request as ready for review April 13, 2026 01:37
Copilot AI review requested due to automatic review settings April 13, 2026 01:37
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates the generated GitHub Actions workflow logic so push_repo_memory does not run when the agent job was skipped (e.g., no-op bot-triggered runs), while still allowing repo-memory pushes when the agent ran but failed.

Changes:

  • Update push_repo_memory job condition to gate on needs.agent.result != 'skipped' (instead of == 'success').
  • Add a unit test ensuring the job condition uses the new gating logic with and without threat detection.
  • Regenerate compiled .lock.yml workflows to reflect the updated condition.
Show a summary per file
File Description
pkg/workflow/repo_memory.go Adjusts push_repo_memory job condition generation to gate on agent not being skipped.
pkg/workflow/repo_memory_test.go Adds test coverage for the updated job condition logic.
.github/workflows/workflow-health-manager.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/weekly-blog-post-writer.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/technical-doc-writer.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/security-compliance.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/pr-triage-agent.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/metrics-collector.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/glossary-maintainer.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/firewall-escape.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/discussion-task-miner.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/developer-docs-consolidator.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/delight.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/deep-report.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/daily-testify-uber-super-expert.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/daily-news.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/daily-community-attribution.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/daily-code-metrics.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/daily-cli-performance.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/copilot-token-optimizer.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/copilot-token-audit.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/copilot-session-insights.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/copilot-pr-prompt-analysis.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/copilot-pr-nlp-analysis.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/copilot-cli-deep-research.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/copilot-agent-analysis.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/code-scanning-fixer.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/audit-workflows.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.
.github/workflows/agent-performance-analyzer.lock.yml Regenerated lock workflow reflecting updated push_repo_memory condition.

Copilot's findings

Tip

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

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

Comment on lines 747 to 759
agentRan := BuildNotEquals(
BuildPropertyAccess(fmt.Sprintf("needs.%s.result", constants.AgentJobName)),
BuildStringLiteral("success"),
BuildStringLiteral("skipped"),
)
jobNeeds := []string{string(constants.AgentJobName), string(constants.ActivationJobName)}
var jobCondition string
if threatDetectionEnabled {
// When threat detection is enabled, also require detection passed (succeeded or skipped).
jobCondition = RenderCondition(BuildAnd(BuildAnd(BuildFunctionCall("always"), buildDetectionPassedCondition()), agentSucceeded))
jobCondition = RenderCondition(BuildAnd(BuildAnd(BuildFunctionCall("always"), buildDetectionPassedCondition()), agentRan))
jobNeeds = append(jobNeeds, string(constants.DetectionJobName))
} else {
jobCondition = RenderCondition(BuildAnd(BuildFunctionCall("always"), agentSucceeded))
jobCondition = RenderCondition(BuildAnd(BuildFunctionCall("always"), agentRan))
}
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

The new job condition gates on needs.agent.result != 'skipped', which will also evaluate true when the agent job is cancelled. Previously (when checking == 'success') this job would not run on cancellations; with always() it can now attempt to push repo memory for cancelled runs, risking spurious commits/runner usage. Consider adding !cancelled() to the job-level condition (similar to pkg/workflow/compiler_safe_outputs_job.go:451-457) so push_repo_memory does not run after workflow cancellation.

See below for a potential fix:

	// Also require !cancelled() so the job does not run after workflow cancellation.
	agentRan := BuildNotEquals(
		BuildPropertyAccess(fmt.Sprintf("needs.%s.result", constants.AgentJobName)),
		BuildStringLiteral("skipped"),
	)
	notCancelled := BuildNot(BuildFunctionCall("cancelled"))
	jobNeeds := []string{string(constants.AgentJobName), string(constants.ActivationJobName)}
	var jobCondition string
	if threatDetectionEnabled {
		// When threat detection is enabled, also require detection passed (succeeded or skipped).
		jobCondition = RenderCondition(BuildAnd(BuildAnd(BuildAnd(BuildFunctionCall("always"), notCancelled), buildDetectionPassedCondition()), agentRan))
		jobNeeds = append(jobNeeds, string(constants.DetectionJobName))
	} else {
		jobCondition = RenderCondition(BuildAnd(BuildAnd(BuildFunctionCall("always"), notCancelled), agentRan))

Copilot uses AI. Check for mistakes.
Comment on lines +1304 to +1324
assert.Contains(t, pushJob.If, "always()",
"Condition should contain always() to bypass normal skip propagation")
assert.Contains(t, pushJob.If, "!= 'skipped'",
"Condition should check agent result != 'skipped' to gate on agent having run")
assert.NotContains(t, pushJob.If, "== 'success'",
"Condition should NOT use == 'success' — agent failures should still push memory")
})

t.Run("with threat detection", func(t *testing.T) {
pushJob, err := compiler.buildPushRepoMemoryJob(data, true)
require.NoError(t, err, "Should build push job without error")
require.NotNil(t, pushJob, "Should produce a push job")

assert.Contains(t, pushJob.If, "always()",
"Condition should contain always()")
assert.Contains(t, pushJob.If, "!= 'skipped'",
"Condition should check agent result != 'skipped'")
assert.Contains(t, pushJob.If, "needs.detection.result",
"Condition should still check detection result when threat detection is enabled")
assert.NotContains(t, pushJob.If, "needs.agent.result == 'success'",
"Condition should NOT use == 'success' for agent check")
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

The new test asserts on substrings only; if you add a cancellation guard (e.g. !cancelled()), it would be good for this test to assert it explicitly so cancellations don’t regress. Also consider asserting the full expected pushJob.If for both branches to make the condition semantics unambiguous.

Copilot uses AI. Check for mistakes.
Comment on lines 739 to 750
// Job condition: only run when the agent actually executed (not skipped).
// Using always() so the job still runs even when upstream jobs are skipped
// (e.g. detection is skipped when agent produces no outputs).
// We check != 'skipped' rather than == 'success' so that repo-memory is
// pushed even when the agent fails — partial memory data is still valuable.
// Crucially, this prevents the job from running on no-op workflow invocations
// (e.g. bot comments) where pre_activation is skipped and the skip cascades
// through activation → agent → detection.
agentRan := BuildNotEquals(
BuildPropertyAccess(fmt.Sprintf("needs.%s.result", constants.AgentJobName)),
BuildStringLiteral("success"),
BuildStringLiteral("skipped"),
)
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

Minor consistency: this variable represents needs.agent.result != 'skipped' (matching the common name agentNotSkipped used elsewhere, e.g. pkg/workflow/threat_detection.go:746 and pkg/workflow/compiler_safe_outputs_job.go:451). Renaming agentRan to agentNotSkipped would align terminology and reduce ambiguity (especially if you later add !cancelled() and the job is no longer strictly “agent ran”).

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 90/100

Excellent test quality

Metric Value
New/modified tests analyzed 1 (with 2 sub-cases via t.Run)
✅ Design tests (behavioral contracts) 1 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 1 (100%)
Duplicate test clusters 0
Test inflation detected ⚠️ YES — 43 test lines added vs 12 production lines (3.6:1)
🚨 Coding-guideline violations None

Test Classification Details

Test File Classification Issues Detected
TestPushRepoMemoryJobConditionGatesOnAgentNotSkipped pkg/workflow/repo_memory_test.go:1284 ✅ Design Test inflation (43 vs 12 lines added) — see note below

Test Analysis

TestPushRepoMemoryJobConditionGatesOnAgentNotSkipped

Classification: Design test (behavioral contract)

What design invariant does this test enforce?
It enforces the contract that buildPushRepoMemoryJob generates a job condition using != 'skipped' (not == 'success'), ensuring repo-memory is pushed after agent failures and is suppressed only on no-op invocations where the agent was skipped entirely. Both the non-threat-detection and threat-detection paths are verified.

What would break if deleted?
A regression back to == 'success' (the old behavior) would go completely undetected. This is precisely the bug the PR fixes — the test is the safety net that prevents it from being reintroduced.

Quality notes:

  • ✅ Two sub-cases via t.Run cover both code branches (threatDetectionEnabled=false and true)
  • require.NoError / require.NotNil guard setup; assert.Contains and assert.NotContains verify the behavioral contract
  • NotContains(t, ..., "== 'success'") explicitly pins the old (buggy) behavior as forbidden — high-value negative assertion
  • ✅ All assertions carry descriptive messages
  • ✅ Build tag //go:build !integration present on line 1
  • ✅ No mock libraries used

i️ Test inflation note: 43 test lines were added vs 12 production lines (3.6:1 ratio, threshold 2:1). This is a cosmetic flag only — the production change is a targeted one-line logic fix (== 'success'!= 'skipped'), while the test comprehensively covers both code branches with clear assertions. The inflation is justified by the coverage breadth, not by padding.


Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 1 test function — unit (//go:build !integration)
  • 🟨 JavaScript (*.test.cjs, *.test.js): 0 tests

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). The added test directly enforces the behavioral contract introduced by this fix.


📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

🧪 Test quality analysis by Test Quality Sentinel · ● 386.2K ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

✅ Test Quality Sentinel: 90/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). The added test TestPushRepoMemoryJobConditionGatesOnAgentNotSkipped is a strong behavioral contract test that directly pins the fix against regression.

@github-actions github-actions bot mentioned this pull request Apr 13, 2026
@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot review all comments

Address review feedback:
1. Add !cancelled() to prevent push_repo_memory from running after
   workflow cancellation (consistent with compiler_safe_outputs_job.go)
2. Rename agentRan to agentNotSkipped for consistency with the naming
   convention used elsewhere in the codebase
3. Update test to assert full expected condition string and verify
   !cancelled() presence

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/b01603df-bac9-4013-8fe3-6f9d9e8a2d41

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 13, 2026

@copilot review all comments

Addressed all three review comments in 1638f4e:

  1. Added !cancelled() guard — prevents push_repo_memory from running after workflow cancellation, consistent with compiler_safe_outputs_job.go
  2. Renamed agentRanagentNotSkipped — aligns with naming convention used in threat_detection.go, compiler_safe_outputs_job.go, and expression_builder.go
  3. Updated test assertions — the "without threat detection" subtest now asserts the full expected condition string (always() && (!cancelled()) && needs.agent.result != 'skipped'), and the "with threat detection" subtest explicitly checks for !cancelled()

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/graphql
    • Triggering command: /usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw (http block)
    • Triggering command: /usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw -embedcfg /tmp/go-build3527440204/b063/embedcfg -pack (http block)
    • Triggering command: /usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw /lcs (http block)
  • https://api.github.com/repos/actions/upload-artifact/git/ref/tags/v7
    • Triggering command: /usr/bin/gh gh api /repos/actions/upload-artifact/git/ref/tags/v7 --jq .object.sha (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/upload-artifact/git/ref/tags/v7 --jq .object.sha fflib@v1.0.0/difflib/difflib.go (http block)
    • Triggering command: /usr/bin/gh gh api /repos/actions/upload-artifact/git/ref/tags/v7 --jq .object.sha to pkg/workflow/data/action_pins.json...&#34; .cfg 64/pkg/tool/linux_amd64/vet HooksPath (http block)
  • https://api.github.com/repos/astral-sh/setup-uv/git/ref/tags/eac588ad8def6316056a12d4907a9d4d84ff7a3b
    • Triggering command: /usr/bin/gh gh api /repos/astral-sh/setup-uv/git/ref/tags/eac588ad8def6316056a12d4907a9d4d84ff7a3b --jq .object.sha (http block)
  • https://api.github.com/repos/docker/build-push-action/git/ref/tags/v7
    • Triggering command: /usr/bin/gh gh api /repos/docker/build-push-action/git/ref/tags/v7 --jq .object.sha ew@v1.1.1/spew/b-f ew@v1.1.1/spew/cowner=github 64/pkg/tool/linu-f ame (http block)
    • Triggering command: /usr/bin/gh gh api /repos/docker/build-push-action/git/ref/tags/v7 --jq .object.sha /x/exp/golden@v0.0.0-20251215102-errorsas (http block)
  • https://api.github.com/repos/githubnext/agentics/git/ref/tags/-
    • Triggering command: /usr/bin/gh gh api /repos/githubnext/agentics/git/ref/tags/- --jq .object.sha (http block)

If you need me to access, download, or install something from one of these locations, you can either:

@pelikhan pelikhan merged commit 6e82007 into main Apr 13, 2026
@pelikhan pelikhan deleted the copilot/fix-push-repo-memory-job branch April 13, 2026 02:13
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.

bug: push_repo_memory job runs when workflow is triggered by a bot comment (pre_activation skipped)

3 participants