diff --git a/blueprint/pkg/loader/loader.go b/blueprint/pkg/loader/loader.go index 8af90d8a..e25128e0 100644 --- a/blueprint/pkg/loader/loader.go +++ b/blueprint/pkg/loader/loader.go @@ -35,6 +35,9 @@ type InjectorOverrider func(value cue.Value) map[string]string type BlueprintLoader interface { // Load loads the blueprint. Load(projectPath, gitRootPath string) (blueprint.RawBlueprint, error) + + // SetOverrider sets the InjectorOverrider. + SetOverrider(overrider InjectorOverrider) } // DefaultBlueprintLoader is the default implementation of the BlueprintLoader @@ -142,6 +145,11 @@ func (b *DefaultBlueprintLoader) Load(projectPath, gitRootPath string) (blueprin return blueprint.NewRawBlueprint(finalBlueprint), nil } +// SetOverrider sets the InjectorOverrider. +func (b *DefaultBlueprintLoader) SetOverrider(overrider InjectorOverrider) { + b.overrider = overrider +} + // NewDefaultBlueprintLoader creates a new DefaultBlueprintLoader. func NewDefaultBlueprintLoader(overrider InjectorOverrider, logger *slog.Logger) DefaultBlueprintLoader { if logger == nil { diff --git a/blueprint/pkg/loader/mocks/loader.go b/blueprint/pkg/loader/mocks/loader.go index a9b5ebed..a0801105 100644 --- a/blueprint/pkg/loader/mocks/loader.go +++ b/blueprint/pkg/loader/mocks/loader.go @@ -22,6 +22,9 @@ var _ loader.BlueprintLoader = &BlueprintLoaderMock{} // LoadFunc: func(projectPath string, gitRootPath string) (blueprint.RawBlueprint, error) { // panic("mock out the Load method") // }, +// SetOverriderFunc: func(overrider loader.InjectorOverrider) { +// panic("mock out the SetOverrider method") +// }, // } // // // use mockedBlueprintLoader in code that requires loader.BlueprintLoader @@ -32,6 +35,9 @@ type BlueprintLoaderMock struct { // LoadFunc mocks the Load method. LoadFunc func(projectPath string, gitRootPath string) (blueprint.RawBlueprint, error) + // SetOverriderFunc mocks the SetOverrider method. + SetOverriderFunc func(overrider loader.InjectorOverrider) + // calls tracks calls to the methods. calls struct { // Load holds details about calls to the Load method. @@ -41,8 +47,14 @@ type BlueprintLoaderMock struct { // GitRootPath is the gitRootPath argument value. GitRootPath string } + // SetOverrider holds details about calls to the SetOverrider method. + SetOverrider []struct { + // Overrider is the overrider argument value. + Overrider loader.InjectorOverrider + } } - lockLoad sync.RWMutex + lockLoad sync.RWMutex + lockSetOverrider sync.RWMutex } // Load calls LoadFunc. @@ -80,3 +92,35 @@ func (mock *BlueprintLoaderMock) LoadCalls() []struct { mock.lockLoad.RUnlock() return calls } + +// SetOverrider calls SetOverriderFunc. +func (mock *BlueprintLoaderMock) SetOverrider(overrider loader.InjectorOverrider) { + if mock.SetOverriderFunc == nil { + panic("BlueprintLoaderMock.SetOverriderFunc: method is nil but BlueprintLoader.SetOverrider was just called") + } + callInfo := struct { + Overrider loader.InjectorOverrider + }{ + Overrider: overrider, + } + mock.lockSetOverrider.Lock() + mock.calls.SetOverrider = append(mock.calls.SetOverrider, callInfo) + mock.lockSetOverrider.Unlock() + mock.SetOverriderFunc(overrider) +} + +// SetOverriderCalls gets all the calls that were made to SetOverrider. +// Check the length with: +// +// len(mockedBlueprintLoader.SetOverriderCalls()) +func (mock *BlueprintLoaderMock) SetOverriderCalls() []struct { + Overrider loader.InjectorOverrider +} { + var calls []struct { + Overrider loader.InjectorOverrider + } + mock.lockSetOverrider.RLock() + calls = mock.calls.SetOverrider + mock.lockSetOverrider.RUnlock() + return calls +} diff --git a/forge/cli/Earthfile b/forge/cli/Earthfile index c6e2f6cd..aa82de38 100644 --- a/forge/cli/Earthfile +++ b/forge/cli/Earthfile @@ -53,7 +53,7 @@ test: release: FROM scratch - ARG version="0.0.0" + ARG version="dev" ARG TARGETOS ARG TARGETARCH @@ -74,7 +74,7 @@ publish: ARG container="forge" ARG tag="latest" - ARG version="0.0.2" + ARG version="dev" ARG TARGETOS ARG TARGETARCH diff --git a/forge/cli/blueprint.cue b/forge/cli/blueprint.cue index 79b659e8..84e1f892 100644 --- a/forge/cli/blueprint.cue +++ b/forge/cli/blueprint.cue @@ -2,15 +2,25 @@ version: "1.0" project: { name: "forge" ci: targets: { - publish: platforms: [ - "linux/amd64", - "linux/arm64", - ] - release: platforms: [ - "linux/amd64", - "linux/arm64", - "darwin/amd64", - "darwin/arm64", - ] + publish: { + args: { + version: string | *"dev" @env(name="GIT_TAG",type="string") + } + platforms: [ + "linux/amd64", + "linux/arm64", + ] + } + release: { + args: { + version: string | *"dev" @env(name="GIT_TAG",type="string") + } + platforms: [ + "linux/amd64", + "linux/arm64", + "darwin/amd64", + "darwin/arm64", + ] + } } } diff --git a/forge/cli/cmd/cmds/scan.go b/forge/cli/cmd/cmds/scan.go index db39e4b9..406f074f 100644 --- a/forge/cli/cmd/cmds/scan.go +++ b/forge/cli/cmd/cmds/scan.go @@ -26,7 +26,7 @@ type ScanCmd struct { func (c *ScanCmd) Run(logger *slog.Logger) error { walker := walker.NewDefaultFSWalker(logger) - loader := project.NewDefaultProjectLoader(logger) + loader := project.NewDefaultProjectLoader(loadRuntimes(logger), logger) var rootPath string if c.Absolute { diff --git a/forge/cli/cmd/cmds/tag.go b/forge/cli/cmd/cmds/tag.go index 31eb07c4..0a96df44 100644 --- a/forge/cli/cmd/cmds/tag.go +++ b/forge/cli/cmd/cmds/tag.go @@ -4,7 +4,7 @@ import ( "fmt" "log/slog" - "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/tag" + p "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/project" ) type TagCmd struct { @@ -26,7 +26,7 @@ func (c *TagCmd) Run(logger *slog.Logger) error { } var output TagOutput - tagger := tag.NewTagger(&project, c.CI, c.Trim, logger) + tagger := p.NewTagger(&project, c.CI, c.Trim, logger) if project.Blueprint.Global.CI.Tagging.Strategy != "" { tag, err := tagger.GenerateTag() diff --git a/forge/cli/cmd/cmds/util.go b/forge/cli/cmd/cmds/util.go index 84b54004..60674475 100644 --- a/forge/cli/cmd/cmds/util.go +++ b/forge/cli/cmd/cmds/util.go @@ -86,10 +86,17 @@ func generateOpts(target string, flags *RunCmd, config *schema.Blueprint) []eart // loadProject loads the project from the given root path. func loadProject(rootPath string, logger *slog.Logger) (project.Project, error) { - loader := project.NewDefaultProjectLoader(logger) + loader := project.NewDefaultProjectLoader(loadRuntimes(logger), logger) return loader.Load(rootPath) } +// loadRuntimes loads the all runtime data collectors. +func loadRuntimes(logger *slog.Logger) []project.RuntimeData { + return []project.RuntimeData{ + project.NewGitRuntime(logger), + } +} + // printJson prints the given data as a JSON string. func printJson(data interface{}, pretty bool) { var out []byte diff --git a/forge/cli/pkg/earthly/earthly.go b/forge/cli/pkg/earthly/earthly.go index beb2561b..00fba67e 100644 --- a/forge/cli/pkg/earthly/earthly.go +++ b/forge/cli/pkg/earthly/earthly.go @@ -89,6 +89,7 @@ func (e EarthlyExecutor) Run() (map[string]EarthlyExecutionResult, error) { for i := 0; i < e.opts.retries+1; i++ { arguments := e.buildArguments(platform) + os.Setenv("GIT_TAG", "v0.0.0") e.logger.Info("Executing Earthly", "attempt", i, "retries", e.opts.retries, "arguments", arguments, "platform", platform) output, err = e.executor.Execute("earthly", arguments) if err == nil { diff --git a/forge/cli/pkg/project/loader.go b/forge/cli/pkg/project/loader.go index 06b384d1..b8cc8e98 100644 --- a/forge/cli/pkg/project/loader.go +++ b/forge/cli/pkg/project/loader.go @@ -5,8 +5,11 @@ import ( "fmt" "io" "log/slog" + "os" "path/filepath" + "cuelang.org/go/cue" + "github.com/input-output-hk/catalyst-forge/blueprint/pkg/blueprint" "github.com/input-output-hk/catalyst-forge/blueprint/pkg/loader" "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/earthfile" "github.com/input-output-hk/catalyst-forge/tools/pkg/git" @@ -28,10 +31,11 @@ type DefaultProjectLoader struct { fs afero.Fs logger *slog.Logger repoLoader git.RepoLoader + runtimes []RuntimeData } func (p *DefaultProjectLoader) Load(projectPath string) (Project, error) { - p.logger.Info("Finding git root", "at", projectPath) + p.logger.Info("Finding git root", "projectPath", projectPath) w := walker.NewCustomReverseFSWalker(p.fs, p.logger) gitRoot, err := git.FindGitRoot(projectPath, &w) if err != nil { @@ -39,6 +43,7 @@ func (p *DefaultProjectLoader) Load(projectPath string) (Project, error) { return Project{}, fmt.Errorf("failed to find git root: %w", err) } + p.logger.Info("Loading repository", "path", gitRoot) rl := git.NewCustomDefaultRepoLoader(p.fs) repo, err := rl.Load(gitRoot) if err != nil { @@ -46,19 +51,6 @@ func (p *DefaultProjectLoader) Load(projectPath string) (Project, error) { return Project{}, fmt.Errorf("failed to load repository: %w", err) } - p.logger.Info("Loading blueprint", "path", projectPath) - rbp, err := p.blueprintLoader.Load(projectPath, gitRoot) - if err != nil { - p.logger.Error("Failed to load blueprint", "error", err, "path", projectPath) - return Project{}, fmt.Errorf("failed to load blueprint: %w", err) - } - - bp, err := rbp.Decode() - if err != nil { - p.logger.Error("Failed to decode blueprint", "error", err) - return Project{}, fmt.Errorf("failed to decode blueprint: %w", err) - } - efPath := filepath.Join(projectPath, "Earthfile") exists, err := afero.Exists(p.fs, efPath) if err != nil { @@ -83,6 +75,43 @@ func (p *DefaultProjectLoader) Load(projectPath string) (Project, error) { ef = &efs } + p.logger.Info("Setting blueprint runtime data") + p.blueprintLoader.SetOverrider(func(value cue.Value) map[string]string { + data := make(map[string]string) + for _, r := range p.runtimes { + partialProject := Project{ + Earthfile: ef, + Repo: repo, + Path: projectPath, + RepoRoot: gitRoot, + rawBlueprint: blueprint.NewRawBlueprint(value), + } + + for k, v := range r.Load(&partialProject) { + if err := os.Setenv(k, v); err != nil { + p.logger.Error("Failed to set environment variable", "error", err, "key", k, "value", v) + } + + data[k] = v + } + } + + return data + }) + + p.logger.Info("Loading blueprint", "path", projectPath) + rbp, err := p.blueprintLoader.Load(projectPath, gitRoot) + if err != nil { + p.logger.Error("Failed to load blueprint", "error", err, "path", projectPath) + return Project{}, fmt.Errorf("failed to load blueprint: %w", err) + } + + bp, err := rbp.Decode() + if err != nil { + p.logger.Error("Failed to decode blueprint", "error", err) + return Project{}, fmt.Errorf("failed to decode blueprint: %w", err) + } + return Project{ Blueprint: bp, Earthfile: ef, @@ -95,7 +124,7 @@ func (p *DefaultProjectLoader) Load(projectPath string) (Project, error) { } // NewDefaultProjectLoader creates a new DefaultProjectLoader. -func NewDefaultProjectLoader(logger *slog.Logger) DefaultProjectLoader { +func NewDefaultProjectLoader(runtimes []RuntimeData, logger *slog.Logger) DefaultProjectLoader { if logger == nil { logger = slog.New(slog.NewTextHandler(io.Discard, nil)) } @@ -107,6 +136,7 @@ func NewDefaultProjectLoader(logger *slog.Logger) DefaultProjectLoader { fs: afero.NewOsFs(), logger: logger, repoLoader: &rl, + runtimes: runtimes, } } @@ -115,6 +145,7 @@ func NewCustomProjectLoader( fs afero.Fs, bl loader.BlueprintLoader, rl git.RepoLoader, + runtimes []RuntimeData, logger *slog.Logger, ) DefaultProjectLoader { if logger == nil { @@ -126,5 +157,6 @@ func NewCustomProjectLoader( fs: fs, logger: logger, repoLoader: rl, + runtimes: runtimes, } } diff --git a/forge/cli/pkg/project/loader_test.go b/forge/cli/pkg/project/loader_test.go index c95bb3f8..ab61185d 100644 --- a/forge/cli/pkg/project/loader_test.go +++ b/forge/cli/pkg/project/loader_test.go @@ -10,6 +10,7 @@ import ( "github.com/go-git/go-git/v5/plumbing/cache" "github.com/go-git/go-git/v5/storage/filesystem" "github.com/input-output-hk/catalyst-forge/blueprint/pkg/blueprint" + "github.com/input-output-hk/catalyst-forge/blueprint/pkg/loader" "github.com/input-output-hk/catalyst-forge/blueprint/pkg/loader/mocks" "github.com/input-output-hk/catalyst-forge/tools/pkg/testutils" "github.com/spf13/afero" @@ -148,6 +149,7 @@ bar: } return tt.blueprint, nil }, + SetOverriderFunc: func(overrider loader.InjectorOverrider) {}, } loader := DefaultProjectLoader{ diff --git a/forge/cli/pkg/project/project.go b/forge/cli/pkg/project/project.go index 04afa9ff..a627129d 100644 --- a/forge/cli/pkg/project/project.go +++ b/forge/cli/pkg/project/project.go @@ -11,6 +11,11 @@ import ( "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/earthfile" ) +type TagInfo struct { + Generated string `json:"generated"` + Git string `json:"git"` +} + // Project represents a project type Project struct { Blueprint schema.Blueprint @@ -19,6 +24,7 @@ type Project struct { Path string Repo *gg.Repository RepoRoot string + Tags TagInfo rawBlueprint blueprint.RawBlueprint } diff --git a/forge/cli/pkg/project/runtime.go b/forge/cli/pkg/project/runtime.go new file mode 100644 index 00000000..37dd963e --- /dev/null +++ b/forge/cli/pkg/project/runtime.go @@ -0,0 +1,76 @@ +package project + +import ( + "log/slog" + + "github.com/input-output-hk/catalyst-forge/blueprint/schema" + "github.com/input-output-hk/catalyst-forge/tools/pkg/git" +) + +// RuntimeData is an interface for runtime data loaders. +type RuntimeData interface { + Load(project *Project) map[string]string +} + +// GitRuntime is a runtime data loader for git related data. +type GitRuntime struct { + logger *slog.Logger +} + +func (g *GitRuntime) Load(project *Project) map[string]string { + g.logger.Debug("Loading git runtime data") + + var err error + var strategy string + var aliases map[string]string + + if project.Raw().Get("global.ci.tagging.strategy").Exists() { + strategy, err = project.Raw().Get("global.ci.tagging.strategy").String() + if err != nil { + g.logger.Error("Failed to get tag strategy", "error", err) + } + } + + if project.Raw().Get("global.ci.tagging.aliases").Exists() { + err = project.Raw().DecodePath("global.ci.tagging.aliases", &aliases) + if err != nil { + g.logger.Error("Failed to get tag aliases", "error", err) + } + } + + project.Blueprint = schema.Blueprint{ + Global: schema.Global{ + CI: schema.GlobalCI{ + Tagging: schema.Tagging{ + Aliases: aliases, + Strategy: schema.TagStrategy(strategy), + }, + }, + }, + } + + data := make(map[string]string) + tagger := NewTagger(project, git.InCI(), true, g.logger) + + generated, err := tagger.GenerateTag() + if err != nil { + g.logger.Error("Failed to get git tag", "error", err) + } else if generated != "" { + data["GIT_TAG_GENERATED"] = generated + } + + gitTag, err := tagger.GetGitTag() + if err != nil { + g.logger.Error("Failed to get git tag", "error", err) + } else if gitTag != "" { + data["GIT_TAG"] = gitTag + } + + return data +} + +func NewGitRuntime(logger *slog.Logger) *GitRuntime { + return &GitRuntime{ + logger: logger, + } +} diff --git a/forge/cli/pkg/tag/strategies.go b/forge/cli/pkg/project/tag/strategies.go similarity index 57% rename from forge/cli/pkg/tag/strategies.go rename to forge/cli/pkg/project/tag/strategies.go index 5788c3f7..016ba662 100644 --- a/forge/cli/pkg/tag/strategies.go +++ b/forge/cli/pkg/project/tag/strategies.go @@ -3,17 +3,17 @@ package tag import ( "fmt" - "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/project" + "github.com/go-git/go-git/v5" ) // GitCommit returns the commit hash of the HEAD commit. -func GitCommit(project *project.Project) (string, error) { - ref, err := project.Repo.Head() +func GitCommit(repo *git.Repository) (string, error) { + ref, err := repo.Head() if err != nil { return "", fmt.Errorf("failed to get HEAD: %w", err) } - obj, err := project.Repo.CommitObject(ref.Hash()) + obj, err := repo.CommitObject(ref.Hash()) if err != nil { return "", fmt.Errorf("failed to get commit object: %w", err) } diff --git a/forge/cli/pkg/tag/strategies_test.go b/forge/cli/pkg/project/tag/strategies_test.go similarity index 70% rename from forge/cli/pkg/tag/strategies_test.go rename to forge/cli/pkg/project/tag/strategies_test.go index a9803773..baae1a24 100644 --- a/forge/cli/pkg/tag/strategies_test.go +++ b/forge/cli/pkg/project/tag/strategies_test.go @@ -3,7 +3,6 @@ package tag import ( "testing" - "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/project" "github.com/input-output-hk/catalyst-forge/tools/pkg/testutils" "github.com/stretchr/testify/assert" ) @@ -12,11 +11,8 @@ func TestGitCommit(t *testing.T) { repo := testutils.NewInMemRepo(t) repo.AddFile(t, "example.txt", "example content") commit := repo.Commit(t, "Initial commit") - project := project.Project{ - Repo: repo.Repo, - } - commitHash, err := GitCommit(&project) + commitHash, err := GitCommit(repo.Repo) assert.NoError(t, err) assert.Equal(t, commit.String(), commitHash) } diff --git a/forge/cli/pkg/tag/tagger.go b/forge/cli/pkg/project/tagger.go similarity index 95% rename from forge/cli/pkg/tag/tagger.go rename to forge/cli/pkg/project/tagger.go index 1946c501..a8a63422 100644 --- a/forge/cli/pkg/tag/tagger.go +++ b/forge/cli/pkg/project/tagger.go @@ -1,4 +1,4 @@ -package tag +package project import ( "fmt" @@ -9,7 +9,7 @@ import ( gg "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/input-output-hk/catalyst-forge/blueprint/schema" - "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/project" + "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/project/tag" ) // MonoTag represents a monorepo tag. @@ -22,16 +22,18 @@ type MonoTag struct { type Tagger struct { ci bool logger *slog.Logger - project *project.Project + project *Project trim bool } // GenerateTag generates a tag for the project based on the tagging strategy. func (t *Tagger) GenerateTag() (string, error) { strategy := t.project.Blueprint.Global.CI.Tagging.Strategy + + t.logger.Info("Generating tag", "strategy", strategy) switch strategy { case schema.TagStrategyGitCommit: - tag, err := GitCommit(t.project) + tag, err := tag.GitCommit(t.project.Repo) if err != nil { return "", err } @@ -116,7 +118,7 @@ func (t *Tagger) GetGitTag() (string, error) { } // NewTagger creates a new tagger for the given project. -func NewTagger(p *project.Project, ci bool, trim bool, logger *slog.Logger) *Tagger { +func NewTagger(p *Project, ci bool, trim bool, logger *slog.Logger) *Tagger { return &Tagger{ ci: ci, logger: logger, diff --git a/forge/cli/pkg/tag/tagger_test.go b/forge/cli/pkg/project/tagger_test.go similarity index 96% rename from forge/cli/pkg/tag/tagger_test.go rename to forge/cli/pkg/project/tagger_test.go index 4ba915f6..bc9bec4e 100644 --- a/forge/cli/pkg/tag/tagger_test.go +++ b/forge/cli/pkg/project/tagger_test.go @@ -1,11 +1,10 @@ -package tag +package project import ( "os" "testing" "github.com/input-output-hk/catalyst-forge/blueprint/schema" - "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/project" "github.com/input-output-hk/catalyst-forge/tools/pkg/testutils" "github.com/stretchr/testify/assert" ) @@ -127,7 +126,7 @@ func TestTaggerGetGitTag(t *testing.T) { }, } - project := project.Project{ + project := Project{ Blueprint: bp, Earthfile: nil, Name: "test", diff --git a/tools/pkg/git/util.go b/tools/pkg/git/util.go index 85447b0e..6ce5273d 100644 --- a/tools/pkg/git/util.go +++ b/tools/pkg/git/util.go @@ -3,6 +3,7 @@ package git import ( "errors" "io" + "os" "path/filepath" "github.com/input-output-hk/catalyst-forge/tools/pkg/walker" @@ -42,3 +43,12 @@ func FindGitRoot(startPath string, rw walker.ReverseWalker) (string, error) { return gitRoot, nil } + +// InCI returns true if the code is running in a CI environment. +func InCI() bool { + if _, ok := os.LookupEnv("GITHUB_ACTIONS"); ok { + return true + } + + return false +}