From 97690d4fccfe328019cec2bf401d355962ffc443 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sat, 30 May 2026 18:22:32 -0400 Subject: [PATCH 1/5] build(deps): bump workflow to v0.67.0 (cigen) Co-Authored-By: Claude Opus 4.8 (1M context) --- go.mod | 24 ++++++++++++------------ go.sum | 52 ++++++++++++++++++++++++++-------------------------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/go.mod b/go.mod index d0b0049..7cf4765 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/GoCodeAlone/workflow-plugin-ci-generator go 1.26.0 require ( - github.com/GoCodeAlone/workflow v0.64.0 + github.com/GoCodeAlone/workflow v0.67.0 google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af ) @@ -11,12 +11,12 @@ require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/DataDog/datadog-go/v5 v5.8.3 // indirect github.com/GoCodeAlone/go-plugin v1.7.0 // indirect - github.com/GoCodeAlone/modular v1.13.0 // indirect - github.com/GoCodeAlone/modular/modules/auth v1.15.0 // indirect - github.com/GoCodeAlone/modular/modules/cache v1.15.0 // indirect - github.com/GoCodeAlone/modular/modules/eventbus/v2 v2.8.0 // indirect - github.com/GoCodeAlone/modular/modules/jsonschema v1.15.0 // indirect - github.com/GoCodeAlone/modular/modules/reverseproxy/v2 v2.8.0 // indirect + github.com/GoCodeAlone/modular v1.13.4 // indirect + github.com/GoCodeAlone/modular/modules/auth v1.17.0 // indirect + github.com/GoCodeAlone/modular/modules/cache v1.17.0 // indirect + github.com/GoCodeAlone/modular/modules/eventbus/v2 v2.10.0 // indirect + github.com/GoCodeAlone/modular/modules/jsonschema v1.17.0 // indirect + github.com/GoCodeAlone/modular/modules/reverseproxy/v2 v2.10.0 // indirect github.com/GoCodeAlone/yaegi v0.17.2 // indirect github.com/IBM/sarama v1.47.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect @@ -34,7 +34,7 @@ require ( github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22 // indirect - github.com/aws/aws-sdk-go-v2/service/kinesis v1.43.4 // indirect + github.com/aws/aws-sdk-go-v2/service/kinesis v1.43.5 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.10 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.16 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20 // indirect @@ -66,7 +66,7 @@ require ( github.com/fatih/color v1.19.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/flowchartsman/retry v1.2.0 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect github.com/fxamacker/cbor/v2 v2.9.2 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect @@ -98,11 +98,11 @@ require ( github.com/hashicorp/memberlist v0.5.4 // indirect github.com/hashicorp/vault/api v1.23.0 // indirect github.com/hashicorp/yamux v0.1.2 // indirect - github.com/itchyny/gojq v0.12.18 // indirect - github.com/itchyny/timefmt-go v0.1.7 // indirect + github.com/itchyny/gojq v0.12.19 // indirect + github.com/itchyny/timefmt-go v0.1.8 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.9.1 // indirect + github.com/jackc/pgx/v5 v5.9.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jcmturner/aescts/v2 v2.0.0 // indirect github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect diff --git a/go.sum b/go.sum index 2663c33..a1fa518 100644 --- a/go.sum +++ b/go.sum @@ -10,20 +10,20 @@ github.com/DataDog/datadog-go/v5 v5.8.3 h1:s58CUJ9s8lezjhTNJO/SxkPBv2qZjS3ktpRSq github.com/DataDog/datadog-go/v5 v5.8.3/go.mod h1:K9kcYBlxkcPP8tvvjZZKs/m1edNAUFzBbdpTUKfCsuw= github.com/GoCodeAlone/go-plugin v1.7.0 h1:EwnhqPlXiNmp85S+MXnKKvm3YlfA6O4NzBb4+GSlEVY= github.com/GoCodeAlone/go-plugin v1.7.0/go.mod h1:HbGQRZUIa+jbDfjsaZIMJYvrz+LnxL0mJpggfynSTMk= -github.com/GoCodeAlone/modular v1.13.0 h1:UfsegfAmPWcPYQOqYZFsw/LNySBmMDcthiOQe5bscqE= -github.com/GoCodeAlone/modular v1.13.0/go.mod h1:b06Pvgcc8HsGxvl30iO39zGH2jIWz467QEj2+OQL2Do= -github.com/GoCodeAlone/modular/modules/auth v1.15.0 h1:pBSkPSf4k4GLSbUQFLuPa+nFbfoJXGzSz9q89VoapZk= -github.com/GoCodeAlone/modular/modules/auth v1.15.0/go.mod h1:vmIm/LQrcURS2p02YwaELb+CZoHPtT0XB0v1i+sj9i4= -github.com/GoCodeAlone/modular/modules/cache v1.15.0 h1:6Y2EJ5S7mb/TjyG/uN6dto5VUYJNDFYULUamRsqAKvo= -github.com/GoCodeAlone/modular/modules/cache v1.15.0/go.mod h1:PRun74dRZKfqlBM+f6QrvI9oa4joUU3j1hisiLyQ+oM= -github.com/GoCodeAlone/modular/modules/eventbus/v2 v2.8.0 h1:buYs0TGNbAZgtTq1Qb+dfmTv3+ZOBIN0HbvVBLyNqxE= -github.com/GoCodeAlone/modular/modules/eventbus/v2 v2.8.0/go.mod h1:329flAKmwrPq2JEwu9iltWv6A83H/Di82Xze+kvdKDw= -github.com/GoCodeAlone/modular/modules/jsonschema v1.15.0 h1:xb1mI4NZkzvNKQ2F6nkyXQvK/kEvvfs1z7FoGf3/LRA= -github.com/GoCodeAlone/modular/modules/jsonschema v1.15.0/go.mod h1:hhGouwAVsonmJ4Lain4jINZ9nZCoc9l9eF3BHbmR8eE= -github.com/GoCodeAlone/modular/modules/reverseproxy/v2 v2.8.0 h1:cvdLHbM/vzvygQTcAWSJsy+dAPzzwWyjzKMmTBFcFIo= -github.com/GoCodeAlone/modular/modules/reverseproxy/v2 v2.8.0/go.mod h1:/9ipMG4qM2CHQ14BfXKdVlYRJelef6M8MFI5TbZv67M= -github.com/GoCodeAlone/workflow v0.64.0 h1:2CpbYPwIqdGDb3xi3YJpwcteIum4ehBSrnRql/1YvB4= -github.com/GoCodeAlone/workflow v0.64.0/go.mod h1:659GGDrw3QJ7b625y9rf8QhKIpt1VCoEG0MxKu5tGQs= +github.com/GoCodeAlone/modular v1.13.4 h1:De4p2qyJSVmstRGno/PM+fPdUCMu/7a9WgU5FUVGDa8= +github.com/GoCodeAlone/modular v1.13.4/go.mod h1:+JEPUYOxGaD332EMZ5PbJCz5rxwvFu4Tm6MrnZT0vxM= +github.com/GoCodeAlone/modular/modules/auth v1.17.0 h1:GbKG6s/2qe6N9YZ8vtvYsNon56MLWECncPxWvAsazSc= +github.com/GoCodeAlone/modular/modules/auth v1.17.0/go.mod h1:E9dDIxiAxIrXK8gn/rEhaqI5OYe6Aw/uGpRyI7iyxj8= +github.com/GoCodeAlone/modular/modules/cache v1.17.0 h1:1cColHYfF7aFZhxBjS4RuZ2wBOYWAIVr5GkJ3nk5VIA= +github.com/GoCodeAlone/modular/modules/cache v1.17.0/go.mod h1:RURzRp+vpRzKR2LjZXwQ4QGc1cvE+53SYODcxY6AR3Y= +github.com/GoCodeAlone/modular/modules/eventbus/v2 v2.10.0 h1:2ljVafd/1LYchF47WrnA1+ji8mcmVXMJ4F5qDrhZZi4= +github.com/GoCodeAlone/modular/modules/eventbus/v2 v2.10.0/go.mod h1:AKLcRGsw5gp2Q1zhuK0TBnMJOsaRe3aJ2OKnLFE2O5w= +github.com/GoCodeAlone/modular/modules/jsonschema v1.17.0 h1:zoWioqUvuNNDfnjHA1sHixdlHfBreJdGhnnEBtxkzI8= +github.com/GoCodeAlone/modular/modules/jsonschema v1.17.0/go.mod h1:GDU/jsD6AddmXKedj0wZwieUIaQsTBSGMzuj+XHXMrw= +github.com/GoCodeAlone/modular/modules/reverseproxy/v2 v2.10.0 h1:+2M/ecyCxDiXfJM4ibcERuu/BBeIbLTQNcVgRsllR64= +github.com/GoCodeAlone/modular/modules/reverseproxy/v2 v2.10.0/go.mod h1:tlVH1mA5yuU8CB7R7+HXIRaBixZoNid6h+5tew5u3FU= +github.com/GoCodeAlone/workflow v0.67.0 h1:jyzzBq3+axqdqpq7+Z8ozckhjxGISIIheyPcoHbu7lk= +github.com/GoCodeAlone/workflow v0.67.0/go.mod h1:4UwFYm1cM8a/AvGNb1CZAuob0b0gq7552sxcNMdDALA= github.com/GoCodeAlone/yaegi v0.17.2 h1:WK6Y6e0t1a6U7r+S2dN3CGWW1PizYD3zO0zneToZPxM= github.com/GoCodeAlone/yaegi v0.17.2/go.mod h1:z5Pr6Wse6QJcQvpgxTxzMAevFarH0N37TG88Y9dprx0= github.com/IBM/sarama v1.47.0 h1:GcQFEd12+KzfPYeLgN69Fh7vLCtYRhVIx0rO4TZO318= @@ -68,8 +68,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8 h1:HtOTYcb github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8/go.mod h1:VsK9abqQeGlzPgUr+isNWzPlK2vKe9INMLWnY65f5Xs= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22 h1:PUmZeJU6Y1Lbvt9WFuJ0ugUK2xn6hIWUBBbKuOWF30s= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22/go.mod h1:nO6egFBoAaoXze24a2C0NjQCvdpk8OueRoYimvEB9jo= -github.com/aws/aws-sdk-go-v2/service/kinesis v1.43.4 h1:3m9iJtMtLq75jKRAfw0kapoHUlbzi0CRVigysBN/FHA= -github.com/aws/aws-sdk-go-v2/service/kinesis v1.43.4/go.mod h1:O2L6vGm4xacEuN2otHFMgn7yXXlgzFKzxrba0fy/yk8= +github.com/aws/aws-sdk-go-v2/service/kinesis v1.43.5 h1:LxgRVyuY+5DEPSX7kmin/V7toE8MWZ9U8n2dqRtX+RE= +github.com/aws/aws-sdk-go-v2/service/kinesis v1.43.5/go.mod h1:eUebEBEqVfOwEyDDDbGauH4PNqDCuepRvTaNbJeWr5w= github.com/aws/aws-sdk-go-v2/service/signin v1.0.10 h1:a1Fq/KXn75wSzoJaPQTgZO0wHGqE9mjFnylnqEPTchA= github.com/aws/aws-sdk-go-v2/service/signin v1.0.10/go.mod h1:p6+MXNxW7IA6dMgHfTAzljuwSKD0NCm/4lbS4t6+7vI= github.com/aws/aws-sdk-go-v2/service/sso v1.30.16 h1:x6bKbmDhsgSZwv6q19wY/u3rLk/3FGjJWyqKcIRufpE= @@ -172,12 +172,12 @@ github.com/flowchartsman/retry v1.2.0 h1:qDhlw6RNufXz6RGr+IiYimFpMMkt77SUSHY5tgF github.com/flowchartsman/retry v1.2.0/go.mod h1:+sfx8OgCCiAr3t5jh2Gk+T0fRTI+k52edaYxURQxY64= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= -github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -329,16 +329,16 @@ github.com/hashicorp/vault/api v1.23.0 h1:gXgluBsSECfRWTSW9niY2jwg2e9mMJc4WoHNv4 github.com/hashicorp/vault/api v1.23.0/go.mod h1:zransKiB9ftp+kgY8ydjnvCU7Wk8i9L0DYWpXeMj9ko= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= -github.com/itchyny/gojq v0.12.18 h1:gFGHyt/MLbG9n6dqnvlliiya2TaMMh6FFaR2b1H6Drc= -github.com/itchyny/gojq v0.12.18/go.mod h1:4hPoZ/3lN9fDL1D+aK7DY1f39XZpY9+1Xpjz8atrEkg= -github.com/itchyny/timefmt-go v0.1.7 h1:xyftit9Tbw+Dc/huSSPJaEmX1TVL8lw5vxjJLK4GMMA= -github.com/itchyny/timefmt-go v0.1.7/go.mod h1:5E46Q+zj7vbTgWY8o5YkMeYb4I6GeWLFnetPy5oBrAI= +github.com/itchyny/gojq v0.12.19 h1:ttXA0XCLEMoaLOz5lSeFOZ6u6Q3QxmG46vfgI4O0DEs= +github.com/itchyny/gojq v0.12.19/go.mod h1:5galtVPDywX8SPSOrqjGxkBeDhSxEW1gSxoy7tn1iZY= +github.com/itchyny/timefmt-go v0.1.8 h1:1YEo1JvfXeAHKdjelbYr/uCuhkybaHCeTkH8Bo791OI= +github.com/itchyny/timefmt-go v0.1.8/go.mod h1:5E46Q+zj7vbTgWY8o5YkMeYb4I6GeWLFnetPy5oBrAI= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc= -github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= From 5dc7f704061e3dfa69ef8abf979ca3cbabd6f5b4 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sat, 30 May 2026 18:23:06 -0400 Subject: [PATCH 2/5] feat(contract): additive from_plan/phase_config inputs Add optional from_plan (field 7) and phase_config (field 8) to both CIGenerateConfig and CIGenerateInput. from_plan allows rendering from a pre-computed CIPlan JSON; phase_config wires a prereq deploy phase. Regenerated pb.go via protoc v35 + protoc-gen-go (paths=source_relative). Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/contracts/ci_generator.pb.go | 52 ++++++++++++++++++++++++--- internal/contracts/ci_generator.proto | 12 +++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/internal/contracts/ci_generator.pb.go b/internal/contracts/ci_generator.pb.go index 100422f..9810c69 100644 --- a/internal/contracts/ci_generator.pb.go +++ b/internal/contracts/ci_generator.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.34.1 +// protoc v7.35.0 // source: internal/contracts/ci_generator.proto package contracts @@ -29,6 +29,12 @@ type CIGenerateConfig struct { ProjectName string `protobuf:"bytes,4,opt,name=project_name,json=projectName,proto3" json:"project_name,omitempty"` Runner string `protobuf:"bytes,5,opt,name=runner,proto3" json:"runner,omitempty"` DefaultBranch string `protobuf:"bytes,6,opt,name=default_branch,json=defaultBranch,proto3" json:"default_branch,omitempty"` + // from_plan is an optional path to a CIPlan JSON file. When set, analysis is + // skipped and the plan is rendered directly from the file. + FromPlan string `protobuf:"bytes,7,opt,name=from_plan,json=fromPlan,proto3" json:"from_plan,omitempty"` + // phase_config is an optional path to a prerequisite workflow config that + // becomes the first DeployPhase (prereq) before the primary phase. + PhaseConfig string `protobuf:"bytes,8,opt,name=phase_config,json=phaseConfig,proto3" json:"phase_config,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -105,6 +111,20 @@ func (x *CIGenerateConfig) GetDefaultBranch() string { return "" } +func (x *CIGenerateConfig) GetFromPlan() string { + if x != nil { + return x.FromPlan + } + return "" +} + +func (x *CIGenerateConfig) GetPhaseConfig() string { + if x != nil { + return x.PhaseConfig + } + return "" +} + type CIGenerateInput struct { state protoimpl.MessageState `protogen:"open.v1"` Platform string `protobuf:"bytes,1,opt,name=platform,proto3" json:"platform,omitempty"` @@ -113,6 +133,12 @@ type CIGenerateInput struct { ProjectName string `protobuf:"bytes,4,opt,name=project_name,json=projectName,proto3" json:"project_name,omitempty"` Runner string `protobuf:"bytes,5,opt,name=runner,proto3" json:"runner,omitempty"` DefaultBranch string `protobuf:"bytes,6,opt,name=default_branch,json=defaultBranch,proto3" json:"default_branch,omitempty"` + // from_plan is an optional path to a CIPlan JSON file. When set, analysis is + // skipped and the plan is rendered directly from the file. + FromPlan string `protobuf:"bytes,7,opt,name=from_plan,json=fromPlan,proto3" json:"from_plan,omitempty"` + // phase_config is an optional path to a prerequisite workflow config that + // becomes the first DeployPhase (prereq) before the primary phase. + PhaseConfig string `protobuf:"bytes,8,opt,name=phase_config,json=phaseConfig,proto3" json:"phase_config,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -189,6 +215,20 @@ func (x *CIGenerateInput) GetDefaultBranch() string { return "" } +func (x *CIGenerateInput) GetFromPlan() string { + if x != nil { + return x.FromPlan + } + return "" +} + +func (x *CIGenerateInput) GetPhaseConfig() string { + if x != nil { + return x.PhaseConfig + } + return "" +} + type CIGenerateOutput struct { state protoimpl.MessageState `protogen:"open.v1"` Platform string `protobuf:"bytes,1,opt,name=platform,proto3" json:"platform,omitempty"` @@ -269,7 +309,7 @@ var File_internal_contracts_ci_generator_proto protoreflect.FileDescriptor const file_internal_contracts_ci_generator_proto_rawDesc = "" + "\n" + - "%internal/contracts/ci_generator.proto\x12 workflow.plugins.ci_generator.v1\"\xd2\x01\n" + + "%internal/contracts/ci_generator.proto\x12 workflow.plugins.ci_generator.v1\"\x92\x02\n" + "\x10CIGenerateConfig\x12\x1a\n" + "\bplatform\x18\x01 \x01(\tR\bplatform\x12\x1d\n" + "\n" + @@ -277,7 +317,9 @@ const file_internal_contracts_ci_generator_proto_rawDesc = "" + "\finfra_config\x18\x03 \x01(\tR\vinfraConfig\x12!\n" + "\fproject_name\x18\x04 \x01(\tR\vprojectName\x12\x16\n" + "\x06runner\x18\x05 \x01(\tR\x06runner\x12%\n" + - "\x0edefault_branch\x18\x06 \x01(\tR\rdefaultBranch\"\xd1\x01\n" + + "\x0edefault_branch\x18\x06 \x01(\tR\rdefaultBranch\x12\x1b\n" + + "\tfrom_plan\x18\a \x01(\tR\bfromPlan\x12!\n" + + "\fphase_config\x18\b \x01(\tR\vphaseConfig\"\x91\x02\n" + "\x0fCIGenerateInput\x12\x1a\n" + "\bplatform\x18\x01 \x01(\tR\bplatform\x12\x1d\n" + "\n" + @@ -285,7 +327,9 @@ const file_internal_contracts_ci_generator_proto_rawDesc = "" + "\finfra_config\x18\x03 \x01(\tR\vinfraConfig\x12!\n" + "\fproject_name\x18\x04 \x01(\tR\vprojectName\x12\x16\n" + "\x06runner\x18\x05 \x01(\tR\x06runner\x12%\n" + - "\x0edefault_branch\x18\x06 \x01(\tR\rdefaultBranch\"\xa7\x01\n" + + "\x0edefault_branch\x18\x06 \x01(\tR\rdefaultBranch\x12\x1b\n" + + "\tfrom_plan\x18\a \x01(\tR\bfromPlan\x12!\n" + + "\fphase_config\x18\b \x01(\tR\vphaseConfig\"\xa7\x01\n" + "\x10CIGenerateOutput\x12\x1a\n" + "\bplatform\x18\x01 \x01(\tR\bplatform\x12\x1d\n" + "\n" + diff --git a/internal/contracts/ci_generator.proto b/internal/contracts/ci_generator.proto index 2cd2431..414605b 100644 --- a/internal/contracts/ci_generator.proto +++ b/internal/contracts/ci_generator.proto @@ -11,6 +11,12 @@ message CIGenerateConfig { string project_name = 4; string runner = 5; string default_branch = 6; + // from_plan is an optional path to a CIPlan JSON file. When set, analysis is + // skipped and the plan is rendered directly from the file. + string from_plan = 7; + // phase_config is an optional path to a prerequisite workflow config that + // becomes the first DeployPhase (prereq) before the primary phase. + string phase_config = 8; } message CIGenerateInput { @@ -20,6 +26,12 @@ message CIGenerateInput { string project_name = 4; string runner = 5; string default_branch = 6; + // from_plan is an optional path to a CIPlan JSON file. When set, analysis is + // skipped and the plan is rendered directly from the file. + string from_plan = 7; + // phase_config is an optional path to a prerequisite workflow config that + // becomes the first DeployPhase (prereq) before the primary phase. + string phase_config = 8; } message CIGenerateOutput { From 772ff6af83393e073daa37c052f44a5499feb96b Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sat, 30 May 2026 18:25:57 -0400 Subject: [PATCH 3/5] feat: smart github_actions/gitlab_ci generation via cigen (jenkins/circleci keep templates) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Route github_actions and gitlab_ci through cigen.Analyze → Render* instead of the old template generators. from_plan (field 7) allows rendering from a pre-computed CIPlan JSON. phase_config (field 8) injects a prereq deploy phase. Jenkins and CircleCI continue to use the existing template generators. TDD: add 4 targeted tests — CigenMarkers for github_actions (secrets env, wfctl migrations up, no old build.yml/deploy.yml split) and gitlab_ci (WFCTL_VERSION, migrations up, no image: golang line); TemplateUnchanged for jenkins + circleci. testdata/app.yaml: representative config with env_vars_secret + ci.migrations. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/generator.go | 97 +++++++++++++--- internal/generator_test.go | 221 +++++++++++++++++++++++++++++++++++-- internal/testdata/app.yaml | 28 +++++ 3 files changed, 322 insertions(+), 24 deletions(-) create mode 100644 internal/testdata/app.yaml diff --git a/internal/generator.go b/internal/generator.go index 512d525..e75281c 100644 --- a/internal/generator.go +++ b/internal/generator.go @@ -2,6 +2,7 @@ package internal import ( "context" + "encoding/json" "fmt" "os" "path/filepath" @@ -10,6 +11,7 @@ import ( "github.com/GoCodeAlone/workflow-plugin-ci-generator/internal/contracts" "github.com/GoCodeAlone/workflow-plugin-ci-generator/internal/platforms" + "github.com/GoCodeAlone/workflow/cigen" sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" ) @@ -27,15 +29,26 @@ type Generator interface { Generate(opts platforms.Options) (map[string]string, error) } -// registry maps platform names to generator constructors. +// registry maps platform names to template generator constructors. +// Only jenkins and circleci are handled here; github_actions and gitlab_ci +// are routed through the cigen smart analyzer in ExecuteCIGenerate. var registry = map[string]func() Generator{ - PlatformGitHubActions: func() Generator { return platforms.NewGitHubActionsGenerator() }, - PlatformGitLabCI: func() Generator { return platforms.NewGitLabCIGenerator() }, - PlatformJenkins: func() Generator { return platforms.NewJenkinsGenerator() }, - PlatformCircleCI: func() Generator { return platforms.NewCircleCIGenerator() }, + PlatformJenkins: func() Generator { return platforms.NewJenkinsGenerator() }, + PlatformCircleCI: func() Generator { return platforms.NewCircleCIGenerator() }, +} + +// knownPlatforms is the complete set of supported platform names. +var knownPlatforms = map[string]bool{ + PlatformGitHubActions: true, + PlatformGitLabCI: true, + PlatformJenkins: true, + PlatformCircleCI: true, } // ExecuteCIGenerate generates CI/CD config files for the specified platform. +// For github_actions and gitlab_ci, the cigen smart analyzer is used +// (analyze → CIPlan → render). For jenkins and circleci, the existing +// template generators are used unchanged. func ExecuteCIGenerate(ctx context.Context, req sdk.TypedStepRequest[*contracts.CIGenerateConfig, *contracts.CIGenerateInput]) (*sdk.TypedStepResult[*contracts.CIGenerateOutput], error) { _ = ctx platform := resolveTypedString(req.Input.GetPlatform(), req.Config.GetPlatform()) @@ -48,22 +61,72 @@ func ExecuteCIGenerate(ctx context.Context, req sdk.TypedStepRequest[*contracts. return typedCIGenerateError("output_dir is required"), nil } - newGen, ok := registry[platform] - if !ok { + if !knownPlatforms[platform] { return typedCIGenerateError(fmt.Sprintf("unknown platform %q", platform)), nil } - opts := platforms.Options{ - InfraConfig: resolveTypedStringDefault(req.Input.GetInfraConfig(), req.Config.GetInfraConfig(), "infra.yaml"), - ProjectName: resolveTypedStringDefault(req.Input.GetProjectName(), req.Config.GetProjectName(), "my-project"), - Runner: resolveTypedStringDefault(req.Input.GetRunner(), req.Config.GetRunner(), "self-hosted, Linux, X64"), - DefaultBranch: resolveTypedStringDefault(req.Input.GetDefaultBranch(), req.Config.GetDefaultBranch(), "main"), - } + infraConfig := resolveTypedStringDefault(req.Input.GetInfraConfig(), req.Config.GetInfraConfig(), "infra.yaml") + projectName := resolveTypedStringDefault(req.Input.GetProjectName(), req.Config.GetProjectName(), "my-project") + runner := resolveTypedStringDefault(req.Input.GetRunner(), req.Config.GetRunner(), "self-hosted, Linux, X64") + defaultBranch := resolveTypedStringDefault(req.Input.GetDefaultBranch(), req.Config.GetDefaultBranch(), "main") + fromPlan := resolveTypedString(req.Input.GetFromPlan(), req.Config.GetFromPlan()) + phaseConfig := resolveTypedString(req.Input.GetPhaseConfig(), req.Config.GetPhaseConfig()) + + var files map[string]string + + switch platform { + case PlatformGitHubActions, PlatformGitLabCI: + var plan *cigen.CIPlan + if fromPlan != "" { + // Load a pre-computed CIPlan JSON directly. + raw, err := os.ReadFile(fromPlan) + if err != nil { + return typedCIGenerateError(fmt.Sprintf("read from_plan %s: %v", fromPlan, err)), nil + } + plan = &cigen.CIPlan{} + if err := json.Unmarshal(raw, plan); err != nil { + return typedCIGenerateError(fmt.Sprintf("parse from_plan %s: %v", fromPlan, err)), nil + } + } else { + // Run the smart analyzer. + opts := cigen.Options{ + Runner: runner, + DefaultBranch: defaultBranch, + Project: projectName, + PhaseConfig: phaseConfig, + } + var err error + plan, err = cigen.Analyze([]string{infraConfig}, opts) + if err != nil { + return typedCIGenerateError(fmt.Sprintf("cigen analyze: %v", err)), nil + } + } + + var err error + switch platform { + case PlatformGitHubActions: + files, err = cigen.RenderGitHubActions(plan) + case PlatformGitLabCI: + files, err = cigen.RenderGitLabCI(plan) + } + if err != nil { + return typedCIGenerateError(fmt.Sprintf("cigen render: %v", err)), nil + } - gen := newGen() - files, err := gen.Generate(opts) - if err != nil { - return typedCIGenerateError(err.Error()), nil + default: + // jenkins and circleci: template generators. + opts := platforms.Options{ + InfraConfig: infraConfig, + ProjectName: projectName, + Runner: runner, + DefaultBranch: defaultBranch, + } + gen := registry[platform]() + var err error + files, err = gen.Generate(opts) + if err != nil { + return typedCIGenerateError(err.Error()), nil + } } written := make([]string, 0, len(files)) diff --git a/internal/generator_test.go b/internal/generator_test.go index 12a09c4..451460c 100644 --- a/internal/generator_test.go +++ b/internal/generator_test.go @@ -4,6 +4,7 @@ import ( "context" "os" "path/filepath" + "strings" "testing" "github.com/GoCodeAlone/workflow-plugin-ci-generator/internal/contracts" @@ -11,6 +12,10 @@ import ( sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" ) +// testdataConfig is the path to the representative workflow config used in +// cigen integration tests. +const testdataConfig = "testdata/app.yaml" + func TestExecuteCIGenerateTyped(t *testing.T) { outputDir := t.TempDir() @@ -20,8 +25,9 @@ func TestExecuteCIGenerateTyped(t *testing.T) { DefaultBranch: "main", }, Input: &contracts.CIGenerateInput{ - Platform: PlatformGitHubActions, - OutputDir: outputDir, + Platform: PlatformGitHubActions, + OutputDir: outputDir, + InfraConfig: testdataConfig, }, }) if err != nil { @@ -49,6 +55,198 @@ func TestExecuteCIGenerateTyped(t *testing.T) { } } +// TestExecuteCIGenerateGitHubActions_CigenMarkers verifies that github_actions +// output is generated by the cigen smart analyzer (not the old template). +// The representative config (testdata/app.yaml) has env_vars_secret +// (APP_DB_URL, APP_SECRET) and ci.migrations, so the rendered workflow must +// contain: +// - a ${{ secrets.APP_DB_URL }} env binding (cigen secrets block) +// - a "wfctl migrations up" step (cigen migrations step) +// - no old "build.yml" / "deploy.yml" split (cigen renders a single file) +func TestExecuteCIGenerateGitHubActions_CigenMarkers(t *testing.T) { + outputDir := t.TempDir() + + result, err := ExecuteCIGenerate(context.Background(), sdk.TypedStepRequest[*contracts.CIGenerateConfig, *contracts.CIGenerateInput]{ + Config: &contracts.CIGenerateConfig{}, + Input: &contracts.CIGenerateInput{ + Platform: PlatformGitHubActions, + OutputDir: outputDir, + InfraConfig: testdataConfig, + ProjectName: "my-app", + DefaultBranch: "main", + }, + }) + if err != nil { + t.Fatalf("ExecuteCIGenerate: %v", err) + } + if result.Output.Error != "" { + t.Fatalf("unexpected error: %s", result.Output.Error) + } + if result.Output.FileCount == 0 { + t.Fatal("expected at least one file written") + } + + // Read all generated file contents. + combined := "" + for _, written := range result.Output.FilesWritten { + raw, err := os.ReadFile(written) + if err != nil { + t.Fatalf("read %s: %v", written, err) + } + combined += string(raw) + "\n" + } + t.Logf("generated github_actions output:\n%s", combined) + + // cigen renders secrets as job-level env: blocks using ${{ secrets.NAME }}. + // APP_DB_URL comes from env_vars_secret in the testdata config. + cigenSecretMarkers := []string{ + "${{ secrets.APP_DB_URL }}", + "${{ secrets.APP_SECRET }}", + } + for _, marker := range cigenSecretMarkers { + if !strings.Contains(combined, marker) { + t.Errorf("github_actions: cigen marker %q not found in output", marker) + } + } + + // cigen renders a "wfctl migrations up" step when ci.migrations is present. + if !strings.Contains(combined, "wfctl migrations up") { + t.Errorf("github_actions: expected cigen migrations step 'wfctl migrations up' not found") + } + + // The old template produced build.yml + deploy.yml. cigen renders a single + // workflow file. The old-template-only string "name: Build" (from build.yml) + // must NOT appear in cigen output. + if strings.Contains(combined, "name: Build\n") { + t.Errorf("github_actions: found old-template marker 'name: Build' — cigen should not render that") + } +} + +// TestExecuteCIGenerateGitLabCI_CigenMarkers verifies that gitlab_ci output is +// generated by the cigen smart analyzer (not the old template). +func TestExecuteCIGenerateGitLabCI_CigenMarkers(t *testing.T) { + outputDir := t.TempDir() + + result, err := ExecuteCIGenerate(context.Background(), sdk.TypedStepRequest[*contracts.CIGenerateConfig, *contracts.CIGenerateInput]{ + Config: &contracts.CIGenerateConfig{}, + Input: &contracts.CIGenerateInput{ + Platform: PlatformGitLabCI, + OutputDir: outputDir, + InfraConfig: testdataConfig, + ProjectName: "my-app", + DefaultBranch: "main", + }, + }) + if err != nil { + t.Fatalf("ExecuteCIGenerate: %v", err) + } + if result.Output.Error != "" { + t.Fatalf("unexpected error: %s", result.Output.Error) + } + + // cigen renders a single .gitlab-ci.yml + combined := "" + for _, written := range result.Output.FilesWritten { + raw, err := os.ReadFile(written) + if err != nil { + t.Fatalf("read %s: %v", written, err) + } + combined += string(raw) + } + t.Logf("generated gitlab_ci output:\n%s", combined) + + // cigen emits a WFCTL_VERSION variable and uses `wfctl infra apply --config`. + if !strings.Contains(combined, "WFCTL_VERSION") { + t.Errorf("gitlab_ci: cigen marker 'WFCTL_VERSION' not found in output") + } + // cigen renders migrations as `wfctl migrations up`. + if !strings.Contains(combined, "wfctl migrations up") { + t.Errorf("gitlab_ci: expected cigen migrations step 'wfctl migrations up' not found") + } + // Old template emitted "image: golang:1.26" at the top level — cigen does not. + if strings.Contains(combined, "image: golang:1.26") { + t.Errorf("gitlab_ci: found old-template marker 'image: golang:1.26' — cigen should not render that") + } +} + +// TestExecuteCIGenerateJenkins_TemplateUnchanged verifies that jenkins output +// still comes from the template generator (unchanged). +func TestExecuteCIGenerateJenkins_TemplateUnchanged(t *testing.T) { + outputDir := t.TempDir() + + result, err := ExecuteCIGenerate(context.Background(), sdk.TypedStepRequest[*contracts.CIGenerateConfig, *contracts.CIGenerateInput]{ + Config: &contracts.CIGenerateConfig{}, + Input: &contracts.CIGenerateInput{ + Platform: PlatformJenkins, + OutputDir: outputDir, + InfraConfig: "infra.yaml", + DefaultBranch: "main", + }, + }) + if err != nil { + t.Fatalf("ExecuteCIGenerate: %v", err) + } + if result.Output.Error != "" { + t.Fatalf("unexpected error: %s", result.Output.Error) + } + + combined := "" + for _, written := range result.Output.FilesWritten { + raw, err := os.ReadFile(written) + if err != nil { + t.Fatalf("read %s: %v", written, err) + } + combined += string(raw) + } + + // Jenkins template produces a declarative Jenkinsfile. + if !strings.Contains(combined, "pipeline {") { + t.Errorf("jenkins: expected 'pipeline {' from template generator") + } + if !strings.Contains(combined, "stage('Checkout')") { + t.Errorf("jenkins: expected 'stage('Checkout')' from template generator") + } +} + +// TestExecuteCIGenerateCircleCI_TemplateUnchanged verifies that circleci output +// still comes from the template generator (unchanged). +func TestExecuteCIGenerateCircleCI_TemplateUnchanged(t *testing.T) { + outputDir := t.TempDir() + + result, err := ExecuteCIGenerate(context.Background(), sdk.TypedStepRequest[*contracts.CIGenerateConfig, *contracts.CIGenerateInput]{ + Config: &contracts.CIGenerateConfig{}, + Input: &contracts.CIGenerateInput{ + Platform: PlatformCircleCI, + OutputDir: outputDir, + InfraConfig: "infra.yaml", + DefaultBranch: "main", + }, + }) + if err != nil { + t.Fatalf("ExecuteCIGenerate: %v", err) + } + if result.Output.Error != "" { + t.Fatalf("unexpected error: %s", result.Output.Error) + } + + combined := "" + for _, written := range result.Output.FilesWritten { + raw, err := os.ReadFile(written) + if err != nil { + t.Fatalf("read %s: %v", written, err) + } + combined += string(raw) + } + + // CircleCI template produces version: 2.1 with orbs. + if !strings.Contains(combined, "version: 2.1") { + t.Errorf("circleci: expected 'version: 2.1' from template generator") + } + if !strings.Contains(combined, "orbs:") { + t.Errorf("circleci: expected 'orbs:' from template generator") + } +} + func TestExecuteCIGenerateTypedValidation(t *testing.T) { result, err := ExecuteCIGenerate(context.Background(), sdk.TypedStepRequest[*contracts.CIGenerateConfig, *contracts.CIGenerateInput]{ Config: &contracts.CIGenerateConfig{}, @@ -125,15 +323,24 @@ func (g staticGenerator) Generate(_ platforms.Options) (map[string]string, error return g.files, nil } +// registerTestGenerator registers a test generator in registry + knownPlatforms, +// and removes it on cleanup. func registerTestGenerator(t *testing.T, platform string, generator Generator) func() { t.Helper() - original, existed := registry[platform] + origGen, genExisted := registry[platform] + origKnown := knownPlatforms[platform] registry[platform] = func() Generator { return generator } + knownPlatforms[platform] = true return func() { - if existed { - registry[platform] = original - return + if genExisted { + registry[platform] = origGen + } else { + delete(registry, platform) + } + if origKnown { + knownPlatforms[platform] = true + } else { + delete(knownPlatforms, platform) } - delete(registry, platform) } } diff --git a/internal/testdata/app.yaml b/internal/testdata/app.yaml new file mode 100644 index 0000000..f17e677 --- /dev/null +++ b/internal/testdata/app.yaml @@ -0,0 +1,28 @@ +modules: + - name: do-provider + type: iac.provider + config: + provider: digitalocean + token: ${DIGITALOCEAN_TOKEN} + + - name: my-app + type: infra.container_service + config: + provider: do-provider + health_check: + http_path: /healthz + domains: + - domain: myapp.example.com + type: PRIMARY + zone: example.com + env_vars_secret: + APP_DB_URL: ${APP_DB_URL} + APP_SECRET: ${APP_SECRET} + +ci: + migrations: + - name: app + driver: golang-migrate + source_dir: migrations + database: + env: APP_DB_URL From df351f4bca589acf5980edaeca995f5c89d8cef6 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sat, 30 May 2026 18:26:03 -0400 Subject: [PATCH 4/5] chore: bump minEngineVersion to 0.67.0 (cigen) Co-Authored-By: Claude Opus 4.8 (1M context) --- plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin.json b/plugin.json index 27e1282..f60c46c 100644 --- a/plugin.json +++ b/plugin.json @@ -7,7 +7,7 @@ "type": "external", "tier": "core", "private": false, - "minEngineVersion": "0.53.0", + "minEngineVersion": "0.67.0", "keywords": ["ci", "cd", "github-actions", "gitlab-ci", "jenkins", "circleci", "devops", "infrastructure"], "homepage": "https://github.com/GoCodeAlone/workflow-plugin-ci-generator", "repository": "https://github.com/GoCodeAlone/workflow-plugin-ci-generator", From 3a5e4f76c3f2cb8daf72f17d9d4662b3e6ac9f0c Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sat, 30 May 2026 18:32:56 -0400 Subject: [PATCH 5/5] fix: set ConfigPathAlias so absolute infra_config renders repo-relative CI paths When infra_config is an absolute path (or outside the repo checkout), cigen.Analyze's filepath.Rel(cwd, abs) can yield an absolute or "../"-escaping path that lands in the generated `wfctl infra apply --config ` steps and the `paths:` trigger filter, producing CI that never matches a checkout. Pass a cleaned repo-relative alias via cigen.Options.ConfigPathAlias (and PhaseConfigAlias when phase_config is set): cleanConfigAlias collapses absolute or ".."-containing paths to the base filename, else filepath.Clean. Tests: TestExecuteCIGenerateGitHubActions_AbsoluteConfigRendersRepoRelative (absolute tmp config renders `--config 'deploy.yaml'` + relative paths entry, no absolute/".." leak) and TestCleanConfigAlias unit cases. Also annotate the retained NewGitHubActionsGenerator/NewGitLabCIGenerator as template-reference fallbacks (those platforms now route through cigen) so the unreferenced constructors don't read as dead code. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/generator.go | 39 ++++++++++-- internal/generator_test.go | 93 ++++++++++++++++++++++++++++ internal/platforms/github_actions.go | 3 + internal/platforms/gitlab_ci.go | 3 + 4 files changed, 133 insertions(+), 5 deletions(-) diff --git a/internal/generator.go b/internal/generator.go index e75281c..914d829 100644 --- a/internal/generator.go +++ b/internal/generator.go @@ -88,12 +88,21 @@ func ExecuteCIGenerate(ctx context.Context, req sdk.TypedStepRequest[*contracts. return typedCIGenerateError(fmt.Sprintf("parse from_plan %s: %v", fromPlan, err)), nil } } else { - // Run the smart analyzer. + // Run the smart analyzer. Pass repo-relative aliases so that an + // absolute or out-of-tree infra_config does not leak an absolute or + // "../"-escaping path into the rendered `wfctl infra apply --config` + // steps and the `paths:` trigger filter (which would never match a + // CI checkout). cigen.Analyze uses ConfigPathAlias/PhaseConfigAlias + // verbatim as the DeployPhase config paths when set. opts := cigen.Options{ - Runner: runner, - DefaultBranch: defaultBranch, - Project: projectName, - PhaseConfig: phaseConfig, + Runner: runner, + DefaultBranch: defaultBranch, + Project: projectName, + PhaseConfig: phaseConfig, + ConfigPathAlias: cleanConfigAlias(infraConfig), + } + if phaseConfig != "" { + opts.PhaseConfigAlias = cleanConfigAlias(phaseConfig) } var err error plan, err = cigen.Analyze([]string{infraConfig}, opts) @@ -161,6 +170,26 @@ func ExecuteCIGenerate(ctx context.Context, req sdk.TypedStepRequest[*contracts. }, nil } +// cleanConfigAlias returns a repo-relative config path safe to embed in +// generated CI steps and `paths:` trigger filters. An absolute path or one +// containing a ".." segment (which would escape a CI checkout) is collapsed to +// its base filename; otherwise the path is cleaned (normalizing "./", double +// slashes, etc.) and returned as-is. +func cleanConfigAlias(p string) string { + if p == "" { + return p + } + if filepath.IsAbs(p) { + return filepath.Base(p) + } + for _, segment := range strings.Split(filepath.ToSlash(p), "/") { + if segment == ".." { + return filepath.Base(p) + } + } + return filepath.Clean(p) +} + func validateRelativeOutputPath(relPath string) error { if relPath == "" { return fmt.Errorf("generated output path is empty") diff --git a/internal/generator_test.go b/internal/generator_test.go index 451460c..7d5787c 100644 --- a/internal/generator_test.go +++ b/internal/generator_test.go @@ -169,6 +169,99 @@ func TestExecuteCIGenerateGitLabCI_CigenMarkers(t *testing.T) { } } +// TestExecuteCIGenerateGitHubActions_AbsoluteConfigRendersRepoRelative verifies +// that when infra_config is an absolute path (outside the repo checkout), the +// rendered GitHub Actions workflow references a clean repo-relative config path +// (base filename) — never an absolute path or one with a leading "/" or "..". +// This guards against cigen.Analyze's filepath.Rel(cwd, abs) leaking an +// out-of-tree path into the `--config` steps and `paths:` trigger filter. +func TestExecuteCIGenerateGitHubActions_AbsoluteConfigRendersRepoRelative(t *testing.T) { + // Copy the representative config to an absolute tmp path outside the repo. + src, err := os.ReadFile(testdataConfig) + if err != nil { + t.Fatalf("read testdata: %v", err) + } + tmpDir := t.TempDir() + absConfig := filepath.Join(tmpDir, "deploy.yaml") + if err := os.WriteFile(absConfig, src, 0o644); err != nil { + t.Fatalf("write tmp config: %v", err) + } + if !filepath.IsAbs(absConfig) { + t.Fatalf("expected absolute config path, got %s", absConfig) + } + + outputDir := t.TempDir() + result, err := ExecuteCIGenerate(context.Background(), sdk.TypedStepRequest[*contracts.CIGenerateConfig, *contracts.CIGenerateInput]{ + Config: &contracts.CIGenerateConfig{}, + Input: &contracts.CIGenerateInput{ + Platform: PlatformGitHubActions, + OutputDir: outputDir, + InfraConfig: absConfig, + ProjectName: "my-app", + DefaultBranch: "main", + }, + }) + if err != nil { + t.Fatalf("ExecuteCIGenerate: %v", err) + } + if result.Output.Error != "" { + t.Fatalf("unexpected error: %s", result.Output.Error) + } + + combined := "" + for _, written := range result.Output.FilesWritten { + raw, err := os.ReadFile(written) + if err != nil { + t.Fatalf("read %s: %v", written, err) + } + combined += string(raw) + } + t.Logf("generated github_actions output (absolute infra_config):\n%s", combined) + + // The config alias must collapse to the base filename. + if !strings.Contains(combined, "--config 'deploy.yaml'") { + t.Errorf("expected '--config '\\''deploy.yaml'\\''' (repo-relative base), output:\n%s", combined) + } + // The paths: trigger filter must reference the relative base name. + if !strings.Contains(combined, "- 'deploy.yaml'") { + t.Errorf("expected paths: entry \"- 'deploy.yaml'\", output:\n%s", combined) + } + // The absolute tmp path must NOT leak into the rendered workflow. + if strings.Contains(combined, absConfig) { + t.Errorf("absolute config path %q leaked into rendered output:\n%s", absConfig, combined) + } + // No "/" prefixed --config and no ".." escaping path. + if strings.Contains(combined, "--config '/") { + t.Errorf("rendered output contains an absolute --config path:\n%s", combined) + } + if strings.Contains(combined, "..") { + t.Errorf("rendered output contains a '..' escaping path:\n%s", combined) + } +} + +// TestCleanConfigAlias exercises the alias normalization directly. +func TestCleanConfigAlias(t *testing.T) { + cases := []struct { + name string + in string + want string + }{ + {"empty", "", ""}, + {"relative clean", "infra.yaml", "infra.yaml"}, + {"relative nested", "deploy/app.yaml", "deploy/app.yaml"}, + {"relative dot prefix", "./infra.yaml", "infra.yaml"}, + {"absolute collapses to base", "/tmp/build123/deploy.yaml", "deploy.yaml"}, + {"parent escape collapses to base", "../sibling/deploy.yaml", "deploy.yaml"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := cleanConfigAlias(tc.in); got != tc.want { + t.Errorf("cleanConfigAlias(%q) = %q, want %q", tc.in, got, tc.want) + } + }) + } +} + // TestExecuteCIGenerateJenkins_TemplateUnchanged verifies that jenkins output // still comes from the template generator (unchanged). func TestExecuteCIGenerateJenkins_TemplateUnchanged(t *testing.T) { diff --git a/internal/platforms/github_actions.go b/internal/platforms/github_actions.go index f85f0d3..d338323 100644 --- a/internal/platforms/github_actions.go +++ b/internal/platforms/github_actions.go @@ -10,6 +10,9 @@ import ( type GitHubActionsGenerator struct{} // NewGitHubActionsGenerator returns a new GitHubActionsGenerator. +// Retained as the template-reference fallback: github_actions now routes +// through the cigen smart analyzer in generator.go, but this template +// generator is kept for reference and potential reuse. func NewGitHubActionsGenerator() *GitHubActionsGenerator { return &GitHubActionsGenerator{} } diff --git a/internal/platforms/gitlab_ci.go b/internal/platforms/gitlab_ci.go index ea9c30a..8b5ea0a 100644 --- a/internal/platforms/gitlab_ci.go +++ b/internal/platforms/gitlab_ci.go @@ -10,6 +10,9 @@ import ( type GitLabCIGenerator struct{} // NewGitLabCIGenerator returns a new GitLabCIGenerator. +// Retained as the template-reference fallback: gitlab_ci now routes through +// the cigen smart analyzer in generator.go, but this template generator is +// kept for reference and potential reuse. func NewGitLabCIGenerator() *GitLabCIGenerator { return &GitLabCIGenerator{} }