From 45d523c2287b7faa0658e1d06dc2b77834de6f37 Mon Sep 17 00:00:00 2001 From: zoetaka38 Date: Tue, 7 Jul 2026 09:10:56 +0200 Subject: [PATCH] fix(ci): tolerate services:/container: so real workflows trigger Found by pushing the real Corevice/open-git repo to a self-hosted instance: its ci.yml (and any workflow using `services:`) never triggered. The workflow parser declared `Job.Services` as map[string]string and `Job.Container` as string, but GitHub's `services.` is always a mapping (image/env/ports/ options) and `container` may be a mapping too. Decoding failed with "cannot unmarshal !!map into string", so ParseWorkflow returned an error and the trigger silently skipped the whole workflow. These fields aren't executed by our simplified runner, so capture them as raw yaml.Node instead of rejecting them. All eight real open-git workflows now parse (previously ci.yml, and any with services, did not). Regression test covers a workflow with a map-form container and a postgres service parsing via both ParseWorkflow and ParseWorkflowFull. --- .../infrastructure/workflow/parser.go | 8 +++- .../infrastructure/workflow/parser_test.go | 47 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/backend/internal/infrastructure/workflow/parser.go b/backend/internal/infrastructure/workflow/parser.go index 6a1807c1..44255f53 100644 --- a/backend/internal/infrastructure/workflow/parser.go +++ b/backend/internal/infrastructure/workflow/parser.go @@ -37,8 +37,12 @@ type Job struct { Env map[string]string `yaml:"env"` Outputs map[string]string `yaml:"outputs"` TimeoutMinutes int `yaml:"timeout-minutes"` - Container string `yaml:"container"` - Services map[string]string `yaml:"services"` + // Container and Services accept any shape GitHub allows (a scalar image or a + // full mapping with image/env/ports/options). They are captured but not yet + // executed; keeping them as raw nodes stops otherwise-valid real workflows + // from failing to parse (and therefore never triggering). + Container yaml.Node `yaml:"container"` + Services yaml.Node `yaml:"services"` } type StrategyConfig struct { diff --git a/backend/internal/infrastructure/workflow/parser_test.go b/backend/internal/infrastructure/workflow/parser_test.go index ca70b7cf..f2ef3c50 100644 --- a/backend/internal/infrastructure/workflow/parser_test.go +++ b/backend/internal/infrastructure/workflow/parser_test.go @@ -50,6 +50,53 @@ jobs: } } +// TestParseWorkflowWithServicesAndContainer guards that real-world workflows +// using `services:` (a map of objects) and a map-form `container:` parse +// successfully. These fields aren't executed, but rejecting them made valid +// workflows fail to parse and therefore never trigger. +func TestParseWorkflowWithServicesAndContainer(t *testing.T) { + yamlSrc := []byte(`name: CI +on: [push] +jobs: + test: + container: + image: node:20 + env: + NODE_ENV: test + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready" + steps: + - run: echo hi +`) + wf, err := ParseWorkflow(yamlSrc) + if err != nil { + t.Fatalf("ParseWorkflow rejected services/container workflow: %v", err) + } + if _, ok := wf.Jobs["test"]; !ok { + t.Fatal("expected job 'test'") + } + + ir, diags, ferr := ParseWorkflowFull(yamlSrc) + if ferr != nil { + t.Fatalf("ParseWorkflowFull error: %v", ferr) + } + for _, d := range diags { + if d.Severity == "error" { + t.Fatalf("unexpected parse error diagnostic: %s", d.Message) + } + } + if ir == nil || len(ir.Jobs) != 1 { + t.Fatalf("expected 1 job in IR, got %v", ir) + } +} + func TestParseInvalidYAML(t *testing.T) { yamlSrc := []byte("name: CI\njobs:\n build:\n steps: [\n - run: echo ok\n")