Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
65 changes: 65 additions & 0 deletions internal/generate/external_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -657,3 +657,68 @@ func TestExternalUpdateGenerator_ConcurrencyOverride(t *testing.T) {
assert.Contains(t, content, "group: my-custom-external", "custom group must propagate to external-update")
assert.Contains(t, content, "cancel-in-progress: true", "custom cancel_in_progress must propagate to external-update")
}

// TestNotifyPrimaryStep_BuildOnlySatellite_EmitsDeployName verifies that a
// build-only satellite (builds + notify, zero deploys) still emits a non-empty
// deploy_name in the dispatch to the primary's external-update workflow. The
// consumer declares deploy_name as required: true, so omitting it produces a
// live 422 "Required input 'deploy_name' not provided". The fallback is the
// first build name, which represents this satellite's artifact.
func TestNotifyPrimaryStep_BuildOnlySatellite_EmitsDeployName(t *testing.T) {
baseDir := t.TempDir()
createMockWorkflow(t, baseDir, ".github/workflows/build-shared.yaml")

cfg := &config.TrunkConfig{
TrunkBranch: "main",
Environments: []string{"dev"},
Builds: []config.BuildConfig{
{Name: "shared", Workflow: ".github/workflows/build-shared.yaml", Triggers: []string{"src/**"}},
},
// No deploys: this is a build-only artifact satellite.
Notify: &config.NotifyConfig{
Repo: "example/primary-backend",
Workflow: ".github/workflows/external-update.yaml",
},
}

require.True(t, cfg.IsSatellite())
require.Empty(t, cfg.Deploys)

gen := NewGenerator(cfg, baseDir)
content, err := gen.Generate()
require.NoError(t, err)

require.Contains(t, content, "Notify Primary Repo")
// The dispatch inputs MUST include a non-empty deploy_name. Falls back to
// the first build name when there are no deploys.
assert.Contains(t, content, "deploy_name: 'shared',",
"build-only satellite must emit a non-empty deploy_name (consumer requires it)")
}

// TestNotifyPrimaryStep_NoStrayDotInJobsAccessor verifies the github-script
// context accessor uses context.jobs['id'] form with no stray dot before the
// bracket. The earlier context.jobs.['id'] form is invalid JavaScript and would
// fail at runtime for any satellite that collects deploy outputs.
func TestNotifyPrimaryStep_NoStrayDotInJobsAccessor(t *testing.T) {
baseDir := t.TempDir()
createMockWorkflow(t, baseDir, ".github/workflows/deploy-cdk.yaml")

cfg := &config.TrunkConfig{
TrunkBranch: "main",
Environments: []string{"dev"},
Deploys: []config.DeployConfig{
{Name: "cdk", Workflow: ".github/workflows/deploy-cdk.yaml", Triggers: []string{"cdk/**"}},
},
Notify: &config.NotifyConfig{
Repo: "example/primary-backend",
Workflow: ".github/workflows/external-update.yaml",
},
}

gen := NewGenerator(cfg, baseDir)
content, err := gen.Generate()
require.NoError(t, err)

assert.NotContains(t, content, "context.jobs.[",
"github-script accessor must not have a stray dot before the bracket")
}
15 changes: 12 additions & 3 deletions internal/generate/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -1596,7 +1596,7 @@ func (g *Generator) writeNotifyPrimaryStep(sb *strings.Builder) {
for _, d := range g.config.Deploys {
jobID := config.JobID(config.CallbackTypeDeploy, d.Name)
for _, out := range g.outputs[jobID] {
fmt.Fprintf(sb, " if (context.jobs.['%s']?.outputs?.%s) {\n", jobID, out)
fmt.Fprintf(sb, " if (context.jobs['%s']?.outputs?.%s) {\n", jobID, out)
fmt.Fprintf(sb, " artifacts['%s'] = context.jobs['%s'].outputs.%s;\n", out, jobID, out)
sb.WriteString(" }\n")
}
Expand All @@ -1612,9 +1612,18 @@ func (g *Generator) writeNotifyPrimaryStep(sb *strings.Builder) {
sb.WriteString(" inputs: {\n")
sb.WriteString(" source_repo: context.repo.owner + '/' + context.repo.repo,\n")

// Use first deploy name as the deploy_name (satellites typically have one deploy)
if len(g.config.Deploys) > 0 {
// deploy_name is required: true by the primary's external-update consumer, so
// it must always be present and non-empty. Prefer the first deploy name
// (satellites typically have one deploy); fall back to the first build name
// for build-only artifact satellites; otherwise use the satellite's own repo
// name from the runtime context as a last-resort identifier.
switch {
case len(g.config.Deploys) > 0:
fmt.Fprintf(sb, " deploy_name: '%s',\n", g.config.Deploys[0].Name)
case len(g.config.Builds) > 0:
fmt.Fprintf(sb, " deploy_name: '%s',\n", g.config.Builds[0].Name)
default:
sb.WriteString(" deploy_name: context.repo.repo,\n")
}

if len(g.config.Environments) > 0 {
Expand Down
Loading