Skip to content

Commit 065aed7

Browse files
feat: add support for workflow.local.yaml (#1634)
Adds support for `workflow.local.yaml` Incorporates this into `dependents`. If the user declines to clone a repo, we will offer to create this file and guide them to edit it Needs speakeasy-api/sdk-gen-config#94 <img width="2198" height="1454" alt="CleanShot 2025-09-26 at 12 44 28@2x" src="https://github.com/user-attachments/assets/b6f591f9-b2ca-42b4-b8e9-e3872d1b0599" />
1 parent 5455b49 commit 065aed7

File tree

4 files changed

+137
-11
lines changed

4 files changed

+137
-11
lines changed

cmd/configure.go

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ var configureCmd = &model.CommandGroup{
6464
Short: "Configure your Speakeasy SDK Setup.",
6565
Long: utils.RenderMarkdown(configureLong),
6666
InteractiveMsg: "What do you want to configure?",
67-
Commands: []model.Command{configureSourcesCmd, configureTargetCmd, configureGithubCmd, configurePublishingCmd, configureTestingCmd},
67+
Commands: []model.Command{configureSourcesCmd, configureTargetCmd, configureGithubCmd, configurePublishingCmd, configureTestingCmd, configureLocalWorkflowCmd},
6868
}
6969

7070
type ConfigureSourcesFlags struct {
@@ -176,6 +176,24 @@ var configureTestingCmd = &model.ExecutableCommand[ConfigureTestsFlags]{
176176
RequiresAuth: true,
177177
}
178178

179+
type ConfigureLocalWorkflowFlags struct {
180+
WorkflowDirectory string `json:"workflow-directory"`
181+
}
182+
183+
var configureLocalWorkflowCmd = &model.ExecutableCommand[ConfigureLocalWorkflowFlags]{
184+
Usage: "local-workflow",
185+
Short: "Create a local workflow configuration file.",
186+
Long: "Copies workflow.yaml to workflow.local.yaml with all settings commented out for local overrides.",
187+
Run: configureLocalWorkflow,
188+
Flags: []flag.Flag{
189+
flag.StringFlag{
190+
Name: "workflow-directory",
191+
Shorthand: "d",
192+
Description: "directory of speakeasy workflow file",
193+
},
194+
},
195+
}
196+
179197
func configureSources(ctx context.Context, flags ConfigureSourcesFlags) error {
180198
workingDir, err := os.Getwd()
181199
if err != nil {
@@ -1052,6 +1070,44 @@ func getActionWorkingDirectoryFromFlag(rootDir string, workflowDir string) strin
10521070
return actionWorkingDir
10531071
}
10541072

1073+
func configureLocalWorkflow(ctx context.Context, flags ConfigureLocalWorkflowFlags) error {
1074+
logger := log.From(ctx)
1075+
1076+
workingDir, err := os.Getwd()
1077+
if err != nil {
1078+
return err
1079+
}
1080+
1081+
actionWorkingDir := getActionWorkingDirectoryFromFlag(workingDir, flags.WorkflowDirectory)
1082+
workflowDir := filepath.Join(workingDir, actionWorkingDir)
1083+
1084+
localWorkflowPath := filepath.Join(workflowDir, ".speakeasy", "workflow.local.yaml")
1085+
1086+
// Check if workflow.yaml exists first
1087+
workflowPath := filepath.Join(workflowDir, ".speakeasy", "workflow.yaml")
1088+
if _, err := os.Stat(workflowPath); os.IsNotExist(err) {
1089+
return renderAndPrintWorkflowNotFound("local-workflow", logger)
1090+
}
1091+
1092+
// Check if workflow.local.yaml already exists
1093+
if _, err := os.Stat(localWorkflowPath); err == nil {
1094+
logger.Println(styles.Info.Render(fmt.Sprintf("workflow.local.yaml already exists at %s", localWorkflowPath)))
1095+
logger.Println(styles.Info.Render("Remove the existing file if you want to regenerate it."))
1096+
return nil
1097+
}
1098+
1099+
if err := run.CreateWorkflowLocalFile(workflowDir); err != nil {
1100+
return errors.Wrapf(err, "failed to create workflow.local.yaml")
1101+
}
1102+
1103+
boxStyle := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(styles.Colors.Green).Padding(0, 1)
1104+
successMsg := fmt.Sprintf("Successfully created workflow.local.yaml 🎉\n\nLocation: %s\n\nYou can now uncomment and modify any field in this file to override values from workflow.yaml for local development.", localWorkflowPath)
1105+
success := styles.Success.Render(successMsg)
1106+
logger.PrintfStyled(boxStyle, "%s", success)
1107+
1108+
return nil
1109+
}
1110+
10551111
func renderAndPrintWorkflowNotFound(cmd string, logger log.Logger) error {
10561112
msg := styles.RenderErrorMessage("we couldn't find your Speakeasy workflow file (*.speakeasy/workflow.yaml*)",
10571113
lipgloss.Left,

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ require (
4040
github.com/speakeasy-api/openapi v1.7.3
4141
github.com/speakeasy-api/openapi-generation/v2 v2.716.10
4242
github.com/speakeasy-api/openapi-overlay v0.10.3
43-
github.com/speakeasy-api/sdk-gen-config v1.34.0
43+
github.com/speakeasy-api/sdk-gen-config v1.35.0
4444
github.com/speakeasy-api/speakeasy-client-sdk-go/v3 v3.26.7
4545
github.com/speakeasy-api/speakeasy-core v0.20.9
4646
github.com/speakeasy-api/versioning-reports v0.6.1
@@ -61,7 +61,7 @@ require (
6161
atomicgo.dev/cursor v0.2.0 // indirect
6262
atomicgo.dev/keyboard v0.2.9 // indirect
6363
atomicgo.dev/schedule v0.1.0 // indirect
64-
dario.cat/mergo v1.0.1 // indirect
64+
dario.cat/mergo v1.0.2 // indirect
6565
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
6666
github.com/Masterminds/goutils v1.1.1 // indirect
6767
github.com/Masterminds/semver/v3 v3.3.0 // indirect

go.sum

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8=
66
atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ=
77
atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=
88
atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=
9-
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
10-
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
9+
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
10+
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
1111
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
1212
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
1313
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
@@ -545,10 +545,8 @@ github.com/speakeasy-api/openapi-generation/v2 v2.716.10 h1:4JqbXxstVUpmRRjyPCeP
545545
github.com/speakeasy-api/openapi-generation/v2 v2.716.10/go.mod h1:fy5/XuA7hfY9pJkesh8av1l6PoQT1KFjzLZe38OAYFI=
546546
github.com/speakeasy-api/openapi-overlay v0.10.3 h1:70een4vwHyslIp796vM+ox6VISClhtXsCjrQNhxwvWs=
547547
github.com/speakeasy-api/openapi-overlay v0.10.3/go.mod h1:RJjV0jbUHqXLS0/Mxv5XE7LAnJHqHw+01RDdpoGqiyY=
548-
github.com/speakeasy-api/sdk-gen-config v1.33.0 h1:3aBd3aiwtH+t/Yj2AsGpHg/AD/80WATMqJNPErVZjbY=
549-
github.com/speakeasy-api/sdk-gen-config v1.33.0/go.mod h1:2KSnHvP6SxS/NFBddH9B+SrcopPyY8MsDlW1kiG9Pqs=
550-
github.com/speakeasy-api/sdk-gen-config v1.34.0 h1:KSAmSBTApmwq4wrgqVG6KvgcqYM67tXUYon9VlLvyyc=
551-
github.com/speakeasy-api/sdk-gen-config v1.34.0/go.mod h1:2KSnHvP6SxS/NFBddH9B+SrcopPyY8MsDlW1kiG9Pqs=
548+
github.com/speakeasy-api/sdk-gen-config v1.35.0 h1:9yM5BwCqs+HmbyjIXwy+bciIdZROSJJWp98prFwe0OE=
549+
github.com/speakeasy-api/sdk-gen-config v1.35.0/go.mod h1:/3Yl/NF8tG4OVxNF9TAi5GTMeqbZIgqDDbwLkZ7L/9g=
552550
github.com/speakeasy-api/speakeasy-client-sdk-go/v3 v3.26.7 h1:SoWZkRlpFlv8qibCfXWrBZay1JeLS9uqJ+1cu+DFgXo=
553551
github.com/speakeasy-api/speakeasy-client-sdk-go/v3 v3.26.7/go.mod h1:k9JD6Rj0+Iizc5COoLZHyRIOGGITpKZ2qBuFFO8SqNI=
554552
github.com/speakeasy-api/speakeasy-core v0.20.9 h1:khs1KjvQ1Ery5tJulDL/jCphsGrFq4TJpt0no9lY760=

internal/run/dependent.go

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,24 @@ func processDependent(ctx context.Context, dependentName string, dependent workf
9494

9595
logger.Printf("Location %s does not exist for dependent %s", location, dependentName)
9696

97-
// Ask user if they want to clone the repository
98-
if !interactivity.SimpleConfirm("Would you like to clone the repository?", true) {
97+
// Ask user if they want to clone the repository to the specified location
98+
prompt := fmt.Sprintf("Would you like to clone the repository to %s?", location)
99+
if !interactivity.SimpleConfirm(prompt, true) {
100+
logger.Printf("\n🚫 Repository clone declined.")
101+
logger.Printf("💡 You can override the dependent location by creating a local workflow configuration file.")
102+
103+
// Ask if they want to create workflow.local.yaml for local overrides
104+
localWorkflowPath := filepath.Join(projectDir, ".speakeasy", "workflow.local.yaml")
105+
prompt := fmt.Sprintf("Would you like to create %s to customize dependent locations?", localWorkflowPath)
106+
if interactivity.SimpleConfirm(prompt, true) {
107+
if err := CreateWorkflowLocalFile(projectDir); err != nil {
108+
logger.Printf("❌ Failed to create workflow.local.yaml: %v", err)
109+
} else {
110+
logger.Printf("✅ Created %s", localWorkflowPath)
111+
logger.Printf("📝 You can now uncomment and modify the dependents section to set custom locations.")
112+
}
113+
}
114+
99115
return fmt.Errorf("location %s does not exist and user declined to clone", location)
100116
}
101117

@@ -174,3 +190,59 @@ func runSpeakeasyFromLocation(ctx context.Context, location, command, flagsStrin
174190

175191
return cmd.Run()
176192
}
193+
194+
func CreateWorkflowLocalFile(workflowDir string) error {
195+
workflowPath := filepath.Join(workflowDir, ".speakeasy", "workflow.yaml")
196+
localWorkflowPath := filepath.Join(workflowDir, ".speakeasy", "workflow.local.yaml")
197+
198+
if _, err := os.Stat(workflowPath); os.IsNotExist(err) {
199+
return fmt.Errorf("workflow.yaml file not found at %s", workflowPath)
200+
}
201+
202+
if _, err := os.Stat(localWorkflowPath); err == nil {
203+
return fmt.Errorf("workflow.local.yaml already exists at %s", localWorkflowPath)
204+
}
205+
206+
workflowContent, err := os.ReadFile(workflowPath)
207+
if err != nil {
208+
return fmt.Errorf("failed to read workflow.yaml: %w", err)
209+
}
210+
211+
commentedContent := commentOutYAMLContent(string(workflowContent))
212+
213+
instructions := `# Local Workflow Configuration Override File
214+
#
215+
# This file allows you to override any field from workflow.yaml for local development.
216+
# Uncomment and modify any section below to override the corresponding values.
217+
#
218+
# Only uncomment the specific fields (and their parent keys) that you want to override - you don't need to
219+
# uncomment entire sections if you only want to change one value.
220+
#
221+
# Example: To override just the speakeasyVersion, uncomment only that line:
222+
# speakeasyVersion: "1.234.0"
223+
224+
`
225+
226+
finalContent := instructions + commentedContent
227+
228+
if err := os.WriteFile(localWorkflowPath, []byte(finalContent), 0644); err != nil {
229+
return fmt.Errorf("failed to write workflow.local.yaml: %w", err)
230+
}
231+
232+
return nil
233+
}
234+
235+
func commentOutYAMLContent(content string) string {
236+
lines := strings.Split(content, "\n")
237+
var commentedLines []string
238+
239+
for _, line := range lines {
240+
if strings.TrimSpace(line) == "" {
241+
commentedLines = append(commentedLines, line)
242+
} else {
243+
commentedLines = append(commentedLines, "# "+line)
244+
}
245+
}
246+
247+
return strings.Join(commentedLines, "\n")
248+
}

0 commit comments

Comments
 (0)