From eeb87e044488ce6a2f529f13e03eae047c9e9c1d Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 19:04:45 +0000 Subject: [PATCH] feat(build): expose release workflow generation --- pkg/api/provider.go | 40 ++++++++++++++++++++++++++++++++++++++ pkg/api/provider_test.go | 34 ++++++++++++++++++++++++++++++-- pkg/build/workflow.go | 4 ++++ pkg/build/workflow_test.go | 17 ++++++++++++++++ 4 files changed, 93 insertions(+), 2 deletions(-) diff --git a/pkg/api/provider.go b/pkg/api/provider.go index 05e4b79..e75480e 100644 --- a/pkg/api/provider.go +++ b/pkg/api/provider.go @@ -105,6 +105,7 @@ func (p *BuildProvider) RegisterRoutes(rg *gin.RouterGroup) { rg.GET("/release/version", p.getVersion) rg.GET("/release/changelog", p.getChangelog) rg.POST("/release", p.triggerRelease) + rg.POST("/release/workflow", p.generateReleaseWorkflow) // SDK rg.GET("/sdk/diff", p.getSdkDiff) @@ -165,6 +166,13 @@ func (p *BuildProvider) Describe() []api.RouteDescription { Description: "Publishes pre-built artifacts from dist/ to configured targets.", Tags: []string{"release"}, }, + { + Method: "POST", + Path: "/release/workflow", + Summary: "Generate release workflow", + Description: "Writes the embedded GitHub Actions release workflow into .github/workflows/release.yml or a custom path.", + Tags: []string{"release", "workflow"}, + }, { Method: "GET", Path: "/sdk/diff", @@ -504,6 +512,38 @@ func (p *BuildProvider) triggerRelease(c *gin.Context) { })) } +type releaseWorkflowRequest struct { + Path string `json:"path"` +} + +func (p *BuildProvider) generateReleaseWorkflow(c *gin.Context) { + dir, err := p.resolveDir() + if err != nil { + c.JSON(http.StatusInternalServerError, api.Fail("resolve_failed", err.Error())) + return + } + + var req releaseWorkflowRequest + if err := c.ShouldBindJSON(&req); err != nil { + req.Path = "" + } + + path := req.Path + if path == "" { + path = build.ReleaseWorkflowPath(dir) + } + + if err := build.WriteReleaseWorkflow(p.medium, path); err != nil { + c.JSON(http.StatusInternalServerError, api.Fail("workflow_write_failed", err.Error())) + return + } + + c.JSON(http.StatusOK, api.OK(map[string]any{ + "generated": true, + "path": path, + })) +} + // -- SDK Handlers ------------------------------------------------------------- func (p *BuildProvider) getSdkDiff(c *gin.Context) { diff --git a/pkg/api/provider_test.go b/pkg/api/provider_test.go index cde1a63..9d64127 100644 --- a/pkg/api/provider_test.go +++ b/pkg/api/provider_test.go @@ -3,9 +3,14 @@ package api import ( + "bytes" "io/fs" + "net/http" + "net/http/httptest" "testing" + "github.com/gin-gonic/gin" + "dappco.re/go/core/build/internal/ax" "dappco.re/go/core/build/pkg/build" "dappco.re/go/core/io" @@ -45,8 +50,8 @@ func TestProvider_BuildProviderDescribe_Good(t *testing.T) { p := NewProvider(".", nil) routes := p.Describe() - // Should have 9 endpoint descriptions - assert.Len(t, routes, 9) + // Should have 10 endpoint descriptions + assert.Len(t, routes, 10) // Verify key routes exist paths := make(map[string]string) @@ -61,6 +66,7 @@ func TestProvider_BuildProviderDescribe_Good(t *testing.T) { assert.Equal(t, "GET", paths["/release/version"]) assert.Equal(t, "GET", paths["/release/changelog"]) assert.Equal(t, "POST", paths["/release"]) + assert.Equal(t, "POST", paths["/release/workflow"]) assert.Equal(t, "GET", paths["/sdk/diff"]) assert.Equal(t, "POST", paths["/sdk/generate"]) } @@ -152,3 +158,27 @@ func TestProvider_ResolveProjectType_Good(t *testing.T) { assert.Equal(t, build.ProjectTypeGo, projectType) }) } + +func TestProvider_GenerateReleaseWorkflow_Good(t *testing.T) { + gin.SetMode(gin.TestMode) + + projectDir := t.TempDir() + p := NewProvider(projectDir, nil) + + recorder := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodPost, "/release/workflow", bytes.NewBufferString(`{}`)) + request.Header.Set("Content-Type", "application/json") + + ctx, _ := gin.CreateTestContext(recorder) + ctx.Request = request + + p.generateReleaseWorkflow(ctx) + + assert.Equal(t, http.StatusOK, recorder.Code) + + path := build.ReleaseWorkflowPath(projectDir) + content, err := io.Local.Read(path) + require.NoError(t, err) + assert.Contains(t, content, "workflow_call:") + assert.Contains(t, content, "workflow_dispatch:") +} diff --git a/pkg/build/workflow.go b/pkg/build/workflow.go index 7f9cfbe..4038ae6 100644 --- a/pkg/build/workflow.go +++ b/pkg/build/workflow.go @@ -33,6 +33,10 @@ func WriteReleaseWorkflow(fs io_interface.Medium, path string) error { return coreerr.E("build.WriteReleaseWorkflow", "failed to read embedded workflow template", err) } + if err := fs.EnsureDir(ax.Dir(path)); err != nil { + return coreerr.E("build.WriteReleaseWorkflow", "failed to create release workflow directory", err) + } + if err := fs.Write(path, string(content)); err != nil { return coreerr.E("build.WriteReleaseWorkflow", "failed to write release workflow", err) } diff --git a/pkg/build/workflow_test.go b/pkg/build/workflow_test.go index edf8eb5..525ae53 100644 --- a/pkg/build/workflow_test.go +++ b/pkg/build/workflow_test.go @@ -3,6 +3,7 @@ package build import ( "testing" + "dappco.re/go/core/build/internal/ax" "dappco.re/go/core/io" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -37,6 +38,22 @@ func TestWorkflow_WriteReleaseWorkflow_Good(t *testing.T) { require.NoError(t, err) assert.NotEmpty(t, content) }) + + t.Run("creates parent directories on a real filesystem", func(t *testing.T) { + projectDir := t.TempDir() + path := ax.Join(projectDir, ".github", "workflows", "release.yml") + + err := WriteReleaseWorkflow(io.Local, path) + require.NoError(t, err) + + content, err := io.Local.Read(path) + require.NoError(t, err) + + template, err := releaseWorkflowTemplate.ReadFile("templates/release.yml") + require.NoError(t, err) + + assert.Equal(t, string(template), content) + }) } func TestWorkflow_ReleaseWorkflowPath_Good(t *testing.T) {