diff --git a/README.md b/README.md index 6a730b1..4e9c78b 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,12 @@ CI/CD config generator for workflow projects — emits GitHub Actions, GitLab CI, Jenkins, and CircleCI pipelines from a workflow app config. -As of **v0.2.0** the **GitHub Actions** and **GitLab CI** platforms are generated by the workflow engine's config-derived `cigen` engine (analyze → CIPlan → render): the output is *derived from your app config* — its required secrets, deploy phases, migrations, health-check smoke, and plugin-install needs — not a fixed template. **Jenkins** and **CircleCI** still use template generators (smart generation for those is future work). This is the same engine behind `wfctl ci plan` / `wfctl ci generate`. +As of **v0.2.0** **all four platforms** — GitHub Actions, GitLab CI, Jenkins, and CircleCI — are generated by the workflow engine's config-derived `cigen` engine (analyze → CIPlan → render): the output is *derived from your app config* — its required secrets, deploy phases, migrations, health-check smoke, and plugin-install needs — not a fixed template. The legacy Jenkins/CircleCI template generators were retired in #804 (see ADR 0044 in the workflow repo). This is the same engine behind `wfctl ci plan` / `wfctl ci generate`. Requires workflow engine **>= v0.68.0**. ## What it provides **Pipeline step types:** -- `step.ci_generate` — Generate CI/CD configuration files from a workflow app config. GitHub Actions / GitLab CI are config-derived via `cigen`; Jenkins / CircleCI are template-based. +- `step.ci_generate` — Generate CI/CD configuration files from a workflow app config. All four platforms (GitHub Actions, GitLab CI, Jenkins, CircleCI) are config-derived via `cigen`. ## Requirements @@ -62,8 +62,8 @@ See [`examples/minimal/config.yaml`](examples/minimal/config.yaml). |----------|-------------|------------| | GitHub Actions | `.github/workflows/.yml` | config-derived (`cigen`) | | GitLab CI | `.gitlab-ci.yml` | config-derived (`cigen`) | -| Jenkins | `Jenkinsfile` | template | -| CircleCI | `.circleci/config.yml` | template | +| Jenkins | `Jenkinsfile` | config-derived (`cigen`) | +| CircleCI | `.circleci/config.yml` | config-derived (`cigen`) | ## Documentation diff --git a/go.mod b/go.mod index 7cf4765..0d00e1a 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.67.0 + github.com/GoCodeAlone/workflow v0.68.0 google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af ) diff --git a/go.sum b/go.sum index a1fa518..07854f1 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/GoCodeAlone/modular/modules/jsonschema v1.17.0 h1:zoWioqUvuNNDfnjHA1s 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/workflow v0.68.0 h1:7dh/7tPtjsJwgS4IG/NZFeCyNtZnp7uoDBOI6S8dADM= +github.com/GoCodeAlone/workflow v0.68.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= diff --git a/integration_test.go b/integration_test.go index 529f800..71b3c61 100644 --- a/integration_test.go +++ b/integration_test.go @@ -2,11 +2,64 @@ package cigenerator_test import ( "context" + "os" + "strings" "testing" + "github.com/GoCodeAlone/workflow-plugin-ci-generator/internal" + "github.com/GoCodeAlone/workflow-plugin-ci-generator/internal/contracts" "github.com/GoCodeAlone/workflow/wftest" + + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" ) +// TestIntegration_ExecuteCIGenerate_JenkinsCircleCI is the acceptance-#2 +// behavior proof for #804: it drives the REAL plugin entry point +// (internal.ExecuteCIGenerate) through the external package boundary for the +// jenkins and circleci platforms and asserts the written artifacts are +// config-derived (secret wiring, `wfctl migrations up`, `wfctl infra apply`) and +// free of the retired legacy stages (ADR 0044) — NOT a mocked step. +func TestIntegration_ExecuteCIGenerate_JenkinsCircleCI(t *testing.T) { + cases := map[string][]string{ + internal.PlatformJenkins: {"pipeline {", "wfctl migrations up", "wfctl infra apply"}, + internal.PlatformCircleCI: {"version: 2.1", "wfctl migrations up", "wfctl infra apply"}, + } + for platform, markers := range cases { + result, err := internal.ExecuteCIGenerate(context.Background(), sdk.TypedStepRequest[*contracts.CIGenerateConfig, *contracts.CIGenerateInput]{ + Config: &contracts.CIGenerateConfig{}, + Input: &contracts.CIGenerateInput{ + Platform: platform, + OutputDir: t.TempDir(), + InfraConfig: "internal/testdata/app.yaml", + }, + }) + if err != nil { + t.Fatalf("%s: ExecuteCIGenerate: %v", platform, err) + } + if result.Output.Error != "" { + t.Fatalf("%s: %s", platform, result.Output.Error) + } + combined := "" + for _, w := range result.Output.FilesWritten { + raw, rerr := os.ReadFile(w) + if rerr != nil { + t.Fatalf("read %s: %v", w, rerr) + } + combined += string(raw) + "\n" + } + for _, m := range markers { + if !strings.Contains(combined, m) { + t.Errorf("%s: missing config-derived marker %q", platform, m) + } + } + for _, banned := range []string{"go test ./...", "wfctl deploy --image", "docker build"} { + if strings.Contains(combined, banned) { + t.Errorf("%s: found retired legacy marker %q", platform, banned) + } + } + } +} + // TestIntegration_GenerateGitHubActions verifies that a pipeline using // step.ci_generate executes and returns GitHub Actions output. func TestIntegration_GenerateGitHubActions(t *testing.T) { diff --git a/internal/generator.go b/internal/generator.go index 914d829..803c89f 100644 --- a/internal/generator.go +++ b/internal/generator.go @@ -10,12 +10,13 @@ import ( "strings" "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" ) -// Platform constants. +// Platform constants. All four platforms are config-derived through the cigen +// analyze → CIPlan → render pipeline (the legacy text/template generators were +// retired in #804; see ADR 0044 in the workflow repo). const ( PlatformGitHubActions = "github_actions" PlatformGitLabCI = "gitlab_ci" @@ -23,20 +24,6 @@ const ( PlatformCircleCI = "circleci" ) -// Generator defines the interface all platform generators implement. -type Generator interface { - // Generate produces CI config files. Returns a map of relative output path → content. - Generate(opts platforms.Options) (map[string]string, error) -} - -// 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{ - 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, @@ -46,9 +33,9 @@ var knownPlatforms = map[string]bool{ } // 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. +// All four platforms (github_actions, gitlab_ci, jenkins, circleci) are +// config-derived through the cigen analyze → CIPlan → render pipeline; the +// legacy text/template generators were retired in #804 (see ADR 0044). 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()) @@ -75,7 +62,7 @@ func ExecuteCIGenerate(ctx context.Context, req sdk.TypedStepRequest[*contracts. var files map[string]string switch platform { - case PlatformGitHubActions, PlatformGitLabCI: + case PlatformGitHubActions, PlatformGitLabCI, PlatformJenkins, PlatformCircleCI: var plan *cigen.CIPlan if fromPlan != "" { // Load a pre-computed CIPlan JSON directly. @@ -117,25 +104,14 @@ func ExecuteCIGenerate(ctx context.Context, req sdk.TypedStepRequest[*contracts. files, err = cigen.RenderGitHubActions(plan) case PlatformGitLabCI: files, err = cigen.RenderGitLabCI(plan) + case PlatformJenkins: + files, err = cigen.RenderJenkins(plan) + case PlatformCircleCI: + files, err = cigen.RenderCircleCI(plan) } if err != nil { return typedCIGenerateError(fmt.Sprintf("cigen render: %v", err)), 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 7d5787c..100016f 100644 --- a/internal/generator_test.go +++ b/internal/generator_test.go @@ -4,11 +4,11 @@ import ( "context" "os" "path/filepath" + "sort" "strings" "testing" "github.com/GoCodeAlone/workflow-plugin-ci-generator/internal/contracts" - "github.com/GoCodeAlone/workflow-plugin-ci-generator/internal/platforms" sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" ) @@ -262,81 +262,95 @@ func TestCleanConfigAlias(t *testing.T) { } } -// TestExecuteCIGenerateJenkins_TemplateUnchanged verifies that jenkins output -// still comes from the template generator (unchanged). -func TestExecuteCIGenerateJenkins_TemplateUnchanged(t *testing.T) { - outputDir := t.TempDir() - +// executeAndReadCombined runs ExecuteCIGenerate for a platform against the +// representative testdata config and returns the concatenated written-file bytes. +// This drives the REAL plugin entry point (acceptance #2 for #804). +func executeAndReadCombined(t *testing.T, platform string) string { + t.Helper() result, err := ExecuteCIGenerate(context.Background(), sdk.TypedStepRequest[*contracts.CIGenerateConfig, *contracts.CIGenerateInput]{ Config: &contracts.CIGenerateConfig{}, Input: &contracts.CIGenerateInput{ - Platform: PlatformJenkins, - OutputDir: outputDir, - InfraConfig: "infra.yaml", + Platform: platform, + OutputDir: t.TempDir(), + InfraConfig: testdataConfig, + ProjectName: "my-app", DefaultBranch: "main", }, }) if err != nil { - t.Fatalf("ExecuteCIGenerate: %v", err) + t.Fatalf("ExecuteCIGenerate(%s): %v", platform, err) } if result.Output.Error != "" { - t.Fatalf("unexpected error: %s", result.Output.Error) + t.Fatalf("%s: unexpected error: %s", platform, result.Output.Error) + } + if result.Output.FileCount == 0 { + t.Fatalf("%s: expected at least one file written", platform) } - 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) + combined += string(raw) + "\n" } + return combined +} - // 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") +// assertNoRetiredStages asserts the retired, non-config-derived stages (ADR 0044) +// do not appear in cigen-generated output. +func assertNoRetiredStages(t *testing.T, platform, combined string) { + t.Helper() + for _, banned := range []string{"go test ./...", "wfctl deploy --image", "docker build", "docker push", "wfctl ci run --phase migrate"} { + if strings.Contains(combined, banned) { + t.Errorf("%s: found retired legacy marker %q — cigen should not render that", platform, banned) + } } } -// 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) +// TestExecuteCIGenerateJenkins_CigenMarkers verifies jenkins output is generated +// by the cigen smart analyzer (not the retired template generator). +func TestExecuteCIGenerateJenkins_CigenMarkers(t *testing.T) { + combined := executeAndReadCombined(t, PlatformJenkins) + for _, marker := range []string{ + "pipeline {", + "Requires a Jenkins Multibranch Pipeline", + "credentials('APP_DB_URL')", + "wfctl migrations up", + "wfctl infra apply", + "curl --fail --max-time 30 'https://myapp.example.com/healthz'", // smoke + } { + if !strings.Contains(combined, marker) { + t.Errorf("jenkins: cigen marker %q not found in output", marker) + } } - if result.Output.Error != "" { - t.Fatalf("unexpected error: %s", result.Output.Error) + assertNoRetiredStages(t, "jenkins", combined) + // The old template emitted stage('Checkout'); cigen does not. + if strings.Contains(combined, "stage('Checkout')") { + t.Errorf("jenkins: found old-template marker stage('Checkout')") } +} - combined := "" - for _, written := range result.Output.FilesWritten { - raw, err := os.ReadFile(written) - if err != nil { - t.Fatalf("read %s: %v", written, err) +// TestExecuteCIGenerateCircleCI_CigenMarkers verifies circleci output is +// generated by the cigen smart analyzer (not the retired template generator). +func TestExecuteCIGenerateCircleCI_CigenMarkers(t *testing.T) { + combined := executeAndReadCombined(t, PlatformCircleCI) + for _, marker := range []string{ + "version: 2.1", + "requires:", + "APP_DB_URL", // secret referenced (project env var header) + "wfctl migrations up", + "wfctl infra apply", + "curl --fail --max-time 30 'https://myapp.example.com/healthz'", // smoke + } { + if !strings.Contains(combined, marker) { + t.Errorf("circleci: cigen marker %q not found in output", marker) } - 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") + assertNoRetiredStages(t, "circleci", combined) + // The old template emitted `orbs:`; cigen does not. + if strings.Contains(combined, "orbs:") { + t.Errorf("circleci: found old-template marker 'orbs:'") } } @@ -353,87 +367,39 @@ func TestExecuteCIGenerateTypedValidation(t *testing.T) { } } -func TestExecuteCIGenerateRejectsUnsafeGeneratedPath(t *testing.T) { - restore := registerTestGenerator(t, "unsafe", staticGenerator{ - files: map[string]string{"../escape.yml": "bad"}, - }) - defer restore() - - result, err := ExecuteCIGenerate(context.Background(), sdk.TypedStepRequest[*contracts.CIGenerateConfig, *contracts.CIGenerateInput]{ - Config: &contracts.CIGenerateConfig{}, - Input: &contracts.CIGenerateInput{ - Platform: "unsafe", - OutputDir: t.TempDir(), - }, - }) - if err != nil { - t.Fatalf("ExecuteCIGenerate: %v", err) +// TestValidateRelativeOutputPath exercises the path-safety guard directly (the +// registry-injection seam it used to be tested through was removed with the +// template generators in #804). +func TestValidateRelativeOutputPath(t *testing.T) { + for _, bad := range []string{"../escape.yml", "a/../../b.yml", "/abs/path.yml", ""} { + if err := validateRelativeOutputPath(bad); err == nil { + t.Errorf("expected error for unsafe path %q", bad) + } } - if result == nil || result.Output == nil || result.Output.Error == "" { - t.Fatalf("expected unsafe path error, got %#v", result) + for _, good := range []string{"Jenkinsfile", ".circleci/config.yml", ".github/workflows/ci.yml"} { + if err := validateRelativeOutputPath(good); err != nil { + t.Errorf("unexpected error for safe path %q: %v", good, err) + } } } +// TestExecuteCIGenerateSortsFilesWritten asserts FilesWritten is sorted on a real +// cigen render. Each renderer writes a single file today, so this asserts the +// sort invariant holds (and documents it) rather than exercising a multi-file +// permutation that no current renderer produces. func TestExecuteCIGenerateSortsFilesWritten(t *testing.T) { - outputDir := t.TempDir() - restore := registerTestGenerator(t, "static", staticGenerator{ - files: map[string]string{ - "z.yml": "z", - "a.yml": "a", - }, - }) - defer restore() - result, err := ExecuteCIGenerate(context.Background(), sdk.TypedStepRequest[*contracts.CIGenerateConfig, *contracts.CIGenerateInput]{ Config: &contracts.CIGenerateConfig{}, Input: &contracts.CIGenerateInput{ - Platform: "static", - OutputDir: outputDir, + Platform: PlatformGitHubActions, + OutputDir: t.TempDir(), + InfraConfig: testdataConfig, }, }) if err != nil { t.Fatalf("ExecuteCIGenerate: %v", err) } - want := []string{ - filepath.Join(outputDir, "a.yml"), - filepath.Join(outputDir, "z.yml"), - } - if len(result.Output.FilesWritten) != len(want) { - t.Fatalf("FilesWritten length = %d, want %d", len(result.Output.FilesWritten), len(want)) - } - for i := range want { - if result.Output.FilesWritten[i] != want[i] { - t.Fatalf("FilesWritten[%d] = %q, want %q", i, result.Output.FilesWritten[i], want[i]) - } - } -} - -type staticGenerator struct { - files map[string]string -} - -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() - origGen, genExisted := registry[platform] - origKnown := knownPlatforms[platform] - registry[platform] = func() Generator { return generator } - knownPlatforms[platform] = true - return func() { - if genExisted { - registry[platform] = origGen - } else { - delete(registry, platform) - } - if origKnown { - knownPlatforms[platform] = true - } else { - delete(knownPlatforms, platform) - } + if !sort.StringsAreSorted(result.Output.FilesWritten) { + t.Errorf("FilesWritten not sorted: %v", result.Output.FilesWritten) } } diff --git a/internal/platforms/circleci.go b/internal/platforms/circleci.go deleted file mode 100644 index b3728f5..0000000 --- a/internal/platforms/circleci.go +++ /dev/null @@ -1,182 +0,0 @@ -package platforms - -import ( - "bytes" - "fmt" - "text/template" -) - -// CircleCIGenerator generates a .circleci/config.yml file using CircleCI v2.1 syntax. -type CircleCIGenerator struct{} - -// NewCircleCIGenerator returns a new CircleCIGenerator. -func NewCircleCIGenerator() *CircleCIGenerator { - return &CircleCIGenerator{} -} - -// Generate produces: -// - .circleci/config.yml -func (g *CircleCIGenerator) Generate(opts Options) (map[string]string, error) { - branch := opts.DefaultBranch - if branch == "" { - branch = "main" - } - infra := opts.InfraConfig - if infra == "" { - infra = "infra.yaml" - } - - data := circleciData{ - DefaultBranch: branch, - InfraConfig: infra, - ProjectName: opts.ProjectName, - } - - content, err := renderCircleCITemplate(circleciConfigTemplate, data) - if err != nil { - return nil, fmt.Errorf("circleci: render config.yml: %w", err) - } - - return map[string]string{ - ".circleci/config.yml": content, - }, nil -} - -type circleciData struct { - DefaultBranch string - InfraConfig string - ProjectName string -} - -func renderCircleCITemplate(tmplStr string, data circleciData) (string, error) { - tmpl, err := template.New("").Parse(tmplStr) - if err != nil { - return "", err - } - var buf bytes.Buffer - if err := tmpl.Execute(&buf, data); err != nil { - return "", err - } - return buf.String(), nil -} - -// circleciConfigTemplate uses CircleCI v2.1 syntax with orbs, executors, and -// an approval job for the apply step. -const circleciConfigTemplate = `version: 2.1 - -orbs: - go: circleci/go@1.11 - -executors: - default: - docker: - - image: cimg/go:1.26 - resource_class: medium - -jobs: - infra-plan: - executor: default - steps: - - checkout - - run: - name: Install wfctl - command: curl -fsSL https://github.com/GoCodeAlone/workflow/releases/latest/download/wfctl-linux-amd64.tar.gz | tar -xz && sudo mv wfctl /usr/local/bin/ - - run: - name: Plan infrastructure - command: | - wfctl infra plan -c {{.InfraConfig}} --output plan.json - wfctl infra plan -c {{.InfraConfig}} --format markdown > plan.md - - store_artifacts: - path: plan.json - - store_artifacts: - path: plan.md - - persist_to_workspace: - root: . - paths: - - plan.json - - approve-apply: - executor: default - steps: - - run: echo "Waiting for manual approval before applying infrastructure changes." - - infra-apply: - executor: default - steps: - - checkout - - attach_workspace: - at: . - - run: - name: Install wfctl - command: curl -fsSL https://github.com/GoCodeAlone/workflow/releases/latest/download/wfctl-linux-amd64.tar.gz | tar -xz && sudo mv wfctl /usr/local/bin/ - - run: - name: Apply infrastructure - command: wfctl infra apply -c {{.InfraConfig}} --auto-approve - - build: - executor: default - steps: - - checkout - - go/load-cache - - go/mod-download - - go/save-cache - - run: - name: Test - command: go test ./... - - run: - name: Build - command: go build ./... - - run: - name: Build and push container image - command: | - docker build -t $REGISTRY_IMAGE:$CIRCLE_SHA1 . - docker push $REGISTRY_IMAGE:$CIRCLE_SHA1 - - deploy: - executor: default - steps: - - checkout - - run: - name: Install wfctl - command: curl -fsSL https://github.com/GoCodeAlone/workflow/releases/latest/download/wfctl-linux-amd64.tar.gz | tar -xz && sudo mv wfctl /usr/local/bin/ - - run: - name: Deploy application - command: wfctl deploy --image $REGISTRY_IMAGE:$CIRCLE_SHA1 - -workflows: - infra-and-deploy: - jobs: - - infra-plan: - filters: - branches: - only: - - {{.DefaultBranch}} - - approve-apply: - type: approval - requires: - - infra-plan - filters: - branches: - only: - - {{.DefaultBranch}} - - infra-apply: - requires: - - approve-apply - filters: - branches: - only: - - {{.DefaultBranch}} - - build: - filters: - branches: - only: - - {{.DefaultBranch}} - - deploy: - requires: - - infra-apply - - build - filters: - branches: - only: - - {{.DefaultBranch}} -` diff --git a/internal/platforms/circleci_test.go b/internal/platforms/circleci_test.go deleted file mode 100644 index 6f4f20a..0000000 --- a/internal/platforms/circleci_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package platforms_test - -import ( - "strings" - "testing" - - "github.com/GoCodeAlone/workflow-plugin-ci-generator/internal/platforms" -) - -func TestCircleCIGenerator_Generate(t *testing.T) { - g := platforms.NewCircleCIGenerator() - opts := platforms.Options{ - InfraConfig: "infra.yaml", - ProjectName: "my-project", - DefaultBranch: "main", - } - - files, err := g.Generate(opts) - if err != nil { - t.Fatalf("Generate() error: %v", err) - } - - if _, ok := files[".circleci/config.yml"]; !ok { - t.Fatal("expected .circleci/config.yml in output") - } -} - -func TestCircleCIGenerator_Version(t *testing.T) { - g := platforms.NewCircleCIGenerator() - files, err := g.Generate(platforms.Options{DefaultBranch: "main"}) - if err != nil { - t.Fatalf("Generate() error: %v", err) - } - - content := files[".circleci/config.yml"] - if !strings.HasPrefix(strings.TrimSpace(content), "version: 2.1") { - t.Errorf("config.yml must start with 'version: 2.1', got:\n%s", content[:min(80, len(content))]) - } -} - -func TestCircleCIGenerator_OrbsAndExecutors(t *testing.T) { - g := platforms.NewCircleCIGenerator() - files, err := g.Generate(platforms.Options{DefaultBranch: "main"}) - if err != nil { - t.Fatalf("Generate() error: %v", err) - } - - content := files[".circleci/config.yml"] - - if !strings.Contains(content, "orbs:") { - t.Error("config.yml must have 'orbs:' section") - } - if !strings.Contains(content, "executors:") { - t.Error("config.yml must have 'executors:' section") - } -} - -func TestCircleCIGenerator_ApprovalJob(t *testing.T) { - g := platforms.NewCircleCIGenerator() - files, err := g.Generate(platforms.Options{DefaultBranch: "main"}) - if err != nil { - t.Fatalf("Generate() error: %v", err) - } - - content := files[".circleci/config.yml"] - if !strings.Contains(content, "type: approval") { - t.Error("config.yml must include an approval job for apply") - } -} - -func TestCircleCIGenerator_WfctlCommands(t *testing.T) { - g := platforms.NewCircleCIGenerator() - files, err := g.Generate(platforms.Options{InfraConfig: "infra.yaml", DefaultBranch: "main"}) - if err != nil { - t.Fatalf("Generate() error: %v", err) - } - - content := files[".circleci/config.yml"] - checks := []string{ - "wfctl infra plan", - "wfctl infra apply", - "--auto-approve", - "wfctl deploy", - } - for _, want := range checks { - if !strings.Contains(content, want) { - t.Errorf("expected %q in .circleci/config.yml\ngot:\n%s", want, content) - } - } -} - -func TestCircleCIGenerator_WorkflowsSection(t *testing.T) { - g := platforms.NewCircleCIGenerator() - files, err := g.Generate(platforms.Options{DefaultBranch: "main"}) - if err != nil { - t.Fatalf("Generate() error: %v", err) - } - - content := files[".circleci/config.yml"] - if !strings.Contains(content, "workflows:") { - t.Error("config.yml must have 'workflows:' section") - } -} - -func TestCircleCIGenerator_DefaultBranch(t *testing.T) { - g := platforms.NewCircleCIGenerator() - files, err := g.Generate(platforms.Options{DefaultBranch: "release"}) - if err != nil { - t.Fatalf("Generate() error: %v", err) - } - - content := files[".circleci/config.yml"] - if strings.Count(content, "release") < 2 { - t.Error("expected custom branch 'release' to appear multiple times in config.yml") - } -} diff --git a/internal/platforms/github_actions.go b/internal/platforms/github_actions.go deleted file mode 100644 index d338323..0000000 --- a/internal/platforms/github_actions.go +++ /dev/null @@ -1,199 +0,0 @@ -package platforms - -import ( - "bytes" - "fmt" - "text/template" -) - -// GitHubActionsGenerator generates GitHub Actions workflow files. -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{} -} - -// Generate produces the following files: -// - .github/workflows/infra.yml — plan on PR, apply on push to main -// - .github/workflows/build.yml — build, test, push container -// - .github/workflows/deploy.yml — deploy after infra apply -func (g *GitHubActionsGenerator) Generate(opts Options) (map[string]string, error) { - runner := opts.Runner - if runner == "" { - runner = "self-hosted, Linux, X64" - } - branch := opts.DefaultBranch - if branch == "" { - branch = "main" - } - infra := opts.InfraConfig - if infra == "" { - infra = "infra.yaml" - } - - data := ghaData{ - Runner: runner, - DefaultBranch: branch, - InfraConfig: infra, - ProjectName: opts.ProjectName, - } - - files := map[string]string{} - - infraYAML, err := renderGHATemplate(ghaInfraTemplate, data) - if err != nil { - return nil, fmt.Errorf("github actions: render infra.yml: %w", err) - } - files[".github/workflows/infra.yml"] = infraYAML - - buildYAML, err := renderGHATemplate(ghaBuildTemplate, data) - if err != nil { - return nil, fmt.Errorf("github actions: render build.yml: %w", err) - } - files[".github/workflows/build.yml"] = buildYAML - - deployYAML, err := renderGHATemplate(ghaDeployTemplate, data) - if err != nil { - return nil, fmt.Errorf("github actions: render deploy.yml: %w", err) - } - files[".github/workflows/deploy.yml"] = deployYAML - - return files, nil -} - -type ghaData struct { - Runner string - DefaultBranch string - InfraConfig string - ProjectName string -} - -func renderGHATemplate(tmplStr string, data ghaData) (string, error) { - tmpl, err := template.New("").Parse(tmplStr) - if err != nil { - return "", err - } - var buf bytes.Buffer - if err := tmpl.Execute(&buf, data); err != nil { - return "", err - } - return buf.String(), nil -} - -// ghaInfraTemplate is the infra.yml template: plan on PR, apply on push to main. -const ghaInfraTemplate = `name: Infrastructure -on: - pull_request: - paths: - - '{{.InfraConfig}}' - - 'infra/**' - push: - branches: - - {{.DefaultBranch}} - paths: - - '{{.InfraConfig}}' - - 'infra/**' -permissions: - contents: read - pull-requests: write -jobs: - plan: - if: github.event_name == 'pull_request' - runs-on: [{{.Runner}}] - steps: - - uses: actions/checkout@v4 - - uses: GoCodeAlone/setup-wfctl@v1 - - name: Plan infrastructure - run: wfctl infra plan -c {{.InfraConfig}} --output plan.json - - name: Format plan as markdown - run: wfctl infra plan -c {{.InfraConfig}} --format markdown > plan.md - - name: Post plan comment - uses: actions/github-script@v7 - with: - script: | - const fs = require('fs'); - const plan = fs.readFileSync('plan.md', 'utf8'); - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: '## Infrastructure Plan\n\n' + plan - }); - apply: - if: github.event_name == 'push' && github.ref == 'refs/heads/{{.DefaultBranch}}' - runs-on: [{{.Runner}}] - steps: - - uses: actions/checkout@v4 - - uses: GoCodeAlone/setup-wfctl@v1 - - name: Apply infrastructure - run: wfctl infra apply -c {{.InfraConfig}} --auto-approve -` - -// ghaBuildTemplate is the build.yml template: build, test, push container. -const ghaBuildTemplate = `name: Build -on: - push: - branches: - - {{.DefaultBranch}} - pull_request: - branches: - - {{.DefaultBranch}} -permissions: - contents: read - packages: write -jobs: - build: - runs-on: [{{.Runner}}] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version-file: go.mod - cache: true - - name: Run tests - run: go test ./... - - name: Build - run: go build ./... - - name: Log in to GitHub Container Registry - if: github.event_name == 'push' - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{"{{"}} github.actor {{"}}"}} - password: ${{"{{"}} secrets.GITHUB_TOKEN {{"}}"}} - - name: Build and push container image - if: github.event_name == 'push' - uses: docker/build-push-action@v6 - with: - push: true - tags: ghcr.io/${{"{{"}} github.repository {{"}}"}}:${{"{{"}} github.sha {{"}}"}} -` - -// ghaDeployTemplate is the deploy.yml template: deploy after infra apply. -const ghaDeployTemplate = `name: Deploy -on: - workflow_run: - workflows: - - Infrastructure - types: - - completed - branches: - - {{.DefaultBranch}} -permissions: - contents: read -jobs: - deploy: - if: github.event.workflow_run.conclusion == 'success' - runs-on: [{{.Runner}}] - steps: - - uses: actions/checkout@v4 - - uses: GoCodeAlone/setup-wfctl@v1 - - name: Deploy application - run: | - wfctl infra apply -c {{.InfraConfig}} --auto-approve - wfctl deploy --image ghcr.io/${{"{{"}} github.repository {{"}}"}}:${{"{{"}} github.sha {{"}}"}} -` diff --git a/internal/platforms/github_actions_test.go b/internal/platforms/github_actions_test.go deleted file mode 100644 index 325c9c7..0000000 --- a/internal/platforms/github_actions_test.go +++ /dev/null @@ -1,163 +0,0 @@ -package platforms_test - -import ( - "strings" - "testing" - - "github.com/GoCodeAlone/workflow-plugin-ci-generator/internal/platforms" -) - -func TestGitHubActionsGenerator_Generate(t *testing.T) { - g := platforms.NewGitHubActionsGenerator() - opts := platforms.Options{ - InfraConfig: "infra.yaml", - ProjectName: "my-project", - Runner: "self-hosted, Linux, X64", - DefaultBranch: "main", - } - - files, err := g.Generate(opts) - if err != nil { - t.Fatalf("Generate() error: %v", err) - } - - expectedFiles := []string{ - ".github/workflows/infra.yml", - ".github/workflows/build.yml", - ".github/workflows/deploy.yml", - } - for _, f := range expectedFiles { - if _, ok := files[f]; !ok { - t.Errorf("expected file %q not generated", f) - } - } -} - -func TestGitHubActionsGenerator_InfraYML(t *testing.T) { - g := platforms.NewGitHubActionsGenerator() - opts := platforms.Options{ - InfraConfig: "infra.yaml", - DefaultBranch: "main", - Runner: "self-hosted, Linux, X64", - } - - files, err := g.Generate(opts) - if err != nil { - t.Fatalf("Generate() error: %v", err) - } - - content := files[".github/workflows/infra.yml"] - - checks := []string{ - "name: Infrastructure", - "uses: actions/checkout@v4", - "uses: GoCodeAlone/setup-wfctl@v1", - "permissions:", - "contents: read", - "pull-requests: write", - "uses: actions/github-script@v7", - "wfctl infra plan -c infra.yaml --output plan.json", - "wfctl infra plan -c infra.yaml --format markdown > plan.md", - "wfctl infra apply -c infra.yaml --auto-approve", - "refs/heads/main", - "self-hosted, Linux, X64", - } - for _, want := range checks { - if !strings.Contains(content, want) { - t.Errorf("infra.yml: expected to contain %q\ngot:\n%s", want, content) - } - } -} - -func TestGitHubActionsGenerator_BuildYML(t *testing.T) { - g := platforms.NewGitHubActionsGenerator() - opts := platforms.Options{ - DefaultBranch: "main", - Runner: "self-hosted, Linux, X64", - } - - files, err := g.Generate(opts) - if err != nil { - t.Fatalf("Generate() error: %v", err) - } - - content := files[".github/workflows/build.yml"] - - checks := []string{ - "name: Build", - "uses: actions/checkout@v4", - "uses: actions/setup-go@v5", - "go-version-file: go.mod", - "go test ./...", - "go build ./...", - "docker/login-action@v3", - "docker/build-push-action@v6", - "ghcr.io", - } - for _, want := range checks { - if !strings.Contains(content, want) { - t.Errorf("build.yml: expected to contain %q\ngot:\n%s", want, content) - } - } -} - -func TestGitHubActionsGenerator_DeployYML(t *testing.T) { - g := platforms.NewGitHubActionsGenerator() - opts := platforms.Options{ - InfraConfig: "infra.yaml", - DefaultBranch: "main", - Runner: "self-hosted, Linux, X64", - } - - files, err := g.Generate(opts) - if err != nil { - t.Fatalf("Generate() error: %v", err) - } - - content := files[".github/workflows/deploy.yml"] - - checks := []string{ - "name: Deploy", - "workflow_run:", - "Infrastructure", - "wfctl infra apply -c infra.yaml --auto-approve", - "wfctl deploy --image", - } - for _, want := range checks { - if !strings.Contains(content, want) { - t.Errorf("deploy.yml: expected to contain %q\ngot:\n%s", want, content) - } - } -} - -func TestGitHubActionsGenerator_DefaultsApplied(t *testing.T) { - g := platforms.NewGitHubActionsGenerator() - // empty options — defaults should kick in - files, err := g.Generate(platforms.Options{}) - if err != nil { - t.Fatalf("Generate() error: %v", err) - } - - infra := files[".github/workflows/infra.yml"] - if !strings.Contains(infra, "infra.yaml") { - t.Error("expected default infra config path 'infra.yaml'") - } - if !strings.Contains(infra, "refs/heads/main") { - t.Error("expected default branch 'main'") - } -} - -func TestGitHubActionsGenerator_CustomRunner(t *testing.T) { - g := platforms.NewGitHubActionsGenerator() - opts := platforms.Options{ - Runner: "ubuntu-latest", - DefaultBranch: "main", - } - files, err := g.Generate(opts) - if err != nil { - t.Fatalf("Generate() error: %v", err) - } - if !strings.Contains(files[".github/workflows/infra.yml"], "ubuntu-latest") { - t.Error("expected custom runner 'ubuntu-latest' in infra.yml") - } -} diff --git a/internal/platforms/gitlab_ci.go b/internal/platforms/gitlab_ci.go deleted file mode 100644 index 8b5ea0a..0000000 --- a/internal/platforms/gitlab_ci.go +++ /dev/null @@ -1,141 +0,0 @@ -package platforms - -import ( - "bytes" - "fmt" - "text/template" -) - -// GitLabCIGenerator generates a .gitlab-ci.yml file using GitLab CI v17+ syntax. -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{} -} - -// Generate produces: -// - .gitlab-ci.yml -func (g *GitLabCIGenerator) Generate(opts Options) (map[string]string, error) { - branch := opts.DefaultBranch - if branch == "" { - branch = "main" - } - infra := opts.InfraConfig - if infra == "" { - infra = "infra.yaml" - } - - data := gitlabData{ - DefaultBranch: branch, - InfraConfig: infra, - ProjectName: opts.ProjectName, - } - - content, err := renderGitLabTemplate(gitlabCITemplate, data) - if err != nil { - return nil, fmt.Errorf("gitlab ci: render .gitlab-ci.yml: %w", err) - } - - return map[string]string{ - ".gitlab-ci.yml": content, - }, nil -} - -type gitlabData struct { - DefaultBranch string - InfraConfig string - ProjectName string -} - -func renderGitLabTemplate(tmplStr string, data gitlabData) (string, error) { - tmpl, err := template.New("").Parse(tmplStr) - if err != nil { - return "", err - } - var buf bytes.Buffer - if err := tmpl.Execute(&buf, data); err != nil { - return "", err - } - return buf.String(), nil -} - -// gitlabCITemplate uses GitLab CI v17+ syntax: -// - rules: (not deprecated only:) -// - needs: for DAG pipeline ordering -// - environment: for deployment tracking -const gitlabCITemplate = `stages: - - plan - - apply - - build - - deploy - -default: - image: golang:1.26 - -variables: - INFRA_CONFIG: "{{.InfraConfig}}" - -# ── Plan ──────────────────────────────────────────────────────────────────── -infra-plan: - stage: plan - script: - - wfctl infra plan -c "$INFRA_CONFIG" --output plan.json - - wfctl infra plan -c "$INFRA_CONFIG" --format markdown > plan.md - artifacts: - paths: - - plan.json - - plan.md - expire_in: 1 hour - rules: - - if: $CI_PIPELINE_SOURCE == "merge_request_event" - changes: - - "{{.InfraConfig}}" - - "infra/**/*" - -# ── Apply ─────────────────────────────────────────────────────────────────── -infra-apply: - stage: apply - needs: - - job: infra-plan - artifacts: true - script: - - wfctl infra apply -c "$INFRA_CONFIG" --auto-approve - environment: - name: production - url: https://your-app.example.com - rules: - - if: $CI_COMMIT_BRANCH == "{{.DefaultBranch}}" && $CI_PIPELINE_SOURCE == "push" - changes: - - "{{.InfraConfig}}" - - "infra/**/*" - -# ── Build ─────────────────────────────────────────────────────────────────── -build: - stage: build - needs: [] - script: - - go test ./... - - go build ./... - - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . - - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - rules: - - if: $CI_COMMIT_BRANCH == "{{.DefaultBranch}}" - -# ── Deploy ────────────────────────────────────────────────────────────────── -deploy: - stage: deploy - needs: - - job: infra-apply - - job: build - script: - - wfctl deploy --image $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - environment: - name: production - url: https://your-app.example.com - rules: - - if: $CI_COMMIT_BRANCH == "{{.DefaultBranch}}" -` diff --git a/internal/platforms/gitlab_ci_test.go b/internal/platforms/gitlab_ci_test.go deleted file mode 100644 index 2744232..0000000 --- a/internal/platforms/gitlab_ci_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package platforms_test - -import ( - "strings" - "testing" - - "github.com/GoCodeAlone/workflow-plugin-ci-generator/internal/platforms" -) - -func TestGitLabCIGenerator_Generate(t *testing.T) { - g := platforms.NewGitLabCIGenerator() - opts := platforms.Options{ - InfraConfig: "infra.yaml", - ProjectName: "my-project", - DefaultBranch: "main", - } - - files, err := g.Generate(opts) - if err != nil { - t.Fatalf("Generate() error: %v", err) - } - - if _, ok := files[".gitlab-ci.yml"]; !ok { - t.Fatal("expected .gitlab-ci.yml in output") - } -} - -func TestGitLabCIGenerator_Syntax(t *testing.T) { - g := platforms.NewGitLabCIGenerator() - opts := platforms.Options{ - InfraConfig: "infra.yaml", - DefaultBranch: "main", - } - - files, err := g.Generate(opts) - if err != nil { - t.Fatalf("Generate() error: %v", err) - } - - content := files[".gitlab-ci.yml"] - - // Must use rules: not only: - if strings.Contains(content, "\nonly:") { - t.Error(".gitlab-ci.yml must not use deprecated 'only:' syntax") - } - if !strings.Contains(content, "rules:") { - t.Error(".gitlab-ci.yml must use 'rules:' syntax") - } - - // Must use needs: for DAG - if !strings.Contains(content, "needs:") { - t.Error(".gitlab-ci.yml must use 'needs:' for DAG pipeline ordering") - } - - // Must have environment: for deployment tracking - if !strings.Contains(content, "environment:") { - t.Error(".gitlab-ci.yml must use 'environment:' for deployment tracking") - } -} - -func TestGitLabCIGenerator_Stages(t *testing.T) { - g := platforms.NewGitLabCIGenerator() - files, err := g.Generate(platforms.Options{DefaultBranch: "main"}) - if err != nil { - t.Fatalf("Generate() error: %v", err) - } - - content := files[".gitlab-ci.yml"] - for _, stage := range []string{"plan", "apply", "build", "deploy"} { - if !strings.Contains(content, " - "+stage) { - t.Errorf("expected stage %q in stages list", stage) - } - } -} - -func TestGitLabCIGenerator_WfctlCommands(t *testing.T) { - g := platforms.NewGitLabCIGenerator() - files, err := g.Generate(platforms.Options{InfraConfig: "infra.yaml", DefaultBranch: "main"}) - if err != nil { - t.Fatalf("Generate() error: %v", err) - } - - content := files[".gitlab-ci.yml"] - checks := []string{ - "wfctl infra plan", - "wfctl infra apply", - "--auto-approve", - } - for _, want := range checks { - if !strings.Contains(content, want) { - t.Errorf("expected %q in .gitlab-ci.yml\ngot:\n%s", want, content) - } - } -} - -func TestGitLabCIGenerator_DefaultBranch(t *testing.T) { - g := platforms.NewGitLabCIGenerator() - files, err := g.Generate(platforms.Options{DefaultBranch: "develop"}) - if err != nil { - t.Fatalf("Generate() error: %v", err) - } - - content := files[".gitlab-ci.yml"] - if !strings.Contains(content, "develop") { - t.Error("expected custom default branch 'develop' in .gitlab-ci.yml") - } -} diff --git a/internal/platforms/jenkins.go b/internal/platforms/jenkins.go deleted file mode 100644 index 39cfed8..0000000 --- a/internal/platforms/jenkins.go +++ /dev/null @@ -1,140 +0,0 @@ -package platforms - -import ( - "bytes" - "fmt" - "text/template" -) - -// JenkinsGenerator generates a declarative Jenkinsfile. -type JenkinsGenerator struct{} - -// NewJenkinsGenerator returns a new JenkinsGenerator. -func NewJenkinsGenerator() *JenkinsGenerator { - return &JenkinsGenerator{} -} - -// Generate produces: -// - Jenkinsfile -func (g *JenkinsGenerator) Generate(opts Options) (map[string]string, error) { - branch := opts.DefaultBranch - if branch == "" { - branch = "main" - } - infra := opts.InfraConfig - if infra == "" { - infra = "infra.yaml" - } - - data := jenkinsData{ - DefaultBranch: branch, - InfraConfig: infra, - ProjectName: opts.ProjectName, - } - - content, err := renderJenkinsTemplate(jenkinsfileTemplate, data) - if err != nil { - return nil, fmt.Errorf("jenkins: render Jenkinsfile: %w", err) - } - - return map[string]string{ - "Jenkinsfile": content, - }, nil -} - -type jenkinsData struct { - DefaultBranch string - InfraConfig string - ProjectName string -} - -func renderJenkinsTemplate(tmplStr string, data jenkinsData) (string, error) { - tmpl, err := template.New("").Parse(tmplStr) - if err != nil { - return "", err - } - var buf bytes.Buffer - if err := tmpl.Execute(&buf, data); err != nil { - return "", err - } - return buf.String(), nil -} - -// jenkinsfileTemplate uses declarative pipeline syntax (not scripted). -const jenkinsfileTemplate = `pipeline { - agent { - label 'linux' - } - - environment { - INFRA_CONFIG = '{{.InfraConfig}}' - } - - stages { - stage('Checkout') { - steps { - checkout scm - } - } - - stage('Plan') { - when { - changeRequest() - } - steps { - sh 'wfctl infra plan -c $INFRA_CONFIG --output plan.json' - sh 'wfctl infra plan -c $INFRA_CONFIG --format markdown > plan.md' - } - post { - always { - archiveArtifacts artifacts: 'plan.json,plan.md', allowEmptyArchive: true - } - } - } - - stage('Build') { - steps { - sh 'go test ./...' - sh 'go build ./...' - sh 'docker build -t $REGISTRY_IMAGE:$GIT_COMMIT .' - } - } - - stage('Push Image') { - when { - branch '{{.DefaultBranch}}' - } - steps { - sh 'docker push $REGISTRY_IMAGE:$GIT_COMMIT' - } - } - - stage('Apply') { - when { - branch '{{.DefaultBranch}}' - } - steps { - sh 'wfctl infra apply -c $INFRA_CONFIG --auto-approve' - } - } - - stage('Deploy') { - when { - branch '{{.DefaultBranch}}' - } - steps { - sh 'wfctl deploy --image $REGISTRY_IMAGE:$GIT_COMMIT' - } - } - } - - post { - failure { - echo 'Pipeline failed. Check logs above.' - } - success { - echo 'Pipeline completed successfully.' - } - } -} -` diff --git a/internal/platforms/jenkins_test.go b/internal/platforms/jenkins_test.go deleted file mode 100644 index efd9776..0000000 --- a/internal/platforms/jenkins_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package platforms_test - -import ( - "strings" - "testing" - - "github.com/GoCodeAlone/workflow-plugin-ci-generator/internal/platforms" -) - -func TestJenkinsGenerator_Generate(t *testing.T) { - g := platforms.NewJenkinsGenerator() - opts := platforms.Options{ - InfraConfig: "infra.yaml", - ProjectName: "my-project", - DefaultBranch: "main", - } - - files, err := g.Generate(opts) - if err != nil { - t.Fatalf("Generate() error: %v", err) - } - - if _, ok := files["Jenkinsfile"]; !ok { - t.Fatal("expected Jenkinsfile in output") - } -} - -func TestJenkinsGenerator_DeclarativeSyntax(t *testing.T) { - g := platforms.NewJenkinsGenerator() - files, err := g.Generate(platforms.Options{DefaultBranch: "main"}) - if err != nil { - t.Fatalf("Generate() error: %v", err) - } - - content := files["Jenkinsfile"] - - // Must use declarative pipeline syntax - if !strings.HasPrefix(strings.TrimSpace(content), "pipeline {") { - t.Errorf("Jenkinsfile must start with 'pipeline {', got:\n%s", content[:min(80, len(content))]) - } - - // Must have agent block - if !strings.Contains(content, "agent {") { - t.Error("Jenkinsfile must have 'agent {' block") - } - - // Must have stages block - if !strings.Contains(content, "stages {") { - t.Error("Jenkinsfile must have 'stages {' block") - } - - // Each stage must use 'steps {' not raw sh calls - if !strings.Contains(content, "steps {") { - t.Error("Jenkinsfile must have 'steps {' inside stages") - } -} - -func TestJenkinsGenerator_RequiredStages(t *testing.T) { - g := platforms.NewJenkinsGenerator() - files, err := g.Generate(platforms.Options{DefaultBranch: "main", InfraConfig: "infra.yaml"}) - if err != nil { - t.Fatalf("Generate() error: %v", err) - } - - content := files["Jenkinsfile"] - for _, stage := range []string{"Plan", "Build", "Apply", "Deploy"} { - if !strings.Contains(content, "stage('"+stage+"')") { - t.Errorf("expected stage('%s') in Jenkinsfile", stage) - } - } -} - -func TestJenkinsGenerator_WfctlCommands(t *testing.T) { - g := platforms.NewJenkinsGenerator() - files, err := g.Generate(platforms.Options{InfraConfig: "infra.yaml", DefaultBranch: "main"}) - if err != nil { - t.Fatalf("Generate() error: %v", err) - } - - content := files["Jenkinsfile"] - checks := []string{ - "wfctl infra plan", - "wfctl infra apply", - "--auto-approve", - "wfctl deploy", - } - for _, want := range checks { - if !strings.Contains(content, want) { - t.Errorf("expected %q in Jenkinsfile\ngot:\n%s", want, content) - } - } -} - -func TestJenkinsGenerator_DefaultBranch(t *testing.T) { - g := platforms.NewJenkinsGenerator() - files, err := g.Generate(platforms.Options{DefaultBranch: "trunk"}) - if err != nil { - t.Fatalf("Generate() error: %v", err) - } - - content := files["Jenkinsfile"] - if !strings.Contains(content, "trunk") { - t.Error("expected custom default branch 'trunk' in Jenkinsfile") - } -} - -func min(a, b int) int { - if a < b { - return a - } - return b -} diff --git a/internal/platforms/options.go b/internal/platforms/options.go deleted file mode 100644 index 18e97b6..0000000 --- a/internal/platforms/options.go +++ /dev/null @@ -1,14 +0,0 @@ -// Package platforms provides CI/CD config generators for each supported platform. -package platforms - -// Options holds the common inputs all platform generators use. -type Options struct { - // InfraConfig is the path to the infra.yaml file (e.g. "infra.yaml"). - InfraConfig string - // ProjectName is used as a label in generated configs. - ProjectName string - // Runner is the CI runner label (used by GitHub Actions; ignored elsewhere). - Runner string - // DefaultBranch is the main branch name (default: "main"). - DefaultBranch string -} diff --git a/plugin.json b/plugin.json index f60c46c..4fc9fa5 100644 --- a/plugin.json +++ b/plugin.json @@ -1,13 +1,13 @@ { "name": "workflow-plugin-ci-generator", - "version": "0.1.6", + "version": "0.2.0", "description": "CI/CD config generator for GitHub Actions, GitLab CI, Jenkins, and CircleCI", "author": "GoCodeAlone", "license": "MIT", "type": "external", "tier": "core", "private": false, - "minEngineVersion": "0.67.0", + "minEngineVersion": "0.68.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",