Uh oh!
There was an error while loading. Please reload this page.
fix(compiler): auto-add pre_activation to safe_outputs/conclusion needs when messages reference its outputs - #43570
Conversation
…n messages reference it Fixes actionlint errors in skillet.lock.yml at lines 1385, 1411, 1882. The safe_outputs and conclusion jobs referenced needs.pre_activation.outputs.* in GH_AW_SAFE_OUTPUT_MESSAGES but did not declare pre_activation in their needs. - Add messagesContainPreActivationRef() helper in compiler_safe_outputs_job.go - Auto-add pre_activation to safe_outputs job needs when messages reference it - Auto-add pre_activation to conclusion job needs when messages reference it - Recompile all workflows (skillet.lock.yml now correct) Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
pre_activation to safe_outputs/conclusion needs when messages reference its outputs✅ PR Code Quality Reviewer completed the code quality review. |
✅ Design Decision Gate 🏗️ completed the design decision gate check. No ADR enforcement needed: PR #43570 does not have the 'implementation' label and has ≤100 new lines of code in business logic directories (60 additions across 3 files). |
🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅ |
✅ Test Quality Sentinel completed test quality analysis. No test files were added or modified in this PR. Test Quality Sentinel skipped. |
There was a problem hiding this comment.
Pull request overview
Fixes a workflow compilation gap where safe_outputs and conclusion jobs could embed ${{ needs.pre_activation.outputs.* }} inside GH_AW_SAFE_OUTPUT_MESSAGES without declaring pre_activation in needs, causing actionlint expression errors in generated lock YAML.
Changes:
- Added a helper to detect
needs.pre_activation.outputs.*references insafe-outputs.messagestemplates and conditionally wirepre_activationinto jobneeds. - Applied this conditional dependency wiring to both the
safe_outputsjob and theconclusionjob (with deduping forconclusion). - Recompiled
skillet.lock.ymlto reflect the correctedneedslists.
Show a summary per file
| File | Description |
|---|---|
| pkg/workflow/compiler_safe_outputs_job.go | Detects needs.pre_activation.outputs.* in message templates and auto-adds pre_activation to safe_outputs job needs. |
| pkg/workflow/notify_comment.go | Applies the same conditional needs wiring to the conclusion job, with slices.Contains dedup. |
| .github/workflows/skillet.lock.yml | Regenerated lock output showing pre_activation added to safe_outputs and conclusion job needs. |
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: 2
- Review effort level: Low
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Skills-Based Review 🧠
Applied /diagnosing-bugs, /tdd, and /codebase-design — requesting changes on two maintainability risks.
📋 Key Themes & Highlights
Key Themes
- Missing regression protection:
messagesContainPreActivationRef— the heart of the fix — has no unit tests. A future contributor adding a field toSafeOutputMessagesConfigwon't know to update the scanned list, reintroducing this class of bug silently. - Manually maintained field list: The slice of 16 field values in the helper is a structural copy of the struct. Using reflection in a coverage test (or in the helper itself) would make omissions a compile/test failure rather than a runtime surprise.
- Duplicated guard pattern with inconsistent dedup primitive: The 9-line block (nil check → jobManager check → dedup → append) appears verbatim in two files, but uses
setutil.Containsin one place andslices.Containsin the other. ExtractingappendPreActivationNeedIfRequiredwould remove the duplication and standardise the primitive.
Positive Highlights
- ✅ Root cause properly diagnosed: the serializer embeds message templates verbatim, so the dependency must be wired at compile time.
- ✅ The fix is general and automatic — no per-workflow markdown changes needed.
- ✅
jobManager.GetJobguard is correctly placed to avoid wiring the dep when the job doesn't exist. - ✅ Both affected jobs (
safe_outputsandconclusion) are covered. - ✅ The lock file recompilation confirms end-to-end correctness.
🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 62.6 AIC · ⌖ 5.37 AIC · ⊞ 6.7K
Comment /matt to run again
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
| needs := buildConclusionJobNeeds(data, mainJobName, safeOutputJobNames) | ||
| // If any message template references needs.pre_activation.outputs.*, add pre_activation | ||
| // as a dependency so that GitHub Actions can resolve the expression at runtime. | ||
| if data.SafeOutputs != nil && messagesContainPreActivationRef(data.SafeOutputs.Messages) { |
There was a problem hiding this comment.
[/codebase-design] The guard block here is structurally identical to the one in compiler_safe_outputs_job.go (lines 639–647). The same pattern — check nil, check jobManager, dedup-append — is now duplicated across two files with slightly different dedup primitives (setutil.Contains vs slices.Contains). Consider extracting a shared helper or at minimum aligning the two call sites on the same dedup primitive to reduce cognitive load.
💡 Suggested refactor direction
A helper like:
// appendPreActivationNeedIfRequired appends pre_activation to needs if:// 1. any message template references needs.pre_activation.outputs.*// 2. the pre_activation job actually exists in the job manager// It is a no-op if pre_activation is already present.func (c*Compiler) appendPreActivationNeedIfRequired(
data*WorkflowData, needs []string,
) []string {
ifdata.SafeOutputs==nil||!messagesContainPreActivationRef(data.SafeOutputs.Messages) {
returnneeds
}
name:=string(constants.PreActivationJobName)
if_, exists:=c.jobManager.GetJob(name); !exists {
returnneeds
}
ifslices.Contains(needs, name) {
returnneeds
}
returnappend(needs, name)
}Both call sites collapse to a single line and the dedup primitive is consistent.
@copilot please address this.
There was a problem hiding this comment.
Review
The fix is correct and addresses the root cause: safe_outputs and conclusion jobs embedding needs.pre_activation.outputs.* expressions without declaring the dependency.
One non-blocking concern:messagesContainPreActivationRef enumerates all string fields of SafeOutputMessagesConfig by hand. Currently the only omission is AppendOnlyComments (a bool field, which cannot contain such a reference — fine). However, if a new string field is added to the struct in the future, this function will silently miss it — the compiler will not warn and no existing test will fail. Using reflect to iterate string-typed fields, or adding a canary test that asserts the hardcoded field count matches the struct's actual string-field count, would keep the scan complete automatically.
Everything else looks good: the seenNeeds dedup in compiler_safe_outputs_job.go is consistent with existing patterns, slices.Contains in notify_comment.go is the right choice for a plain slice, the jobManager.GetJob guard prevents wiring when pre_activation was never created, and the recompiled skillet.lock.yml diff confirms the intended effect.
Checked
SafeOutputMessagesConfigfields vs scanned fields: onlyAppendOnlyComments(bool) absent — expected- Dedup logic in both jobs: correct
buildConclusionJobNeedsdoes not already insertpre_activation— new code is the only insertion point- No existing test covers the new code path (inline comment filed)
🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 88.1 AIC · ⌖ 6.47 AIC · ⊞ 4.9K
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Verdict: request changes — two correctness bugs and missing test coverage
The logic is sound for the happy path (skillet.lock.yml confirms it compiles correctly when pre_activation exists), but there are two silent-failure bugs and a structural fragility that will cause problems as the codebase evolves.
Blocking issues (2)
1. Silent no-op when pre_activation job is absent (both paths)
Both buildSafeOutputsJobFromParts and buildConclusionJob silently skip adding the needs edge when GetJob(PreActivationJobName) returns false, with no error. A workflow that has needs.pre_activation.outputs.skill_name in a footer template but no pre_activation job will compile without error, emit a valid-looking YAML, and then produce empty/broken message bodies at runtime. The fix is to return a fmt.Errorf in both paths when the job is absent — the compiler already propagates errors from both call sites.
2. No unit tests for the new correctness-critical functionmessagesContainPreActivationRef has zero test coverage despite gating the needs-edge generation. The pre_activation injection in buildConclusionJob also has no tests. Correctness regressions here produce silently broken compiled workflows.
Non-blocking issues (2)
3. Field enumeration will silently miss future struct fields
The hard-coded string-field slice in messagesContainPreActivationRef will silently skip any new fields added to SafeOutputMessagesConfig. A reflection-based exhaustiveness test (or using reflect.Value iteration in the function itself) would catch this at test time.
4. Inconsistent deduplication between the two pathssafe_outputs uses seenNeeds map[string]struct{}, conclusion uses slices.Contains. Both work, but the inconsistency is a maintenance hazard.
🔎 Code quality review by PR Code Quality Reviewer · 178.9 AIC · ⌖ 8.38 AIC · ⊞ 5.4K
Comment /review to run again
Uh oh!
There was an error while loading. Please reload this page.
| // If any message template references needs.pre_activation.outputs.*, add pre_activation | ||
| // as a dependency so that GitHub Actions can resolve the expression at runtime. | ||
| if data.SafeOutputs != nil && messagesContainPreActivationRef(data.SafeOutputs.Messages) { | ||
| if _, exists := c.jobManager.GetJob(string(constants.PreActivationJobName)); exists { |
There was a problem hiding this comment.
Same silent failure in conclusion job path: when GetJob(PreActivationJobName) returns false, the conclusion job is emitted with unresolvable needs.pre_activation.outputs.* template expressions and no error. This is the same bug as the safe_outputs path above.
💡 Suggested fix
Mirror the fix from the safe_outputs path — return an error instead of a no-op:
ifdata.SafeOutputs!=nil&&messagesContainPreActivationRef(data.SafeOutputs.Messages) {
if_, exists:=c.jobManager.GetJob(string(constants.PreActivationJobName)); !exists {
returnnil, fmt.Errorf(
"safe-outputs messages reference %s outputs but no %s job exists",
constants.PreActivationJobName, constants.PreActivationJobName,
)
}
preActName:=string(constants.PreActivationJobName)
if!slices.Contains(needs, preActName) {
needs=append(needs, preActName)
}
}Note: the conclusion job already returns (*Job, error), so the error return is already wired up.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
| if data.SafeOutputs != nil && messagesContainPreActivationRef(data.SafeOutputs.Messages) { | ||
| if _, exists := c.jobManager.GetJob(string(constants.PreActivationJobName)); exists { | ||
| preActName := string(constants.PreActivationJobName) | ||
| if !slices.Contains(needs, preActName) { |
There was a problem hiding this comment.
Inconsistent deduplication strategy: the safe_outputs path uses an O(1) seenNeeds map[string]struct{}, while this conclusion-job path uses slices.Contains (O(n) linear scan). Both are correct for current list sizes, but the inconsistency is a maintenance hazard — contributors will copy one pattern without knowing there are two, and future code audits will waste time questioning whether the difference is intentional.
💡 Suggested fix
Either:
- Extract a shared
addUniqueNeed(needs *[]string, seen map[string]struct{}, name string)helper that both call-sites use, or - Build a local
seenNeedsmap inbuildConclusionJobusing the same approach asbuildSafeOutputsJobFromParts.
The map-based approach is also naturally extensible if the needs list ever grows large (e.g. multi-job workflows with many safe-output dependencies).
gh-aw-bot
commented
Jul 5, 2026
@copilot please run the
|
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Done. Added |
gh-aw-bot
commented
Jul 5, 2026
@copilot please run the skill, address unresolved review comments, and rerun checks once the branch is up to date. Unresolved review feedback to address:
|
The two review threads are already addressed — |
🎉 This pull request is included in a new release. Release: |
skillet.lock.ymlproduced three actionlintexpressionerrors becausesafe_outputsandconclusionjobs embedded${{ needs.pre_activation.outputs.skill_name }}inGH_AW_SAFE_OUTPUT_MESSAGESwithout declaringpre_activationin theirneeds.The compiler serializes
safe-outputs.messagestemplates verbatim into every job that embeds that env var, but the dependency wiring was never propagated forpre_activation(which is explicitly excluded from the normal auto-wiring paths for both jobs).Changes
compiler_safe_outputs_job.go— addsmessagesContainPreActivationRef(*SafeOutputMessagesConfig) boolhelper that scans all message template fields forneeds.pre_activation.outputs.references; used to conditionally appendpre_activationto thesafe_outputsjob's needs list after explicit needs are resolvednotify_comment.go— same guard applied inbuildConclusionJobafterbuildConclusionJobNeeds; usesslices.Containsfor dedupskillet.lock.yml— recompiled;pre_activationnow appears in theneedsof bothsafe_outputsandconclusionThe fix is general: any workflow whose
safe-outputs.messagestemplates referenceneeds.pre_activation.outputs.*will automatically get the dependency wired, without requiring changes to the source markdown.