Skip to content

Commit f717c21

Browse files
committed
refactor: add workflow writer interface
Introduced WorkflowWriter interface to decouple workflow generation from filesystem operations. Previously, githubWorkflow.Persist() directly wrote files, making unit tests require filesystem I/O. Now, Export() accepts a WorkflowWriter, enabling tests to verify YAML generation in-memory using BufferWriter while production code uses fileWriter for actual file operations. Moved feature flag check from runtime to command registration, preventing the subcommand from appearing when feature is disabled rather than failing during execution. Reorganized tests into unit tests (mocked I/O) and integration tests (real filesystem). Additional changes: - Made workflow types private (githubWorkflow, job, step, etc) - Renamed *Option constants to *Flag for consistency - Corrected GitHub capitalization throughout codebase - Added MockLoaderSaver to common package for test reuse Issue SRVOCF-744 Signed-off-by: Stanislav Jakuschevskij <sjakusch@redhat.com>
1 parent bf1bbe2 commit f717c21

File tree

12 files changed

+366
-316
lines changed

12 files changed

+366
-316
lines changed

cmd/ci/config.go

Lines changed: 32 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,43 +9,42 @@ import (
99
const (
1010
ConfigCIFeatureFlag = "FUNC_ENABLE_CI_CONFIG"
1111

12-
// TODO(twoGiants): *Option -> *Flag
13-
PathOption = "path"
12+
PathFlag = "path"
1413

15-
DefaultGithubWorkflowDir = ".github/workflows"
16-
DefaultGithubWorkflowFilename = "func-deploy.yaml"
14+
DefaultGitHubWorkflowDir = ".github/workflows"
15+
DefaultGitHubWorkflowFilename = "func-deploy.yaml"
1716

18-
BranchOption = "branch"
17+
BranchFlag = "branch"
1918
DefaultBranch = "main"
2019

21-
WorkflowNameOption = "workflow-name"
20+
WorkflowNameFlag = "workflow-name"
2221
DefaultWorkflowName = "Func Deploy"
2322

24-
KubeconfigSecretNameOption = "kubeconfig-secret-name"
23+
KubeconfigSecretNameFlag = "kubeconfig-secret-name"
2524
DefaultKubeconfigSecretName = "KUBECONFIG"
2625

27-
RegistryLoginUrlVariableNameOption = "registry-login-url-variable-name"
26+
RegistryLoginUrlVariableNameFlag = "registry-login-url-variable-name"
2827
DefaultRegistryLoginUrlVariableName = "REGISTRY_LOGIN_URL"
2928

30-
RegistryUserVariableNameOption = "registry-user-variable-name"
29+
RegistryUserVariableNameFlag = "registry-user-variable-name"
3130
DefaultRegistryUserVariableName = "REGISTRY_USERNAME"
3231

33-
RegistryPassSecretNameOption = "registry-pass-secret-name"
32+
RegistryPassSecretNameFlag = "registry-pass-secret-name"
3433
DefaultRegistryPassSecretName = "REGISTRY_PASSWORD"
3534

36-
RegistryUrlVariableNameOption = "registry-url-variable-name"
35+
RegistryUrlVariableNameFlag = "registry-url-variable-name"
3736
DefaultRegistryUrlVariableName = "REGISTRY_URL"
3837

39-
UseRegistryLoginOption = "use-registry-login"
38+
UseRegistryLoginFlag = "use-registry-login"
4039
DefaultUseRegistryLogin = true
4140

42-
UseDebugOption = "debug"
41+
UseDebugFlag = "debug"
4342
DefaultUseDebug = false
4443

45-
UseRemoteBuild = "remote"
44+
UseRemoteBuildFlag = "remote"
4645
DefaultUseRemoteBuild = false
4746

48-
UseSelfHostedRunner = "self-hosted-runner"
47+
UseSelfHostedRunnerFlag = "self-hosted-runner"
4948
DefaultUseSelfHostedRunner = false
5049
)
5150

@@ -67,31 +66,31 @@ type CIConfig struct {
6766
debug bool
6867
}
6968

70-
func NewCiGithubConfig() CIConfig {
69+
func NewCIGitHubConfig() CIConfig {
7170
return CIConfig{
72-
githubWorkflowDir: DefaultGithubWorkflowDir,
73-
githubWorkflowFilename: DefaultGithubWorkflowFilename,
74-
path: viper.GetString(PathOption),
75-
branch: viper.GetString(BranchOption),
76-
workflowName: viper.GetString(WorkflowNameOption),
77-
kubeconfigSecret: viper.GetString(KubeconfigSecretNameOption),
78-
registryLoginUrlVar: viper.GetString(RegistryLoginUrlVariableNameOption),
79-
registryUserVar: viper.GetString(RegistryUserVariableNameOption),
80-
registryPassSecret: viper.GetString(RegistryPassSecretNameOption),
81-
registryUrlVar: viper.GetString(RegistryUrlVariableNameOption),
82-
useRegistryLogin: viper.GetBool(UseRegistryLoginOption),
83-
useRemoteBuild: viper.GetBool(UseRemoteBuild),
84-
useSelfHostedRunner: viper.GetBool(UseSelfHostedRunner),
85-
debug: viper.GetBool(UseDebugOption),
71+
githubWorkflowDir: DefaultGitHubWorkflowDir,
72+
githubWorkflowFilename: DefaultGitHubWorkflowFilename,
73+
path: viper.GetString(PathFlag),
74+
branch: viper.GetString(BranchFlag),
75+
workflowName: viper.GetString(WorkflowNameFlag),
76+
kubeconfigSecret: viper.GetString(KubeconfigSecretNameFlag),
77+
registryLoginUrlVar: viper.GetString(RegistryLoginUrlVariableNameFlag),
78+
registryUserVar: viper.GetString(RegistryUserVariableNameFlag),
79+
registryPassSecret: viper.GetString(RegistryPassSecretNameFlag),
80+
registryUrlVar: viper.GetString(RegistryUrlVariableNameFlag),
81+
useRegistryLogin: viper.GetBool(UseRegistryLoginFlag),
82+
useRemoteBuild: viper.GetBool(UseRemoteBuildFlag),
83+
useSelfHostedRunner: viper.GetBool(UseSelfHostedRunnerFlag),
84+
debug: viper.GetBool(UseDebugFlag),
8685
}
8786
}
8887

89-
func (cc *CIConfig) FnGithubWorkflowDir(fnRoot string) string {
88+
func (cc *CIConfig) FnGitHubWorkflowDir(fnRoot string) string {
9089
return filepath.Join(fnRoot, cc.githubWorkflowDir)
9190
}
9291

93-
func (cc *CIConfig) FnGithubWorkflowFilepath(fnRoot string) string {
94-
return filepath.Join(cc.FnGithubWorkflowDir(fnRoot), cc.githubWorkflowFilename)
92+
func (cc *CIConfig) FnGitHubWorkflowFilepath(fnRoot string) string {
93+
return filepath.Join(cc.FnGitHubWorkflowDir(fnRoot), cc.githubWorkflowFilename)
9594
}
9695

9796
func (cc *CIConfig) Path() string {

cmd/ci/workflow.go

Lines changed: 60 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -3,54 +3,31 @@ package ci
33
import (
44
"bytes"
55
"fmt"
6-
"os"
7-
"path/filepath"
86

97
"gopkg.in/yaml.v3"
108
)
119

12-
const (
13-
dirPerm = 0755 // o: rwx, g|u: r-x
14-
filePerm = 0644 // o: rw, g|u: r
15-
)
16-
17-
// TODO(twoGiants)
18-
// - encapsulate => create Interface and defaultGithubWorkflow struct
19-
// - provide printers for configurable properties
20-
// - provide toYamlString for checks in tests
21-
type GithubWorkflow struct {
10+
type githubWorkflow struct {
2211
Name string `yaml:"name"`
23-
On WorkflowTriggers `yaml:"on"`
24-
Jobs map[string]Job `yaml:"jobs"`
12+
On workflowTriggers `yaml:"on"`
13+
Jobs map[string]job `yaml:"jobs"`
2514
}
2615

27-
type WorkflowTriggers struct {
28-
Push *PushTrigger `yaml:"push,omitempty"`
16+
type workflowTriggers struct {
17+
Push *pushTrigger `yaml:"push,omitempty"`
2918
WorkflowDispatch *struct{} `yaml:"workflow_dispatch,omitempty"`
3019
}
3120

32-
func newPushTrigger(branch string, debug bool) WorkflowTriggers {
33-
result := WorkflowTriggers{
34-
Push: &PushTrigger{Branches: []string{branch}},
35-
}
36-
37-
if debug {
38-
result.WorkflowDispatch = &struct{}{}
39-
}
40-
41-
return result
42-
}
43-
44-
type PushTrigger struct {
21+
type pushTrigger struct {
4522
Branches []string `yaml:"branches,omitempty"`
4623
}
4724

48-
type Job struct {
25+
type job struct {
4926
RunsOn string `yaml:"runs-on"`
50-
Steps []Step `yaml:"steps"`
27+
Steps []step `yaml:"steps"`
5128
}
5229

53-
type Step struct {
30+
type step struct {
5431
Name string `yaml:"name,omitempty"`
5532
ID string `yaml:"id,omitempty"`
5633
If string `yaml:"if,omitempty"`
@@ -59,50 +36,17 @@ type Step struct {
5936
With map[string]string `yaml:"with,omitempty"`
6037
}
6138

62-
func newStep(name string) *Step {
63-
return &Step{Name: name}
64-
}
65-
66-
func (s *Step) withID(id string) *Step {
67-
s.ID = id
68-
return s
69-
}
70-
71-
func (s *Step) withIf(ifCond string) *Step {
72-
s.If = ifCond
73-
return s
74-
}
75-
76-
func (s *Step) withUses(u string) *Step {
77-
s.Uses = u
78-
return s
79-
}
80-
81-
func (s *Step) withRun(r string) *Step {
82-
s.Run = r
83-
return s
84-
}
85-
86-
func (s *Step) withActionConfig(key, value string) *Step {
87-
if s.With == nil {
88-
s.With = make(map[string]string)
89-
}
90-
91-
s.With[key] = value
92-
93-
return s
94-
}
95-
9639
// TODO(twoGiants): add validation => no empty values, etc.
97-
func NewGithubWorkflow(conf CIConfig) *GithubWorkflow {
40+
func NewGitHubWorkflow(conf CIConfig) *githubWorkflow {
41+
// TODO(twoGiants): add more runner labels => for GitHub enterprise clients
9842
runsOn := "ubuntu-latest"
9943
if conf.UseSelfHostedRunner() {
10044
runsOn = "self-hosted"
10145
}
10246

10347
pushTrigger := newPushTrigger(conf.Branch(), conf.UseDebug())
10448

105-
var steps []Step
49+
var steps []step
10650
checkoutCode := newStep("Checkout code").
10751
withUses("actions/checkout@v4")
10852
steps = append(steps, *checkoutCode)
@@ -160,10 +104,10 @@ func NewGithubWorkflow(conf CIConfig) *GithubWorkflow {
160104
withRun(runFuncDeploy + " --registry=" + registryUrl + " -v")
161105
steps = append(steps, *deployFunc)
162106

163-
return &GithubWorkflow{
107+
return &githubWorkflow{
164108
Name: name,
165109
On: pushTrigger,
166-
Jobs: map[string]Job{
110+
Jobs: map[string]job{
167111
"deploy": {
168112
RunsOn: runsOn,
169113
Steps: steps,
@@ -172,47 +116,70 @@ func NewGithubWorkflow(conf CIConfig) *GithubWorkflow {
172116
}
173117
}
174118

175-
func NewGithubWorkflowFromPath(path string) (*GithubWorkflow, error) {
176-
raw, err := os.ReadFile(path)
177-
if err != nil {
178-
return nil, err
119+
func newPushTrigger(branch string, debug bool) workflowTriggers {
120+
result := workflowTriggers{
121+
Push: &pushTrigger{Branches: []string{branch}},
179122
}
180123

181-
var result GithubWorkflow
182-
if err = yaml.Unmarshal(raw, &result); err != nil {
183-
return nil, err
124+
if debug {
125+
result.WorkflowDispatch = &struct{}{}
184126
}
185127

186-
return &result, nil
128+
return result
187129
}
188130

189-
func (gw *GithubWorkflow) Persist(path string) error {
190-
raw, err := gw.toYaml()
191-
if err != nil {
192-
return err
193-
}
131+
func newStep(name string) *step {
132+
return &step{Name: name}
133+
}
194134

195-
if err := os.MkdirAll(filepath.Dir(path), dirPerm); err != nil {
196-
return err
197-
}
135+
func (s *step) withID(id string) *step {
136+
s.ID = id
137+
return s
138+
}
198139

199-
if err := os.WriteFile(path, raw, filePerm); err != nil {
200-
return err
140+
func (s *step) withIf(ifCond string) *step {
141+
s.If = ifCond
142+
return s
143+
}
144+
145+
func (s *step) withUses(u string) *step {
146+
s.Uses = u
147+
return s
148+
}
149+
150+
func (s *step) withRun(r string) *step {
151+
s.Run = r
152+
return s
153+
}
154+
155+
func (s *step) withActionConfig(key, value string) *step {
156+
if s.With == nil {
157+
s.With = make(map[string]string)
201158
}
202159

203-
return nil
160+
s.With[key] = value
161+
162+
return s
163+
}
164+
165+
func newSecret(key string) string {
166+
return fmt.Sprintf("${{ secrets.%s }}", key)
167+
}
168+
169+
func newVariable(key string) string {
170+
return fmt.Sprintf("${{ vars.%s }}", key)
204171
}
205172

206-
func (gw *GithubWorkflow) YamlString() (string, error) {
173+
func (gw *githubWorkflow) Export(path string, w WorkflowWriter) error {
207174
raw, err := gw.toYaml()
208175
if err != nil {
209-
return "", err
176+
return err
210177
}
211178

212-
return string(raw), nil
179+
return w.Write(path, raw)
213180
}
214181

215-
func (gw *GithubWorkflow) toYaml() ([]byte, error) {
182+
func (gw *githubWorkflow) toYaml() ([]byte, error) {
216183
var buf bytes.Buffer
217184
encoder := yaml.NewEncoder(&buf)
218185
encoder.SetIndent(2)
@@ -224,11 +191,3 @@ func (gw *GithubWorkflow) toYaml() ([]byte, error) {
224191

225192
return buf.Bytes(), nil
226193
}
227-
228-
func newSecret(key string) string {
229-
return fmt.Sprintf("${{ secrets.%s }}", key)
230-
}
231-
232-
func newVariable(key string) string {
233-
return fmt.Sprintf("${{ vars.%s }}", key)
234-
}

cmd/ci/workflow_test.go

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
11
package ci_test
22

33
import (
4+
"strings"
45
"testing"
56

67
"gotest.tools/v3/assert"
78
"knative.dev/func/cmd/ci"
89
)
910

10-
func TestGithubWorkflow_PersistAndLoad(t *testing.T) {
11+
func TestGitHubWorkflow_Export(t *testing.T) {
1112
// GIVEN
12-
gw := ci.NewGithubWorkflow(ci.NewCiGithubConfig())
13-
targetPath := t.TempDir() + "/" + gw.Name + ".yaml"
13+
gw := ci.NewGitHubWorkflow(ci.NewCIGitHubConfig())
14+
bufferWriter := ci.NewBufferWriter()
1415

1516
// WHEN
16-
persistErr := gw.Persist(targetPath)
17-
actualGw, loadErr := ci.NewGithubWorkflowFromPath(targetPath)
17+
exportErr := gw.Export("path", bufferWriter)
1818

1919
// THEN
20-
assert.NilError(t, persistErr, "unexpected error when persisting Github Workflow")
21-
assert.NilError(t, loadErr, "unexpected error when loading Github Workflow")
22-
assert.Equal(t, actualGw.Name, gw.Name, "expected names to be equal")
20+
assert.NilError(t, exportErr, "unexpected error when exporting GitHub Workflow")
21+
assert.Assert(t, strings.Contains(bufferWriter.Buffer.String(), gw.Name))
2322
}

0 commit comments

Comments
 (0)