Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/agent-performance-analyzer.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/audit-workflows.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/code-scanning-fixer.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/copilot-agent-analysis.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/copilot-cli-deep-research.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/copilot-pr-nlp-analysis.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/copilot-pr-prompt-analysis.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/copilot-session-insights.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/copilot-token-audit.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/copilot-token-optimizer.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/daily-cli-performance.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/daily-code-metrics.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/daily-community-attribution.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/daily-news.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/daily-testify-uber-super-expert.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/deep-report.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/delight.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/developer-docs-consolidator.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/discussion-task-miner.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/firewall-escape.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/glossary-maintainer.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/metrics-collector.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/pr-triage-agent.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/security-compliance.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/technical-doc-writer.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/weekly-blog-post-writer.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/workflow-health-manager.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 12 additions & 7 deletions pkg/workflow/repo_memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -736,21 +736,26 @@ func (c *Compiler) buildPushRepoMemoryJob(data *WorkflowData, threatDetectionEna
steps = append(steps, c.generateRestoreActionsSetupStep())
}

// Job condition: only run if the agent job succeeded (do not push repo memory when agent
// failed or was skipped). Using always() so the job still runs even when upstream jobs
// are skipped (e.g. detection is skipped when agent produces no outputs).
agentSucceeded := BuildEquals(
// 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.
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.

// Build outputs map for validation failures from all memory steps
Expand Down
43 changes: 43 additions & 0 deletions pkg/workflow/repo_memory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1281,3 +1281,46 @@ func TestPushRepoMemoryJobConcurrencyKey(t *testing.T) {
assert.NotContains(t, pushJob.Concurrency, "push-repo-memory-${{ github.repository }}\"",
"Concurrency key should not be the old repo-wide-only key format")
}

// TestPushRepoMemoryJobConditionGatesOnAgentNotSkipped verifies that the push_repo_memory
// job condition uses needs.agent.result != 'skipped' so the job does not run on no-op
// workflow invocations (e.g. bot comments where pre_activation is skipped).
func TestPushRepoMemoryJobConditionGatesOnAgentNotSkipped(t *testing.T) {
data := &WorkflowData{
RepoMemoryConfig: &RepoMemoryConfig{
Memories: []RepoMemoryEntry{
{ID: "default", BranchName: "memory/my-workflow"},
},
},
}

compiler := NewCompiler()

t.Run("without threat detection", func(t *testing.T) {
pushJob, err := compiler.buildPushRepoMemoryJob(data, false)
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() 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.
})
}
Loading