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
2 changes: 2 additions & 0 deletions backend/internal/infrastructure/workflow/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ type WorkflowIR struct {
type IRJob struct {
RunsOn string
Needs []string
If string
MatrixExpansion []map[string]any
Steps []IRStep
Env map[string]string
Expand Down Expand Up @@ -287,6 +288,7 @@ func ParseWorkflowFull(data []byte) (*WorkflowIR, []Diagnostic, error) {
irJob := IRJob{
RunsOn: nodeToString(job.RunsOn),
Needs: decodeNeeds(job.Needs),
If: job.If,
Env: job.Env,
}

Expand Down
62 changes: 47 additions & 15 deletions backend/internal/worker/ci_worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,31 +323,63 @@ func (w *CIWorker) HandleCIRun(ctx context.Context, task *asynq.Task) error {
order = sortedJobNames(ir.Jobs)
}

// skipJob records a job as skipped (status completed / conclusion skipped)
// and notes the reason. Used for both unmet `needs` and a false job-level
// `if:`.
skipJob := func(jobName, reason string) error {
jobResults[jobName] = jobResultSkipped
jobLogStatus[jobName] = jobResultSkipped
fmt.Fprintf(logBuf, "[job=%s] skipped: %s\n", jobName, reason)
if w.jobRepo != nil {
skipUUID := int64CompatibleUUID()
jobIDs[jobName] = skipUUID.String()
now := time.Now().UTC()
if createErr := w.jobRepo.Create(ctx, &entity.WorkflowJob{
ID: skipUUID, WorkflowRunID: &runUUID, OrganizationID: orgUUID, RepositoryID: repoUUID,
Name: jobName, Status: entity.WorkflowJobStatusInProgress, StartedAt: &now, CreatedAt: now,
}); createErr != nil {
return fmt.Errorf("create workflow job: %w", createErr)
}
if completeErr := w.jobRepo.Complete(ctx, skipUUID, entity.WorkflowJobConclusionSkipped, time.Now().UTC()); completeErr != nil {
return fmt.Errorf("complete workflow job: %w", completeErr)
}
}
return nil
}

for _, jobName := range order {
job := ir.Jobs[jobName]

// A job whose dependencies did not all succeed is skipped, like GitHub
// Actions. The dependency's own failure already set the run conclusion,
// so a skip adds no new failure — but independent jobs still run.
if dep, unmet := firstUnsatisfiedNeed(job.Needs, jobResults); unmet {
jobResults[jobName] = jobResultSkipped
jobLogStatus[jobName] = jobResultSkipped
fmt.Fprintf(logBuf, "[job=%s] skipped: dependency %q did not succeed\n", jobName, dep)
if w.jobRepo != nil {
skipUUID := int64CompatibleUUID()
jobIDs[jobName] = skipUUID.String()
now := time.Now().UTC()
if createErr := w.jobRepo.Create(ctx, &entity.WorkflowJob{
ID: skipUUID, WorkflowRunID: &runUUID, OrganizationID: orgUUID, RepositoryID: repoUUID,
Name: jobName, Status: entity.WorkflowJobStatusInProgress, StartedAt: &now, CreatedAt: now,
}); createErr != nil {
return fmt.Errorf("create workflow job: %w", createErr)
if err := skipJob(jobName, fmt.Sprintf("dependency %q did not succeed", dep)); err != nil {
return err
}
continue
}

// A false job-level `if:` skips the whole job. Evaluate it against the
// github context and the workflow+job env.
if job.If != "" {
jobEnv := mergeStringMaps(ir.Env, job.Env)
ec := &workflow.EvalContext{Contexts: map[string]map[string]string{
"github": githubCtx, "runner": runnerCtx, "secrets": secretsCtx, "env": jobEnv, "matrix": {},
}}
run, ifErr := workflow.EvaluateCondition(job.If, ec)
if ifErr != nil {
if err := skipJob(jobName, "invalid job if: "+ifErr.Error()); err != nil {
return err
}
if completeErr := w.jobRepo.Complete(ctx, skipUUID, entity.WorkflowJobConclusionSkipped, time.Now().UTC()); completeErr != nil {
return fmt.Errorf("complete workflow job: %w", completeErr)
continue
}
if !run {
if err := skipJob(jobName, fmt.Sprintf("job if condition %q evaluated false", job.If)); err != nil {
return err
}
continue
}
continue
}

// Expand the matrix into one instance per combination; a job without a
Expand Down
21 changes: 21 additions & 0 deletions backend/internal/worker/ci_worker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,27 @@ jobs:
}
}

func TestCIJobLevelIfSkipsWholeJob(t *testing.T) {
scripts := runWorkflowCapture(t, `name: CI
on: push
jobs:
always:
steps:
- run: echo ALWAYS_JOB
gated:
if: github.ref_name == 'nope'
steps:
- run: echo GATED_SHOULD_NOT_RUN
`, func(p *CIRunPayload) { p.HeadBranch = "main" })
joined := strings.Join(scripts, "\n")
if strings.Contains(joined, "GATED_SHOULD_NOT_RUN") {
t.Errorf("job with false job-level if: ran; scripts=%v", scripts)
}
if !strings.Contains(joined, "ALWAYS_JOB") {
t.Errorf("ungated job did not run; scripts=%v", scripts)
}
}

func TestCIMatrixExpandsAndInterpolates(t *testing.T) {
scripts := runWorkflowCapture(t, `name: CI
on: push
Expand Down
Loading