diff --git a/blueprint.cue b/blueprint.cue index 9131e8ef..2a8dcb66 100644 --- a/blueprint.cue +++ b/blueprint.cue @@ -74,6 +74,7 @@ global: { ] } deployment: { + foundry: api: "https://foundry.projectcatalyst.io" registries: { containers: "ghcr.io/input-output-hk/catalyst-forge" modules: ci.providers.aws.ecr.registry + "/catalyst-deployments" @@ -87,5 +88,6 @@ global: { repo: { defaultBranch: "master" name: "input-output-hk/catalyst-forge" + url: "https://github.com/input-output-hk/catalyst-forge" } } diff --git a/cli/Earthfile b/cli/Earthfile index 605a25fe..fb1abbd8 100644 --- a/cli/Earthfile +++ b/cli/Earthfile @@ -12,6 +12,7 @@ deps: ENV GOMODCACHE=/go/modcache CACHE --persist --sharing shared /go + COPY ../foundry/api+src/src /foundry/api COPY ../lib/project+src/src /lib/project COPY ../lib/schema+src/src /lib/schema COPY ../lib/tools+src/src /lib/tools diff --git a/cli/cmd/cmds/deploy/cmd.go b/cli/cmd/cmds/deploy/cmd.go new file mode 100644 index 00000000..31fe9f40 --- /dev/null +++ b/cli/cmd/cmds/deploy/cmd.go @@ -0,0 +1,6 @@ +package deploy + +type DeployCmd struct { + Create DeployCreateCmd `cmd:"create" help:"Create a new deployment."` + Get DeployGetCmd `cmd:"get" help:"Get a deployment."` +} diff --git a/cli/cmd/cmds/deploy/create.go b/cli/cmd/cmds/deploy/create.go new file mode 100644 index 00000000..c4eb90cf --- /dev/null +++ b/cli/cmd/cmds/deploy/create.go @@ -0,0 +1,35 @@ +package deploy + +import ( + "context" + "fmt" + "time" + + "github.com/input-output-hk/catalyst-forge/cli/pkg/run" + "github.com/input-output-hk/catalyst-forge/cli/pkg/utils" + api "github.com/input-output-hk/catalyst-forge/foundry/api/client" +) + +type DeployCreateCmd struct { + ReleaseID string `arg:"" help:"The release ID to deploy."` + Url string `short:"u" help:"The URL to the Foundry API server (overrides global config)."` +} + +func (c *DeployCreateCmd) Run(ctx run.RunContext) error { + url, err := utils.GetFoundryURL(ctx, c.Url) + if err != nil { + return err + } + + client := api.NewClient(url, api.WithTimeout(10*time.Second)) + deployment, err := client.CreateDeployment(context.Background(), c.ReleaseID) + if err != nil { + return fmt.Errorf("could not create deployment: %w", err) + } + + if err := utils.PrintJson(deployment, true); err != nil { + return err + } + + return nil +} diff --git a/cli/cmd/cmds/deploy/get.go b/cli/cmd/cmds/deploy/get.go new file mode 100644 index 00000000..565cacaa --- /dev/null +++ b/cli/cmd/cmds/deploy/get.go @@ -0,0 +1,36 @@ +package deploy + +import ( + "context" + "fmt" + "time" + + "github.com/input-output-hk/catalyst-forge/cli/pkg/run" + "github.com/input-output-hk/catalyst-forge/cli/pkg/utils" + api "github.com/input-output-hk/catalyst-forge/foundry/api/client" +) + +type DeployGetCmd struct { + ReleaseID string `arg:"" help:"The release ID."` + DeployID string `arg:"" help:"The deployment ID."` + Url string `short:"u" help:"The URL to the Foundry API server (overrides global config)."` +} + +func (c *DeployGetCmd) Run(ctx run.RunContext) error { + url, err := utils.GetFoundryURL(ctx, c.Url) + if err != nil { + return err + } + + client := api.NewClient(url, api.WithTimeout(10*time.Second)) + deployment, err := client.GetDeployment(context.Background(), c.ReleaseID, c.DeployID) + if err != nil { + return fmt.Errorf("could not show deployment: %w", err) + } + + if err := utils.PrintJson(deployment, true); err != nil { + return err + } + + return nil +} diff --git a/cli/cmd/cmds/release/cmd.go b/cli/cmd/cmds/release/cmd.go new file mode 100644 index 00000000..b5d5e9d3 --- /dev/null +++ b/cli/cmd/cmds/release/cmd.go @@ -0,0 +1,7 @@ +package release + +type ReleaseCmd struct { + Create ReleaseCreateCmd `cmd:"create" help:"Create a new release for a project."` + List ReleaseListCmd `cmd:"list" help:"List all releases for a project."` + Get ReleaseGetCmd `cmd:"get" help:"Get a release."` +} diff --git a/cli/cmd/cmds/release/create.go b/cli/cmd/cmds/release/create.go new file mode 100644 index 00000000..ae4902b9 --- /dev/null +++ b/cli/cmd/cmds/release/create.go @@ -0,0 +1,111 @@ +package release + +import ( + "context" + "errors" + "fmt" + "log/slog" + "time" + + "github.com/input-output-hk/catalyst-forge/cli/pkg/run" + "github.com/input-output-hk/catalyst-forge/cli/pkg/utils" + api "github.com/input-output-hk/catalyst-forge/foundry/api/client" + "github.com/input-output-hk/catalyst-forge/lib/project/project" + "github.com/input-output-hk/catalyst-forge/lib/tools/fs" + "github.com/input-output-hk/catalyst-forge/lib/tools/git/github" +) + +type ReleaseCreateCmd struct { + Deploy bool `short:"d" help:"Automatically create a new deployment for the release."` + Project string `arg:"" help:"The path to the project to create a new release for." kong:"arg,predictor=path"` + Url string `short:"u" help:"The URL to the Foundry API server (overrides global config)."` +} + +func (c *ReleaseCreateCmd) Run(ctx run.RunContext) error { + exists, err := fs.Exists(c.Project) + if err != nil { + return fmt.Errorf("could not check if project exists: %w", err) + } else if !exists { + return fmt.Errorf("project does not exist: %s", c.Project) + } + + project, err := ctx.ProjectLoader.Load(c.Project) + if err != nil { + return fmt.Errorf("could not load project: %w", err) + } + + commit, err := getCommitHash(project, ctx.Logger) + if err != nil { + return fmt.Errorf("could not get commit hash: %w", err) + } + + branch, err := getBranch(project, ctx.Logger) + if err != nil { + return fmt.Errorf("could not get branch: %w", err) + } + + // Only set the branch if it is not the default branch + if branch == project.Blueprint.Global.Repo.DefaultBranch { + branch = "" + } + + path, err := project.GetRelativePath() + if err != nil { + return fmt.Errorf("could not get project path: %w", err) + } + + var url string + if c.Url == "" { + if project.Blueprint.Global == nil || + project.Blueprint.Global.Deployment == nil || + project.Blueprint.Global.Deployment.Foundry.Api == "" { + return errors.New("no foundry URL provided and no URL found in the root blueprint") + } + + url = project.Blueprint.Global.Deployment.Foundry.Api + } else { + url = c.Url + } + + client := api.NewClient(url, api.WithTimeout(10*time.Second)) + release, err := client.CreateRelease(context.Background(), &api.Release{ + SourceRepo: project.Blueprint.Global.Repo.Url, + SourceCommit: commit, + SourceBranch: branch, + Project: project.Name, + ProjectPath: path, + Bundle: "something", + }, c.Deploy) + if err != nil { + return fmt.Errorf("could not create release: %w", err) + } + + if err := utils.PrintJson(release, true); err != nil { + return err + } + + return nil +} + +func getCommitHash(project project.Project, logger *slog.Logger) (string, error) { + if github.InGithubActions() { + ghr := github.NewDefaultGithubRepo(logger) + return ghr.GetCommit() + } + + obj, err := project.Repo.HeadCommit() + if err != nil { + return "", err + } + + return obj.Hash.String(), nil +} + +func getBranch(project project.Project, logger *slog.Logger) (string, error) { + if github.InGithubActions() { + ghr := github.NewDefaultGithubRepo(logger) + return ghr.GetBranch(), nil + } + + return project.Repo.GetCurrentBranch() +} diff --git a/cli/cmd/cmds/release/get.go b/cli/cmd/cmds/release/get.go new file mode 100644 index 00000000..fd877cd0 --- /dev/null +++ b/cli/cmd/cmds/release/get.go @@ -0,0 +1,35 @@ +package release + +import ( + "context" + "fmt" + "time" + + "github.com/input-output-hk/catalyst-forge/cli/pkg/run" + "github.com/input-output-hk/catalyst-forge/cli/pkg/utils" + api "github.com/input-output-hk/catalyst-forge/foundry/api/client" +) + +type ReleaseGetCmd struct { + ReleaseID string `arg:"" help:"The ID of the release."` + Url string `short:"u" help:"The URL to the Foundry API server (overrides global config)."` +} + +func (c *ReleaseGetCmd) Run(ctx run.RunContext) error { + url, err := utils.GetFoundryURL(ctx, c.Url) + if err != nil { + return err + } + + client := api.NewClient(url, api.WithTimeout(10*time.Second)) + release, err := client.GetRelease(context.Background(), c.ReleaseID) + if err != nil { + return fmt.Errorf("could not show release: %w", err) + } + + if err := utils.PrintJson(release, true); err != nil { + return err + } + + return nil +} diff --git a/cli/cmd/cmds/release/list.go b/cli/cmd/cmds/release/list.go new file mode 100644 index 00000000..3d8b270e --- /dev/null +++ b/cli/cmd/cmds/release/list.go @@ -0,0 +1,35 @@ +package release + +import ( + "context" + "fmt" + "time" + + "github.com/input-output-hk/catalyst-forge/cli/pkg/run" + "github.com/input-output-hk/catalyst-forge/cli/pkg/utils" + api "github.com/input-output-hk/catalyst-forge/foundry/api/client" +) + +type ReleaseListCmd struct { + Project string `arg:"" help:"The project to list releases for."` + Url string `short:"u" help:"The URL to the Foundry API server (overrides global config)."` +} + +func (c *ReleaseListCmd) Run(ctx run.RunContext) error { + url, err := utils.GetFoundryURL(ctx, c.Url) + if err != nil { + return err + } + + client := api.NewClient(url, api.WithTimeout(10*time.Second)) + releases, err := client.ListReleases(context.Background(), c.Project) + if err != nil { + return fmt.Errorf("could not list releases: %w", err) + } + + if err := utils.PrintJson(releases, true); err != nil { + return err + } + + return nil +} diff --git a/cli/cmd/cmds/scan.go b/cli/cmd/cmds/scan.go index 8fd5a9c8..0aafac87 100644 --- a/cli/cmd/cmds/scan.go +++ b/cli/cmd/cmds/scan.go @@ -42,7 +42,7 @@ func (c *ScanCmd) Run(ctx run.RunContext) error { return fmt.Errorf("root path does not exist: %s", rootPath) } - projects, err := scan.ScanProjects(rootPath, ctx.ProjectLoader, &ctx.FSWalker, ctx.Logger) + projects, err := scan.ScanProjects(rootPath, ctx.ProjectLoader, ctx.Walker, ctx.Logger) if err != nil { return err } diff --git a/cli/cmd/main.go b/cli/cmd/main.go index 55540a29..fb218f69 100644 --- a/cli/cmd/main.go +++ b/cli/cmd/main.go @@ -10,8 +10,11 @@ import ( "github.com/alecthomas/kong" "github.com/charmbracelet/log" "github.com/input-output-hk/catalyst-forge/cli/cmd/cmds" + "github.com/input-output-hk/catalyst-forge/cli/cmd/cmds/deploy" "github.com/input-output-hk/catalyst-forge/cli/cmd/cmds/module" + "github.com/input-output-hk/catalyst-forge/cli/cmd/cmds/release" "github.com/input-output-hk/catalyst-forge/cli/pkg/run" + "github.com/input-output-hk/catalyst-forge/lib/project/blueprint" "github.com/input-output-hk/catalyst-forge/lib/project/deployment" "github.com/input-output-hk/catalyst-forge/lib/project/project" "github.com/input-output-hk/catalyst-forge/lib/project/secrets" @@ -32,15 +35,17 @@ type GlobalArgs struct { var cli struct { GlobalArgs - Dump cmds.DumpCmd `cmd:"" help:"Dumps a project's blueprint to JSON."` - CI cmds.CICmd `cmd:"" help:"Simulate a CI run."` - Mod module.ModuleCmd `kong:"cmd" help:"Commands for working with deployment modules."` - Release cmds.ReleaseCmd `cmd:"" help:"Release a project."` - Run cmds.RunCmd `cmd:"" help:"Run an Earthly target."` - Scan cmds.ScanCmd `cmd:"" help:"Scan for Earthfiles."` - Secret cmds.SecretCmd `cmd:"" help:"Manage secrets."` - Validate cmds.ValidateCmd `cmd:"" help:"Validates a project."` - Version VersionCmd `cmd:"" help:"Print the version."` + Deployments deploy.DeployCmd `cmd:"" help:"Commands for working with deployments."` + Dump cmds.DumpCmd `cmd:"" help:"Dumps a project's blueprint to JSON."` + CI cmds.CICmd `cmd:"" help:"Simulate a CI run."` + Mod module.ModuleCmd `kong:"cmd" help:"Commands for working with deployment modules."` + Release cmds.ReleaseCmd `cmd:"" help:"Release a project."` + Releases release.ReleaseCmd `cmd:"" help:"Commands for dealing with Foundry releases."` + Run cmds.RunCmd `cmd:"" help:"Run an Earthly target."` + Scan cmds.ScanCmd `cmd:"" help:"Scan for Earthfiles."` + Secret cmds.SecretCmd `cmd:"" help:"Manage secrets."` + Validate cmds.ValidateCmd `cmd:"" help:"Validates a project."` + Version VersionCmd `cmd:"" help:"Print the version."` InstallCompletions kongplete.InstallCompletions `cmd:"" help:"install shell completions"` } @@ -92,15 +97,20 @@ func Run() int { logger := slog.New(handler) store := secrets.NewDefaultSecretStore() cc := cuecontext.New() - loader := project.NewDefaultProjectLoader(cc, store, logger) + w := walker.NewDefaultFSWalker(logger) + rw := walker.NewDefaultFSReverseWalker(logger) + bl := blueprint.NewDefaultBlueprintLoader(cc, logger) + pl := project.NewDefaultProjectLoader(cc, store, logger) runctx := run.RunContext{ + BlueprintLoader: &bl, CI: cli.GlobalArgs.CI, CueCtx: cc, - FSWalker: walker.NewDefaultFSWalker(logger), + ReverseWalker: &rw, + Walker: &w, Local: cli.GlobalArgs.Local, Logger: logger, ManifestGeneratorStore: deployment.NewDefaultManifestGeneratorStore(), - ProjectLoader: &loader, + ProjectLoader: &pl, SecretStore: store, Verbose: cli.GlobalArgs.Verbose, } diff --git a/cli/go.mod b/cli/go.mod index bb4bcf55..e1214cfa 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -4,7 +4,7 @@ go 1.23.0 require ( cuelang.org/go v0.12.0 - github.com/alecthomas/kong v0.9.0 + github.com/alecthomas/kong v1.2.1 github.com/aws/aws-sdk-go v1.55.5 github.com/aws/aws-sdk-go-v2 v1.32.6 github.com/aws/aws-sdk-go-v2/config v1.27.40 @@ -14,12 +14,13 @@ require ( github.com/charmbracelet/lipgloss v0.13.0 github.com/charmbracelet/log v0.4.0 github.com/google/go-github/v66 v66.0.0 + github.com/input-output-hk/catalyst-forge/foundry/api v0.0.0 github.com/input-output-hk/catalyst-forge/lib/project v0.0.0 github.com/input-output-hk/catalyst-forge/lib/schema v0.0.0 github.com/input-output-hk/catalyst-forge/lib/tools v0.0.0 github.com/migueleliasweb/go-github-mock v1.0.1 github.com/posener/complete v1.2.3 - github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a + github.com/rogpeppe/go-internal v1.14.1 github.com/stretchr/testify v1.10.0 github.com/willabides/kongplete v0.4.0 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c @@ -244,12 +245,12 @@ require ( gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect helm.sh/helm/v3 v3.17.0 // indirect - k8s.io/api v0.32.2 // indirect + k8s.io/api v0.32.3 // indirect k8s.io/apiextensions-apiserver v0.32.2 // indirect - k8s.io/apimachinery v0.32.2 // indirect + k8s.io/apimachinery v0.32.3 // indirect k8s.io/apiserver v0.32.2 // indirect k8s.io/cli-runtime v0.32.0 // indirect - k8s.io/client-go v0.32.2 // indirect + k8s.io/client-go v0.32.3 // indirect k8s.io/component-base v0.32.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect @@ -271,3 +272,5 @@ replace github.com/input-output-hk/catalyst-forge/lib/project => ../lib/project replace github.com/input-output-hk/catalyst-forge/lib/schema => ../lib/schema replace github.com/input-output-hk/catalyst-forge/lib/tools => ../lib/tools + +replace github.com/input-output-hk/catalyst-forge/foundry/api => ../foundry/api diff --git a/cli/go.sum b/cli/go.sum index dcfef905..31f12bdd 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -221,10 +221,10 @@ github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0k github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= -github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= -github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA= -github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os= +github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY= +github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q= +github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -839,8 +839,8 @@ github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab h1:ZjX6I48eZSFetP github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab/go.mod h1:/PfPXh0EntGc3QAAyUaviy4S9tzy4Zp0e2ilq4voC6E= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM= -github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rubenv/sql-migrate v1.7.1 h1:f/o0WgfO/GqNuVg+6801K/KW3WdDSupzSjDYODmiUq4= github.com/rubenv/sql-migrate v1.7.1/go.mod h1:Ob2Psprc0/3ggbM6wCzyYVFFuc6FyZrb2AS+ezLDFb4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -1531,18 +1531,18 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw= -k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y= +k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= +k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= k8s.io/apiextensions-apiserver v0.32.2 h1:2YMk285jWMk2188V2AERy5yDwBYrjgWYggscghPCvV4= k8s.io/apiextensions-apiserver v0.32.2/go.mod h1:GPwf8sph7YlJT3H6aKUWtd0E+oyShk/YHWQHf/OOgCA= -k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ= -k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= +k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= k8s.io/apiserver v0.32.2 h1:WzyxAu4mvLkQxwD9hGa4ZfExo3yZZaYzoYvvVDlM6vw= k8s.io/apiserver v0.32.2/go.mod h1:PEwREHiHNU2oFdte7BjzA1ZyjWjuckORLIK/wLV5goM= k8s.io/cli-runtime v0.32.0 h1:dP+OZqs7zHPpGQMCGAhectbHU2SNCuZtIimRKTv2T1c= k8s.io/cli-runtime v0.32.0/go.mod h1:Mai8ht2+esoDRK5hr861KRy6z0zHsSTYttNVJXgP3YQ= -k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA= -k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94= +k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= +k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= k8s.io/component-base v0.32.2 h1:1aUL5Vdmu7qNo4ZsE+569PV5zFatM9hl+lb3dEea2zU= k8s.io/component-base v0.32.2/go.mod h1:PXJ61Vx9Lg+P5mS8TLd7bCIr+eMJRQTyXe8KvkrvJq0= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= diff --git a/cli/pkg/run/context.go b/cli/pkg/run/context.go index c763e2e0..3d3abe94 100644 --- a/cli/pkg/run/context.go +++ b/cli/pkg/run/context.go @@ -4,6 +4,7 @@ import ( "log/slog" "cuelang.org/go/cue" + "github.com/input-output-hk/catalyst-forge/lib/project/blueprint" "github.com/input-output-hk/catalyst-forge/lib/project/deployment" "github.com/input-output-hk/catalyst-forge/lib/project/project" "github.com/input-output-hk/catalyst-forge/lib/project/secrets" @@ -12,15 +13,15 @@ import ( // RunContext represents the context in which a CLI run is happening. type RunContext struct { + // BlueprintLoader is the blueprint loader to use for loading blueprints. + BlueprintLoader blueprint.BlueprintLoader + // CI is true if the run is happening in a CI environment. CI bool // CueCtx is the CUE context to use for CUE operations. CueCtx *cue.Context - // FSWalker is the walker to use for walking the filesystem. - FSWalker walker.FSWalker - // Local is true if the run is happening in a local environment. Local bool @@ -33,9 +34,15 @@ type RunContext struct { // ProjectLoader is the project loader to use for loading projects. ProjectLoader project.ProjectLoader + // ReverseWalker is the reverse walker to use for walking the filesystem. + ReverseWalker walker.ReverseWalker + // SecretStore is the secret store to use for fetching secrets. SecretStore secrets.SecretStore // Verbose is the verbosity level of the run. Verbose int + + // Walker is the walker to use for walking the filesystem. + Walker walker.Walker } diff --git a/cli/pkg/utils/api.go b/cli/pkg/utils/api.go new file mode 100644 index 00000000..c76a2293 --- /dev/null +++ b/cli/pkg/utils/api.go @@ -0,0 +1,25 @@ +package utils + +import ( + "errors" + + "github.com/input-output-hk/catalyst-forge/cli/pkg/run" +) + +// GetFoundryURL retrieves the foundry URL from the given url or the root blueprint. +func GetFoundryURL(ctx run.RunContext, url string) (string, error) { + if url == "" { + bp, err := LoadRootBlueprint(ctx) + if err != nil { + return "", errors.New("no foundry URL provided and no root blueprint found") + } + + if bp.Global == nil || bp.Global.Deployment == nil || bp.Global.Deployment.Foundry.Api == "" { + return "", errors.New("no foundry URL provided in the root blueprint") + } + + return bp.Global.Deployment.Foundry.Api, nil + } + + return url, nil +} diff --git a/cli/pkg/utils/bp.go b/cli/pkg/utils/bp.go new file mode 100644 index 00000000..e79be5cd --- /dev/null +++ b/cli/pkg/utils/bp.go @@ -0,0 +1,34 @@ +package utils + +import ( + "errors" + "fmt" + + "github.com/input-output-hk/catalyst-forge/cli/pkg/run" + "github.com/input-output-hk/catalyst-forge/lib/schema/blueprint" + "github.com/input-output-hk/catalyst-forge/lib/tools/git" +) + +// LoadRootBlueprint loads the root blueprint from the local git repository. +func LoadRootBlueprint(ctx run.RunContext) (blueprint.Blueprint, error) { + gitRoot, err := git.FindGitRoot(".", ctx.ReverseWalker) + if err != nil { + return blueprint.Blueprint{}, errors.New("not in a git repository") + } + + rbp, err := ctx.BlueprintLoader.Load(gitRoot, gitRoot) + if err != nil { + return blueprint.Blueprint{}, fmt.Errorf("failed to load root blueprint: %w", err) + } + + if err := rbp.Validate(); err != nil { + return blueprint.Blueprint{}, fmt.Errorf("failed to validate root blueprint: %w", err) + } + + bp, err := rbp.Decode() + if err != nil { + return blueprint.Blueprint{}, fmt.Errorf("failed to decode root blueprint: %w", err) + } + + return bp, nil +} diff --git a/cli/pkg/utils/cli.go b/cli/pkg/utils/cli.go index 292ca4e1..4b3c8a6f 100644 --- a/cli/pkg/utils/cli.go +++ b/cli/pkg/utils/cli.go @@ -6,7 +6,7 @@ import ( ) // PrintJson prints the given data as a JSON string. -func PrintJson(data interface{}, pretty bool) { +func PrintJson(data interface{}, pretty bool) error { var out []byte var err error @@ -17,9 +17,9 @@ func PrintJson(data interface{}, pretty bool) { } if err != nil { - fmt.Println(err) - return + return fmt.Errorf("failed to marshal data to JSON: %w", err) } fmt.Println(string(out)) + return nil } diff --git a/foundry/operator/config/rbac/role.yaml b/foundry/operator/config/rbac/role.yaml index 77ac15b5..d3caea97 100644 --- a/foundry/operator/config/rbac/role.yaml +++ b/foundry/operator/config/rbac/role.yaml @@ -7,7 +7,7 @@ rules: - apiGroups: - foundry.projectcatalyst.io resources: - - releases + - releasedeployments verbs: - create - delete @@ -19,13 +19,13 @@ rules: - apiGroups: - foundry.projectcatalyst.io resources: - - releases/finalizers + - releasedeployments/finalizers verbs: - update - apiGroups: - foundry.projectcatalyst.io resources: - - releases/status + - releasedeployments/status verbs: - get - patch diff --git a/foundry/operator/internal/controller/suite_env_test.go b/foundry/operator/internal/controller/suite_env_test.go index 40c28007..7dffce74 100644 --- a/foundry/operator/internal/controller/suite_env_test.go +++ b/foundry/operator/internal/controller/suite_env_test.go @@ -287,6 +287,7 @@ func newRawBlueprint() string { } global: { deployment: { + foundry: api: "http://foundry/" registries: { containers: "registry.com" modules: "registry.com" @@ -297,6 +298,11 @@ func newRawBlueprint() string { } root: "root" } + repo: { + defaultBranch: "master" + name: "org/repo" + url: "https://github.com/org/repo" + } } } ` diff --git a/foundry/test/.justfile b/foundry/test/.justfile index d4ded5fe..81d1ef08 100644 --- a/foundry/test/.justfile +++ b/foundry/test/.justfile @@ -1,4 +1,4 @@ -up: kind-up postgres-up gitea-up api-up operator-up git-up +up: kind-up postgres-up gitea-up api-up operator-up git-up forge up-local: kind-up postgres-up gitea-up operator-up-local api-up git-up @@ -18,6 +18,9 @@ api-up: cleanup-local: rm -rf ~/.cache/forge +forge: + earthly -a ../../cli+build/forge "$(pwd)/bin/forge" + git-up: ./scripts/git.sh diff --git a/foundry/test/repos/source/blueprint.cue.fake b/foundry/test/repos/source/blueprint.cue.fake index 2c989c19..c923827f 100644 --- a/foundry/test/repos/source/blueprint.cue.fake +++ b/foundry/test/repos/source/blueprint.cue.fake @@ -2,6 +2,7 @@ version: "1.0" global: { deployment: { + foundry: api: "http://localhost:3001" registries: { containers: "foo" modules: "bar" @@ -12,5 +13,10 @@ } root: "k8s" } + repo: { + defaultBranch: "master" + name: "root/source" + url: "http://gitea:3000/root/source" + } } -} \ No newline at end of file +} diff --git a/lib/project/deployment/module_test.go b/lib/project/deployment/module_test.go index 7af20373..6aee53c1 100644 --- a/lib/project/deployment/module_test.go +++ b/lib/project/deployment/module_test.go @@ -261,6 +261,7 @@ func makeBlueprint() string { } global: { deployment: { + foundry: api: "https://foundry.com" registries: { containers: "registry.com" modules: "registry.com" @@ -271,6 +272,11 @@ func makeBlueprint() string { } root: "root" } + repo: { + defaultBranch: "master" + name: "org/repo" + url: "https://github.com/org/repo" + } } } ` diff --git a/lib/project/project/loader_test.go b/lib/project/project/loader_test.go index fef3e94a..a0d885c5 100644 --- a/lib/project/project/loader_test.go +++ b/lib/project/project/loader_test.go @@ -30,8 +30,9 @@ bar: version: "1.0" global: { repo: { + defaultBranch: "main" name: "foo" - defaultBranch: "main" + url: "bar" } } project: name: "foo" @@ -100,8 +101,9 @@ project: name: "foo" version: "1.0" global: { repo: { + defaultBranch: "main" name: "foo" - defaultBranch: "main" + url: "bar" } } project: { @@ -132,8 +134,9 @@ project: name: "foo" version: "1.0" global: { repo: { + defaultBranch: "main" name: "foo" - defaultBranch: "main" + url: "bar" } } project: { @@ -226,8 +229,9 @@ project: name: "foo" version: "1.0" global: { repo: { + defaultBranch: "main" name: "foo" - defaultBranch: "main" + url: "bar" } } project: { diff --git a/lib/project/project/runtime.go b/lib/project/project/runtime.go index c91c90c1..1afcc506 100644 --- a/lib/project/project/runtime.go +++ b/lib/project/project/runtime.go @@ -5,7 +5,6 @@ import ( "log/slog" "cuelang.org/go/cue" - "github.com/google/go-github/v66/github" sb "github.com/input-output-hk/catalyst-forge/lib/schema/blueprint" sg "github.com/input-output-hk/catalyst-forge/lib/schema/blueprint/global" "github.com/input-output-hk/catalyst-forge/lib/tools/fs" @@ -68,6 +67,7 @@ func NewDeploymentRuntime(logger *slog.Logger) *DeploymentRuntime { // GitRuntime is a runtime data loader for git related data. type GitRuntime struct { fs fs.Filesystem + gh gh.GithubRepo logger *slog.Logger } @@ -96,71 +96,41 @@ func (g *GitRuntime) Load(project *Project) map[string]cue.Value { // getCommitHash returns the commit hash of the HEAD commit. func (g *GitRuntime) getCommitHash(repo *repo.GitRepo) (string, error) { - env := gh.NewCustomGithubEnv(g.fs, g.logger) - if env.HasEvent() { - if env.GetEventType() == "pull_request" { - g.logger.Debug("Found GitHub pull request event") - event, err := env.GetEventPayload() - if err != nil { - return "", fmt.Errorf("failed to get event payload: %w", err) - } - - pr, ok := event.(*github.PullRequestEvent) - if !ok { - return "", fmt.Errorf("unexpected event type") - } - - if pr.PullRequest.Head.SHA == nil { - return "", fmt.Errorf("pull request head SHA is empty") - } - - return *pr.PullRequest.Head.SHA, nil - } else if env.GetEventType() == "push" { - g.logger.Debug("Found GitHub push event") - event, err := env.GetEventPayload() - if err != nil { - return "", fmt.Errorf("failed to get event payload: %w", err) - } - - push, ok := event.(*github.PushEvent) - if !ok { - return "", fmt.Errorf("unexpected event type") - } - - if push.After == nil { - return "", fmt.Errorf("push event after SHA is empty") - } - - return *push.After, nil + if gh.InGithubActions() { + commit, err := g.gh.GetCommit() + if err != nil { + return "", fmt.Errorf("failed to get commit from GitHub event: %w", err) } + + return commit, nil } g.logger.Debug("No GitHub event found, getting commit hash from git repository") - ref, err := repo.Head() + obj, err := repo.HeadCommit() if err != nil { return "", fmt.Errorf("failed to get HEAD: %w", err) } - obj, err := repo.GetCommit(ref.Hash()) - if err != nil { - return "", fmt.Errorf("failed to get commit object: %w", err) - } - return obj.Hash.String(), nil } // NewGitRuntime creates a new GitRuntime. func NewGitRuntime(logger *slog.Logger) *GitRuntime { + fs := billy.NewBaseOsFS() + ghr := gh.NewCustomDefaultGithubRepo(fs, logger) return &GitRuntime{ - fs: billy.NewBaseOsFS(), + fs: fs, + gh: &ghr, logger: logger, } } // NewCustomGitRuntime creates a new GitRuntime with a custom filesystem. func NewCustomGitRuntime(fs fs.Filesystem, logger *slog.Logger) *GitRuntime { + ghr := gh.NewCustomDefaultGithubRepo(fs, logger) return &GitRuntime{ fs: fs, + gh: &ghr, logger: logger, } } diff --git a/lib/project/project/runtime_test.go b/lib/project/project/runtime_test.go index 9fa1f102..dcba9b01 100644 --- a/lib/project/project/runtime_test.go +++ b/lib/project/project/runtime_test.go @@ -10,6 +10,7 @@ import ( "github.com/input-output-hk/catalyst-forge/lib/project/blueprint" lc "github.com/input-output-hk/catalyst-forge/lib/tools/cue" "github.com/input-output-hk/catalyst-forge/lib/tools/fs/billy" + ghm "github.com/input-output-hk/catalyst-forge/lib/tools/git/github/mocks" "github.com/input-output-hk/catalyst-forge/lib/tools/git/repo" "github.com/input-output-hk/catalyst-forge/lib/tools/testutils" "github.com/stretchr/testify/assert" @@ -66,17 +67,10 @@ func TestDeploymentRuntimeLoad(t *testing.T) { func TestGitRuntimeLoad(t *testing.T) { ctx := cuecontext.New() - prPayload, err := os.ReadFile("testdata/event_pr.json") - require.NoError(t, err) - - pushPayload, err := os.ReadFile("testdata/event_push.json") - require.NoError(t, err) - tests := []struct { name string tag *ProjectTag - env map[string]string - files map[string]string + ghResult string validate func(*testing.T, repo.GitRepo, map[string]cue.Value) }{ { @@ -100,33 +94,11 @@ func TestGitRuntimeLoad(t *testing.T) { }, }, { - name: "with pr event", - env: map[string]string{ - "GITHUB_EVENT_NAME": "pull_request", - "GITHUB_EVENT_PATH": "/event.json", - }, - files: map[string]string{ - "/event.json": string(prPayload), - }, - validate: func(t *testing.T, repo repo.GitRepo, data map[string]cue.Value) { - require.NoError(t, err) - assert.Contains(t, data, "GIT_COMMIT_HASH") - assert.Equal(t, "0000000000000000000000000000000000000000", getString(t, data["GIT_COMMIT_HASH"])) - }, - }, - { - name: "with push event", - env: map[string]string{ - "GITHUB_EVENT_NAME": "push", - "GITHUB_EVENT_PATH": "/event.json", - }, - files: map[string]string{ - "/event.json": string(pushPayload), - }, + name: "with github event", + ghResult: "hash", validate: func(t *testing.T, repo repo.GitRepo, data map[string]cue.Value) { - require.NoError(t, err) assert.Contains(t, data, "GIT_COMMIT_HASH") - assert.Equal(t, "0000000000000000000000000000000000000000", getString(t, data["GIT_COMMIT_HASH"])) + assert.Equal(t, "hash", getString(t, data["GIT_COMMIT_HASH"])) }, }, } @@ -139,20 +111,15 @@ func TestGitRuntimeLoad(t *testing.T) { err := repo.WriteFile("example.txt", []byte("example content")) require.NoError(t, err) - _, err = repo.Commit("Initial commit") - require.NoError(t, err) - - if len(tt.env) > 0 { - for k, v := range tt.env { - require.NoError(t, os.Setenv(k, v)) - defer os.Unsetenv(k) - } + if tt.ghResult != "" { + os.Setenv("GITHUB_EVENT_PATH", "foo") + os.Setenv("GITHUB_EVENT_NAME", "bar") + defer os.Unsetenv("GITHUB_EVENT_PATH") + defer os.Unsetenv("GITHUB_EVENT_NAME") } - fs := billy.NewInMemoryFs() - if len(tt.files) > 0 { - testutils.SetupFS(t, fs, tt.files) - } + _, err = repo.Commit("Initial commit") + require.NoError(t, err) project := &Project{ ctx: ctx, @@ -162,7 +129,17 @@ func TestGitRuntimeLoad(t *testing.T) { logger: logger, } - runtime := NewCustomGitRuntime(fs, logger) + fs := billy.NewInMemoryFs() + ghr := &ghm.GithubRepoMock{ + GetCommitFunc: func() (string, error) { + return tt.ghResult, nil + }, + } + runtime := GitRuntime{ + fs: fs, + gh: ghr, + logger: logger, + } data := runtime.Load(project) tt.validate(t, repo, data) }) diff --git a/lib/schema/blueprint/global/cue_types_gen.go b/lib/schema/blueprint/global/cue_types_gen.go index 8387f3be..261e54d0 100644 --- a/lib/schema/blueprint/global/cue_types_gen.go +++ b/lib/schema/blueprint/global/cue_types_gen.go @@ -26,6 +26,9 @@ type Deployment struct { // Environment contains the default environment to deploy projects to. Environment string `json:"environment"` + // Foundry contains the configuration for Foundry. + Foundry DeploymentFoundry `json:"foundry"` + // Registries contains the configuration for the global deployment registries. Registries DeploymentRegistries `json:"registries"` @@ -36,6 +39,12 @@ type Deployment struct { Root string `json:"root"` } +// DeploymentFoundry contains the configuration for Foundry. +type DeploymentFoundry struct { + // api contains the URL of the Foundry API server. + Api string `json:"api"` +} + // DeploymentRegistries contains the configuration for the global deployment registries. type DeploymentRegistries struct { // Containers contains the default container registry to use for deploying containers. @@ -72,9 +81,12 @@ type Global struct { } type Repo struct { + // DefaultBranch contains the default branch of the repository. + DefaultBranch string `json:"defaultBranch"` + // Name contains the name of the repository (e.g. "owner/repo-name"). Name string `json:"name"` - // DefaultBranch contains the default branch of the repository. - DefaultBranch string `json:"defaultBranch"` + // URL contains the URL to the repository (used for cloning). + Url string `json:"url"` } diff --git a/lib/schema/blueprint/global/deployment.cue b/lib/schema/blueprint/global/deployment.cue index d4fdb9bb..79973e3f 100644 --- a/lib/schema/blueprint/global/deployment.cue +++ b/lib/schema/blueprint/global/deployment.cue @@ -4,6 +4,9 @@ package global // Environment contains the default environment to deploy projects to. environment: string | *"dev" + // Foundry contains the configuration for Foundry. + foundry: #DeploymentFoundry + // Registries contains the configuration for the global deployment registries. registries: #DeploymentRegistries @@ -14,6 +17,12 @@ package global root: string } +// DeploymentFoundry contains the configuration for Foundry. +#DeploymentFoundry: { + // api contains the URL of the Foundry API server. + api: string +} + // DeploymentRegistries contains the configuration for the global deployment registries. #DeploymentRegistries: { // Containers contains the default container registry to use for deploying containers. diff --git a/lib/schema/blueprint/global/main.cue b/lib/schema/blueprint/global/main.cue index ce9b88e8..d5c7e6c1 100644 --- a/lib/schema/blueprint/global/main.cue +++ b/lib/schema/blueprint/global/main.cue @@ -18,9 +18,12 @@ package global } #Repo: { - // Name contains the name of the repository (e.g. "owner/repo-name"). - name: string - // DefaultBranch contains the default branch of the repository. defaultBranch: string + + // Name contains the name of the repository (e.g. "owner/repo-name"). + name: string + + // URL contains the URL to the repository (used for cloning). + url: string } diff --git a/lib/tools/git/branch.go b/lib/tools/git/branch.go index 6967728a..5c46893a 100644 --- a/lib/tools/git/branch.go +++ b/lib/tools/git/branch.go @@ -12,10 +12,10 @@ var ( ) func GetBranch(repo *repo.GitRepo) (string, error) { - env := github.NewGithubEnv(nil) + ghr := github.NewDefaultGithubRepo(nil) - if github.InCI() { - ref := env.GetBranch() + if github.InGithubActions() { + ref := ghr.GetBranch() if ref != "" { return ref, nil } diff --git a/lib/tools/git/github/env.go b/lib/tools/git/github/env.go deleted file mode 100644 index debfb479..00000000 --- a/lib/tools/git/github/env.go +++ /dev/null @@ -1,114 +0,0 @@ -package github - -import ( - "fmt" - "io" - "log/slog" - "os" - "strings" - - "github.com/google/go-github/v66/github" - "github.com/input-output-hk/catalyst-forge/lib/tools/fs" - "github.com/input-output-hk/catalyst-forge/lib/tools/fs/billy" -) - -var ( - ErrNoEventFound = fmt.Errorf("no GitHub event data found") - ErrTagNotFound = fmt.Errorf("tag not found") -) - -// GithubEnv provides GitHub environment information. -type GithubEnv struct { - fs fs.Filesystem - logger *slog.Logger -} - -func (g *GithubEnv) GetBranch() string { - ref, ok := os.LookupEnv("GITHUB_HEAD_REF") - if !ok || ref == "" { - if strings.HasPrefix(os.Getenv("GITHUB_REF"), "refs/heads/") { - return strings.TrimPrefix(os.Getenv("GITHUB_REF"), "refs/heads/") - } - } - - return ref -} - -// GetEventPayload returns the GitHub event payload. -func (g *GithubEnv) GetEventPayload() (any, error) { - path, pathExists := os.LookupEnv("GITHUB_EVENT_PATH") - name, nameExists := os.LookupEnv("GITHUB_EVENT_NAME") - - if !pathExists || !nameExists { - return nil, ErrNoEventFound - } - - g.logger.Debug("Reading GitHub event data", "path", path, "name", name) - payload, err := g.fs.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("failed to read GitHub event data: %w", err) - } - - event, err := github.ParseWebHook(name, payload) - if err != nil { - return nil, fmt.Errorf("failed to parse GitHub event data: %w", err) - } - - return event, nil -} - -// GetEventType returns the GitHub event type. -func (g *GithubEnv) GetEventType() string { - return os.Getenv("GITHUB_EVENT_NAME") -} - -// GetTag returns the tag from the CI environment if it exists. -// If the tag is not found, an empty string is returned. -func (g *GithubEnv) GetTag() string { - tag, exists := os.LookupEnv("GITHUB_REF") - if exists && strings.HasPrefix(tag, "refs/tags/") { - return strings.TrimPrefix(tag, "refs/tags/") - } - - return "" -} - -// HasEvent returns whether a GitHub event payload exists. -func (g *GithubEnv) HasEvent() bool { - _, pathExists := os.LookupEnv("GITHUB_EVENT_PATH") - _, nameExists := os.LookupEnv("GITHUB_EVENT_NAME") - return pathExists && nameExists -} - -// NewGithubEnv creates a new GithubEnv. -func NewGithubEnv(logger *slog.Logger) GithubEnv { - if logger == nil { - logger = slog.New(slog.NewTextHandler(io.Discard, nil)) - } - - return GithubEnv{ - fs: billy.NewBaseOsFS(), - logger: logger, - } -} - -// NewCustomGithubEnv creates a new GithubEnv with a custom filesystem. -func NewCustomGithubEnv(fs fs.Filesystem, logger *slog.Logger) GithubEnv { - if logger == nil { - logger = slog.New(slog.NewTextHandler(io.Discard, nil)) - } - - return GithubEnv{ - fs: fs, - logger: logger, - } -} - -// InCI returns whether the code is running in a CI environment. -func InCI() bool { - if _, ok := os.LookupEnv("GITHUB_ACTIONS"); ok { - return true - } - - return false -} diff --git a/lib/tools/git/github/mocks/repo.go b/lib/tools/git/github/mocks/repo.go new file mode 100644 index 00000000..705b6947 --- /dev/null +++ b/lib/tools/git/github/mocks/repo.go @@ -0,0 +1,137 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package mocks + +import ( + "sync" +) + +// GithubRepoMock is a mock implementation of github.GithubRepo. +// +// func TestSomethingThatUsesGithubRepo(t *testing.T) { +// +// // make and configure a mocked github.GithubRepo +// mockedGithubRepo := &GithubRepoMock{ +// GetBranchFunc: func() string { +// panic("mock out the GetBranch method") +// }, +// GetCommitFunc: func() (string, error) { +// panic("mock out the GetCommit method") +// }, +// GetTagFunc: func() (string, bool) { +// panic("mock out the GetTag method") +// }, +// } +// +// // use mockedGithubRepo in code that requires github.GithubRepo +// // and then make assertions. +// +// } +type GithubRepoMock struct { + // GetBranchFunc mocks the GetBranch method. + GetBranchFunc func() string + + // GetCommitFunc mocks the GetCommit method. + GetCommitFunc func() (string, error) + + // GetTagFunc mocks the GetTag method. + GetTagFunc func() (string, bool) + + // calls tracks calls to the methods. + calls struct { + // GetBranch holds details about calls to the GetBranch method. + GetBranch []struct { + } + // GetCommit holds details about calls to the GetCommit method. + GetCommit []struct { + } + // GetTag holds details about calls to the GetTag method. + GetTag []struct { + } + } + lockGetBranch sync.RWMutex + lockGetCommit sync.RWMutex + lockGetTag sync.RWMutex +} + +// GetBranch calls GetBranchFunc. +func (mock *GithubRepoMock) GetBranch() string { + if mock.GetBranchFunc == nil { + panic("GithubRepoMock.GetBranchFunc: method is nil but GithubRepo.GetBranch was just called") + } + callInfo := struct { + }{} + mock.lockGetBranch.Lock() + mock.calls.GetBranch = append(mock.calls.GetBranch, callInfo) + mock.lockGetBranch.Unlock() + return mock.GetBranchFunc() +} + +// GetBranchCalls gets all the calls that were made to GetBranch. +// Check the length with: +// +// len(mockedGithubRepo.GetBranchCalls()) +func (mock *GithubRepoMock) GetBranchCalls() []struct { +} { + var calls []struct { + } + mock.lockGetBranch.RLock() + calls = mock.calls.GetBranch + mock.lockGetBranch.RUnlock() + return calls +} + +// GetCommit calls GetCommitFunc. +func (mock *GithubRepoMock) GetCommit() (string, error) { + if mock.GetCommitFunc == nil { + panic("GithubRepoMock.GetCommitFunc: method is nil but GithubRepo.GetCommit was just called") + } + callInfo := struct { + }{} + mock.lockGetCommit.Lock() + mock.calls.GetCommit = append(mock.calls.GetCommit, callInfo) + mock.lockGetCommit.Unlock() + return mock.GetCommitFunc() +} + +// GetCommitCalls gets all the calls that were made to GetCommit. +// Check the length with: +// +// len(mockedGithubRepo.GetCommitCalls()) +func (mock *GithubRepoMock) GetCommitCalls() []struct { +} { + var calls []struct { + } + mock.lockGetCommit.RLock() + calls = mock.calls.GetCommit + mock.lockGetCommit.RUnlock() + return calls +} + +// GetTag calls GetTagFunc. +func (mock *GithubRepoMock) GetTag() (string, bool) { + if mock.GetTagFunc == nil { + panic("GithubRepoMock.GetTagFunc: method is nil but GithubRepo.GetTag was just called") + } + callInfo := struct { + }{} + mock.lockGetTag.Lock() + mock.calls.GetTag = append(mock.calls.GetTag, callInfo) + mock.lockGetTag.Unlock() + return mock.GetTagFunc() +} + +// GetTagCalls gets all the calls that were made to GetTag. +// Check the length with: +// +// len(mockedGithubRepo.GetTagCalls()) +func (mock *GithubRepoMock) GetTagCalls() []struct { +} { + var calls []struct { + } + mock.lockGetTag.RLock() + calls = mock.calls.GetTag + mock.lockGetTag.RUnlock() + return calls +} diff --git a/lib/tools/git/github/repo.go b/lib/tools/git/github/repo.go new file mode 100644 index 00000000..63493fff --- /dev/null +++ b/lib/tools/git/github/repo.go @@ -0,0 +1,155 @@ +package github + +import ( + "fmt" + "io" + "log/slog" + "os" + "strings" + + "github.com/google/go-github/v66/github" + "github.com/input-output-hk/catalyst-forge/lib/tools/fs" + "github.com/input-output-hk/catalyst-forge/lib/tools/fs/billy" +) + +//go:generate go run github.com/matryer/moq@latest -skip-ensure --pkg mocks -out mocks/repo.go . GithubRepo + +var ( + ErrNotRunningInGHActions = fmt.Errorf("not running in GitHub Actions") +) + +// GithubRepo is an interface for interacting with GitHub repositories. +type GithubRepo interface { + GetBranch() string + GetCommit() (string, error) + GetTag() (string, bool) +} + +// DefaultGithubRepo is the default implementation of the GithubRepo interface. +type DefaultGithubRepo struct { + fs fs.Filesystem + logger *slog.Logger +} + +// GetBranch returns the branch name from the CI environment. +func (g *DefaultGithubRepo) GetBranch() string { + ref, ok := os.LookupEnv("GITHUB_HEAD_REF") + if !ok || ref == "" { + if strings.HasPrefix(os.Getenv("GITHUB_REF"), "refs/heads/") { + return strings.TrimPrefix(os.Getenv("GITHUB_REF"), "refs/heads/") + } + } + + return ref +} + +// GetCommit returns the commit SHA from the CI environment. +func (g *DefaultGithubRepo) GetCommit() (string, error) { + if !InGithubActions() { + return "", ErrNotRunningInGHActions + } + + if g.getEventType() == "pull_request" { + g.logger.Debug("Found GitHub pull request event") + event, err := g.getEventPayload() + if err != nil { + return "", fmt.Errorf("failed to get event payload: %w", err) + } + + pr, ok := event.(*github.PullRequestEvent) + if !ok { + return "", fmt.Errorf("unexpected event type") + } + + if pr.PullRequest.Head.SHA == nil { + return "", fmt.Errorf("pull request head SHA is empty") + } + + return *pr.PullRequest.Head.SHA, nil + } else if g.getEventType() == "push" { + g.logger.Debug("Found GitHub push event") + event, err := g.getEventPayload() + if err != nil { + return "", fmt.Errorf("failed to get event payload: %w", err) + } + + push, ok := event.(*github.PushEvent) + if !ok { + return "", fmt.Errorf("unexpected event type") + } + + if push.After == nil { + return "", fmt.Errorf("push event after SHA is empty") + } + + return *push.After, nil + } + + return "", fmt.Errorf("unsupported event type: %s", g.getEventType()) +} + +// GetTag returns the tag from the CI environment if it exists. +// If the tag is not found, it returns an empty string and false. +func (g *DefaultGithubRepo) GetTag() (string, bool) { + tag, exists := os.LookupEnv("GITHUB_REF") + if exists && strings.HasPrefix(tag, "refs/tags/") { + return strings.TrimPrefix(tag, "refs/tags/"), true + } + + return "", false +} + +// getEventPayload returns the GitHub event payload. +func (g *DefaultGithubRepo) getEventPayload() (any, error) { + if !InGithubActions() { + return "", ErrNotRunningInGHActions + } + + path := os.Getenv("GITHUB_EVENT_PATH") + name := os.Getenv("GITHUB_EVENT_NAME") + + g.logger.Debug("Reading GitHub event data", "path", path, "name", name) + payload, err := g.fs.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read GitHub event data: %w", err) + } + + event, err := github.ParseWebHook(name, payload) + if err != nil { + return nil, fmt.Errorf("failed to parse GitHub event data: %w", err) + } + + return event, nil +} + +// getEventType returns the GitHub event type. +func (g *DefaultGithubRepo) getEventType() string { + return os.Getenv("GITHUB_EVENT_NAME") +} + +// NewDefaultGithubRepo creates a new DefaultGithubRepo. +func NewDefaultGithubRepo(logger *slog.Logger) DefaultGithubRepo { + if logger == nil { + logger = slog.New(slog.NewTextHandler(io.Discard, nil)) + } + + return DefaultGithubRepo{ + fs: billy.NewBaseOsFS(), + logger: logger, + } +} + +// NewCustomGithubRepo creates a new DefaultGithubRepo with a custom filesystem. +func NewCustomDefaultGithubRepo(fs fs.Filesystem, logger *slog.Logger) DefaultGithubRepo { + return DefaultGithubRepo{ + fs: fs, + logger: logger, + } +} + +// InGithubActions returns whether the current process is running in a GitHub Actions environment. +func InGithubActions() bool { + _, pathExists := os.LookupEnv("GITHUB_EVENT_PATH") + _, nameExists := os.LookupEnv("GITHUB_EVENT_NAME") + return pathExists && nameExists +} diff --git a/lib/tools/git/github/env_test.go b/lib/tools/git/github/repo_test.go similarity index 60% rename from lib/tools/git/github/env_test.go rename to lib/tools/git/github/repo_test.go index 9bdfb8ee..dc10762e 100644 --- a/lib/tools/git/github/env_test.go +++ b/lib/tools/git/github/repo_test.go @@ -4,14 +4,13 @@ import ( "os" "testing" - "github.com/google/go-github/v66/github" "github.com/input-output-hk/catalyst-forge/lib/tools/fs/billy" "github.com/input-output-hk/catalyst-forge/lib/tools/testutils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestGithubEnvGetBranch(t *testing.T) { +func TestGithubRepoGetBranch(t *testing.T) { tests := []struct { name string env map[string]string @@ -44,113 +43,90 @@ func TestGithubEnvGetBranch(t *testing.T) { defer os.Unsetenv(k) } - gh := GithubEnv{} + gh := DefaultGithubRepo{} tt.validate(t, gh.GetBranch()) }) } } -func TestGithubEnvGetEventPayload(t *testing.T) { - payload, err := os.ReadFile("testdata/event.json") +func TestGithubRepoGetCommit(t *testing.T) { + prPayload, err := os.ReadFile("testdata/event_pr.json") + require.NoError(t, err) + + pushPayload, err := os.ReadFile("testdata/event_push.json") require.NoError(t, err) tests := []struct { name string env map[string]string files map[string]string - validate func(*testing.T, any, error) + validate func(*testing.T, string, error) }{ { - name: "full", - env: map[string]string{ - "GITHUB_EVENT_PATH": "/event.json", - "GITHUB_EVENT_NAME": "pull_request", - }, - files: map[string]string{ - "/event.json": string(payload), - }, - validate: func(t *testing.T, payload any, err error) { - require.NoError(t, err) - _, ok := payload.(*github.PullRequestEvent) - require.True(t, ok) - }, - }, - { - name: "missing path", + name: "pull request", env: map[string]string{ "GITHUB_EVENT_NAME": "pull_request", - }, - validate: func(t *testing.T, payload any, err error) { - require.ErrorIs(t, err, ErrNoEventFound) - }, - }, - { - name: "missing name", - env: map[string]string{ "GITHUB_EVENT_PATH": "/event.json", }, files: map[string]string{ - "/event.json": string(payload), + "/event.json": string(prPayload), }, - validate: func(t *testing.T, payload any, err error) { - require.ErrorIs(t, err, ErrNoEventFound) + validate: func(t *testing.T, commit string, err error) { + require.NoError(t, err) + assert.Equal(t, "0000000000000000000000000000000000000000", commit) }, }, { - name: "invalid payload", + name: "push", env: map[string]string{ + "GITHUB_EVENT_NAME": "push", "GITHUB_EVENT_PATH": "/event.json", - "GITHUB_EVENT_NAME": "pull_request", }, files: map[string]string{ - "/event.json": "invalid", + "/event.json": string(pushPayload), }, - validate: func(t *testing.T, payload any, err error) { - require.Error(t, err) + validate: func(t *testing.T, commit string, err error) { + require.NoError(t, err) + assert.Equal(t, "0000000000000000000000000000000000000000", commit) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + logger := testutils.NewNoopLogger() + + fs := billy.NewInMemoryFs() + testutils.SetupFS(t, fs, tt.files) + for k, v := range tt.env { require.NoError(t, os.Setenv(k, v)) defer os.Unsetenv(k) } - fs := billy.NewInMemoryFs() - testutils.SetupFS(t, fs, tt.files) - - gh := GithubEnv{ + gh := DefaultGithubRepo{ fs: fs, - logger: testutils.NewNoopLogger(), + logger: logger, } - - payload, err := gh.GetEventPayload() - tt.validate(t, payload, err) + commit, err := gh.GetCommit() + tt.validate(t, commit, err) }) } } -func TestGithubEnvGetEventType(t *testing.T) { - gh := GithubEnv{} - - require.NoError(t, os.Setenv("GITHUB_EVENT_NAME", "push")) - assert.Equal(t, "push", gh.GetEventType()) -} - -func TestGithubEnvGetTag(t *testing.T) { +func TestGithubRepoGetTag(t *testing.T) { tests := []struct { name string env map[string]string - validate func(*testing.T, string) + validate func(*testing.T, string, bool) }{ { name: "tag", env: map[string]string{ "GITHUB_REF": "refs/tags/v1.0.0", }, - validate: func(t *testing.T, tag string) { + validate: func(t *testing.T, tag string, ok bool) { + assert.True(t, ok) assert.Equal(t, "v1.0.0", tag) }, }, @@ -159,7 +135,8 @@ func TestGithubEnvGetTag(t *testing.T) { env: map[string]string{ "GITHUB_REF": "refs/heads/feature/branch", }, - validate: func(t *testing.T, tag string) { + validate: func(t *testing.T, tag string, ok bool) { + assert.False(t, ok) assert.Empty(t, tag) }, }, @@ -172,17 +149,18 @@ func TestGithubEnvGetTag(t *testing.T) { defer os.Unsetenv(k) } - gh := GithubEnv{} - tt.validate(t, gh.GetTag()) + gh := DefaultGithubRepo{} + tag, ok := gh.GetTag() + tt.validate(t, tag, ok) }) } } -func TestGithubEnvHasEvent(t *testing.T) { +func TestInGithubActions(t *testing.T) { tests := []struct { - name string - env map[string]string - expect bool + name string + env map[string]string + validate func(*testing.T, bool) }{ { name: "has event", @@ -190,21 +168,27 @@ func TestGithubEnvHasEvent(t *testing.T) { "GITHUB_EVENT_PATH": "/path/to/event", "GITHUB_EVENT_NAME": "push", }, - expect: true, + validate: func(t *testing.T, exists bool) { + assert.True(t, exists) + }, }, { name: "missing path", env: map[string]string{ "GITHUB_EVENT_NAME": "push", }, - expect: false, + validate: func(t *testing.T, exists bool) { + assert.False(t, exists) + }, }, { name: "missing name", env: map[string]string{ "GITHUB_EVENT_PATH": "/path/to/event", }, - expect: false, + validate: func(t *testing.T, exists bool) { + assert.False(t, exists) + }, }, } @@ -215,8 +199,7 @@ func TestGithubEnvHasEvent(t *testing.T) { defer os.Unsetenv(k) } - gh := GithubEnv{} - assert.Equal(t, tt.expect, gh.HasEvent()) + tt.validate(t, InGithubActions()) }) } } diff --git a/lib/tools/git/github/testdata/event_pr.json b/lib/tools/git/github/testdata/event_pr.json new file mode 100644 index 00000000..6bc9bd42 --- /dev/null +++ b/lib/tools/git/github/testdata/event_pr.json @@ -0,0 +1,439 @@ +{ + "action": "synchronize", + "after": "0000000000000000000000000000000000000000", + "before": "0000000000000000000000000000000000000000", + "number": 90, + "organization": { + "avatar_url": "https://example.com/placeholder-avatar.png", + "description": "", + "events_url": "https://api.github.com/orgs/org-name/events", + "hooks_url": "https://api.github.com/orgs/org-name/hooks", + "id": 10000000, + "issues_url": "https://api.github.com/orgs/org-name/issues", + "login": "org-name", + "members_url": "https://api.github.com/orgs/org-name/members{/member}", + "node_id": "ANONYMIZED_NODE_ID", + "public_members_url": "https://api.github.com/orgs/org-name/public_members{/member}", + "repos_url": "https://api.github.com/orgs/org-name/repos", + "url": "https://api.github.com/orgs/org-name" + }, + "pull_request": { + "_links": { + "comments": { + "href": "https://api.github.com/repos/org-name/repo-name/issues/90/comments" + }, + "commits": { + "href": "https://api.github.com/repos/org-name/repo-name/pulls/90/commits" + }, + "html": { + "href": "https://github.com/org-name/repo-name/pull/90" + }, + "issue": { + "href": "https://api.github.com/repos/org-name/repo-name/issues/90" + }, + "review_comment": { + "href": "https://api.github.com/repos/org-name/repo-name/pulls/comments{/number}" + }, + "review_comments": { + "href": "https://api.github.com/repos/org-name/repo-name/pulls/90/comments" + }, + "self": { + "href": "https://api.github.com/repos/org-name/repo-name/pulls/90" + }, + "statuses": { + "href": "https://api.github.com/repos/org-name/repo-name/statuses/0000000000000000000000000000000000000000" + } + }, + "active_lock_reason": null, + "additions": 136, + "assignee": null, + "assignees": [], + "author_association": "COLLABORATOR", + "auto_merge": null, + "base": { + "label": "org-name:master", + "ref": "master", + "repo": { + "allow_auto_merge": false, + "allow_forking": true, + "allow_merge_commit": true, + "allow_rebase_merge": true, + "allow_squash_merge": true, + "allow_update_branch": false, + "archive_url": "https://api.github.com/repos/org-name/repo-name/{archive_format}{/ref}", + "archived": false, + "assignees_url": "https://api.github.com/repos/org-name/repo-name/assignees{/user}", + "blobs_url": "https://api.github.com/repos/org-name/repo-name/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/org-name/repo-name/branches{/branch}", + "clone_url": "https://github.com/org-name/repo-name.git", + "collaborators_url": "https://api.github.com/repos/org-name/repo-name/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/org-name/repo-name/comments{/number}", + "commits_url": "https://api.github.com/repos/org-name/repo-name/commits{/sha}", + "compare_url": "https://api.github.com/repos/org-name/repo-name/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/org-name/repo-name/contents/{+path}", + "contributors_url": "https://api.github.com/repos/org-name/repo-name/contributors", + "created_at": "2024-08-23T22:50:51Z", + "default_branch": "master", + "delete_branch_on_merge": false, + "deployments_url": "https://api.github.com/repos/org-name/repo-name/deployments", + "description": "Example repository description", + "disabled": false, + "downloads_url": "https://api.github.com/repos/org-name/repo-name/downloads", + "events_url": "https://api.github.com/repos/org-name/repo-name/events", + "fork": false, + "forks": 0, + "forks_count": 0, + "forks_url": "https://api.github.com/repos/org-name/repo-name/forks", + "full_name": "org-name/repo-name", + "git_commits_url": "https://api.github.com/repos/org-name/repo-name/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/org-name/repo-name/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/org-name/repo-name/git/tags{/sha}", + "git_url": "git://github.com/org-name/repo-name.git", + "has_discussions": false, + "has_downloads": true, + "has_issues": true, + "has_pages": true, + "has_projects": true, + "has_wiki": true, + "homepage": "https://org-name.github.io/repo-name/", + "hooks_url": "https://api.github.com/repos/org-name/repo-name/hooks", + "html_url": "https://github.com/org-name/repo-name", + "id": 800000000, + "is_template": false, + "issue_comment_url": "https://api.github.com/repos/org-name/repo-name/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/org-name/repo-name/issues/events{/number}", + "issues_url": "https://api.github.com/repos/org-name/repo-name/issues{/number}", + "keys_url": "https://api.github.com/repos/org-name/repo-name/keys{/key_id}", + "labels_url": "https://api.github.com/repos/org-name/repo-name/labels{/name}", + "language": "Go", + "languages_url": "https://api.github.com/repos/org-name/repo-name/languages", + "license": { + "key": "apache-2.0", + "name": "Apache License 2.0", + "node_id": "MDc6TGljZW5zZTI=", + "spdx_id": "Apache-2.0", + "url": "https://api.github.com/licenses/apache-2.0" + }, + "merge_commit_message": "PR_TITLE", + "merge_commit_title": "MERGE_MESSAGE", + "merges_url": "https://api.github.com/repos/org-name/repo-name/merges", + "milestones_url": "https://api.github.com/repos/org-name/repo-name/milestones{/number}", + "mirror_url": null, + "name": "repo-name", + "node_id": "ANONYMIZED_NODE_ID", + "notifications_url": "https://api.github.com/repos/org-name/repo-name/notifications{?since,all,participating}", + "open_issues": 9, + "open_issues_count": 9, + "owner": { + "avatar_url": "https://example.com/placeholder-avatar.png", + "events_url": "https://api.github.com/users/org-name/events{/privacy}", + "followers_url": "https://api.github.com/users/org-name/followers", + "following_url": "https://api.github.com/users/org-name/following{/other_user}", + "gists_url": "https://api.github.com/users/org-name/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/org-name", + "id": 10000000, + "login": "org-name", + "node_id": "ANONYMIZED_NODE_ID", + "organizations_url": "https://api.github.com/users/org-name/orgs", + "received_events_url": "https://api.github.com/users/org-name/received_events", + "repos_url": "https://api.github.com/users/org-name/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/org-name/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/org-name/subscriptions", + "type": "Organization", + "url": "https://api.github.com/users/org-name", + "user_view_type": "public" + }, + "private": false, + "pulls_url": "https://api.github.com/repos/org-name/repo-name/pulls{/number}", + "pushed_at": "2024-10-30T02:21:47Z", + "releases_url": "https://api.github.com/repos/org-name/repo-name/releases{/id}", + "size": 6466, + "squash_merge_commit_message": "COMMIT_MESSAGES", + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "ssh_url": "git@github.com:org-name/repo-name.git", + "stargazers_count": 1, + "stargazers_url": "https://api.github.com/repos/org-name/repo-name/stargazers", + "statuses_url": "https://api.github.com/repos/org-name/repo-name/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/org-name/repo-name/subscribers", + "subscription_url": "https://api.github.com/repos/org-name/repo-name/subscription", + "svn_url": "https://github.com/org-name/repo-name", + "tags_url": "https://api.github.com/repos/org-name/repo-name/tags", + "teams_url": "https://api.github.com/repos/org-name/repo-name/teams", + "topics": [], + "trees_url": "https://api.github.com/repos/org-name/repo-name/git/trees{/sha}", + "updated_at": "2024-10-30T01:08:06Z", + "url": "https://api.github.com/repos/org-name/repo-name", + "use_squash_pr_title_as_default": false, + "visibility": "public", + "watchers": 1, + "watchers_count": 1, + "web_commit_signoff_required": false + }, + "sha": "0000000000000000000000000000000000000000", + "user": { + "avatar_url": "https://example.com/placeholder-avatar.png", + "events_url": "https://api.github.com/users/org-name/events{/privacy}", + "followers_url": "https://api.github.com/users/org-name/followers", + "following_url": "https://api.github.com/users/org-name/following{/other_user}", + "gists_url": "https://api.github.com/users/org-name/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/org-name", + "id": 10000000, + "login": "org-name", + "node_id": "ANONYMIZED_NODE_ID", + "organizations_url": "https://api.github.com/users/org-name/orgs", + "received_events_url": "https://api.github.com/users/org-name/received_events", + "repos_url": "https://api.github.com/users/org-name/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/org-name/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/org-name/subscriptions", + "type": "Organization", + "url": "https://api.github.com/users/org-name", + "user_view_type": "public" + } + }, + "body": "- **fix: uses correct id in container**\n- **wip: fix**\n- **wip: fix**\n- **fix: adds certs**\n- **Add init.sh script**\n- **Move to root**\n- **Set execute bit**\n- **fix: try version 0.17.0**\n- **wip: adds helper**\n- **Bump version**\n- **wip: adds debug flag**\n- **wip: testing**\n", + "changed_files": 8, + "closed_at": null, + "comments": 0, + "comments_url": "https://api.github.com/repos/org-name/repo-name/issues/90/comments", + "commits": 19, + "commits_url": "https://api.github.com/repos/org-name/repo-name/pulls/90/commits", + "created_at": "2024-10-30T00:22:58Z", + "deletions": 101, + "diff_url": "https://github.com/org-name/repo-name/pull/90.diff", + "draft": false, + "head": { + "label": "org-name:branch-name", + "ref": "branch-name", + "repo": { + "id": 800000000, + "node_id": "ANONYMIZED_NODE_ID", + "name": "repo-name", + "full_name": "org-name/repo-name", + "private": false, + "owner": { + "login": "org-name", + "id": 10000000, + "node_id": "ANONYMIZED_NODE_ID", + "avatar_url": "https://example.com/placeholder-avatar.png", + "type": "Organization", + "site_admin": false, + "url": "https://api.github.com/users/org-name", + "html_url": "https://github.com/org-name", + "followers_url": "https://api.github.com/users/org-name/followers", + "following_url": "https://api.github.com/users/org-name/following{/other_user}", + "gists_url": "https://api.github.com/users/org-name/gists{/gist_id}", + "starred_url": "https://api.github.com/users/org-name/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/org-name/subscriptions", + "organizations_url": "https://api.github.com/users/org-name/orgs", + "repos_url": "https://api.github.com/users/org-name/repos", + "events_url": "https://api.github.com/users/org-name/events{/privacy}", + "received_events_url": "https://api.github.com/users/org-name/received_events", + "user_view_type": "public" + } + }, + "sha": "0000000000000000000000000000000000000000", + "user": { + "avatar_url": "https://example.com/placeholder-avatar.png", + "events_url": "https://api.github.com/users/org-name/events{/privacy}", + "followers_url": "https://api.github.com/users/org-name/followers", + "following_url": "https://api.github.com/users/org-name/following{/other_user}", + "gists_url": "https://api.github.com/users/org-name/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/org-name", + "id": 10000000, + "login": "org-name", + "node_id": "ANONYMIZED_NODE_ID", + "organizations_url": "https://api.github.com/users/org-name/orgs", + "received_events_url": "https://api.github.com/users/org-name/received_events", + "repos_url": "https://api.github.com/users/org-name/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/org-name/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/org-name/subscriptions", + "type": "Organization", + "url": "https://api.github.com/users/org-name", + "user_view_type": "public" + } + }, + "html_url": "https://github.com/org-name/repo-name/pull/90", + "id": 2000000000, + "issue_url": "https://api.github.com/repos/org-name/repo-name/issues/90", + "labels": [], + "locked": false, + "maintainer_can_modify": false, + "merge_commit_sha": "0000000000000000000000000000000000000000", + "mergeable": null, + "mergeable_state": "unknown", + "merged": false, + "merged_at": null, + "merged_by": null, + "milestone": null, + "node_id": "ANONYMIZED_NODE_ID", + "number": 90, + "patch_url": "https://github.com/org-name/repo-name/pull/90.patch", + "rebaseable": null, + "requested_reviewers": [], + "requested_teams": [], + "review_comment_url": "https://api.github.com/repos/org-name/repo-name/pulls/comments{/number}", + "review_comments": 0, + "review_comments_url": "https://api.github.com/repos/org-name/repo-name/pulls/90/comments", + "state": "open", + "statuses_url": "https://api.github.com/repos/org-name/repo-name/statuses/0000000000000000000000000000000000000000", + "title": "branch-name", + "updated_at": "2024-10-30T02:21:48Z", + "url": "https://api.github.com/repos/org-name/repo-name/pulls/90", + "user": { + "avatar_url": "https://example.com/placeholder-avatar.png", + "events_url": "https://api.github.com/users/username/events{/privacy}", + "followers_url": "https://api.github.com/users/username/followers", + "following_url": "https://api.github.com/users/username/following{/other_user}", + "gists_url": "https://api.github.com/users/username/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/username", + "id": 2000000, + "login": "username", + "node_id": "ANONYMIZED_NODE_ID", + "organizations_url": "https://api.github.com/users/username/orgs", + "received_events_url": "https://api.github.com/users/username/received_events", + "repos_url": "https://api.github.com/users/username/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/username/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/username/subscriptions", + "type": "User", + "url": "https://api.github.com/users/username", + "user_view_type": "public" + } + }, + "repository": { + "allow_forking": true, + "archive_url": "https://api.github.com/repos/org-name/repo-name/{archive_format}{/ref}", + "archived": false, + "assignees_url": "https://api.github.com/repos/org-name/repo-name/assignees{/user}", + "blobs_url": "https://api.github.com/repos/org-name/repo-name/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/org-name/repo-name/branches{/branch}", + "clone_url": "https://github.com/org-name/repo-name.git", + "collaborators_url": "https://api.github.com/repos/org-name/repo-name/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/org-name/repo-name/comments{/number}", + "commits_url": "https://api.github.com/repos/org-name/repo-name/commits{/sha}", + "compare_url": "https://api.github.com/repos/org-name/repo-name/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/org-name/repo-name/contents/{+path}", + "contributors_url": "https://api.github.com/repos/org-name/repo-name/contributors", + "created_at": "2024-08-23T22:50:51Z", + "custom_properties": {}, + "default_branch": "master", + "deployments_url": "https://api.github.com/repos/org-name/repo-name/deployments", + "description": "Example repository description", + "disabled": false, + "downloads_url": "https://api.github.com/repos/org-name/repo-name/downloads", + "events_url": "https://api.github.com/repos/org-name/repo-name/events", + "fork": false, + "forks": 0, + "forks_count": 0, + "forks_url": "https://api.github.com/repos/org-name/repo-name/forks", + "full_name": "org-name/repo-name", + "git_commits_url": "https://api.github.com/repos/org-name/repo-name/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/org-name/repo-name/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/org-name/repo-name/git/tags{/sha}", + "git_url": "git://github.com/org-name/repo-name.git", + "has_discussions": false, + "has_downloads": true, + "has_issues": true, + "has_pages": true, + "has_projects": true, + "has_wiki": true, + "homepage": "https://org-name.github.io/repo-name/", + "hooks_url": "https://api.github.com/repos/org-name/repo-name/hooks", + "html_url": "https://github.com/org-name/repo-name", + "id": 800000000, + "is_template": false, + "issue_comment_url": "https://api.github.com/repos/org-name/repo-name/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/org-name/repo-name/issues/events{/number}", + "issues_url": "https://api.github.com/repos/org-name/repo-name/issues{/number}", + "keys_url": "https://api.github.com/repos/org-name/repo-name/keys{/key_id}", + "labels_url": "https://api.github.com/repos/org-name/repo-name/labels{/name}", + "language": "Go", + "languages_url": "https://api.github.com/repos/org-name/repo-name/languages", + "license": { + "key": "apache-2.0", + "name": "Apache License 2.0", + "node_id": "MDc6TGljZW5zZTI=", + "spdx_id": "Apache-2.0", + "url": "https://api.github.com/licenses/apache-2.0" + }, + "merges_url": "https://api.github.com/repos/org-name/repo-name/merges", + "milestones_url": "https://api.github.com/repos/org-name/repo-name/milestones{/number}", + "mirror_url": null, + "name": "repo-name", + "node_id": "ANONYMIZED_NODE_ID", + "notifications_url": "https://api.github.com/repos/org-name/repo-name/notifications{?since,all,participating}", + "open_issues": 9, + "open_issues_count": 9, + "owner": { + "avatar_url": "https://example.com/placeholder-avatar.png", + "events_url": "https://api.github.com/users/org-name/events{/privacy}", + "followers_url": "https://api.github.com/users/org-name/followers", + "following_url": "https://api.github.com/users/org-name/following{/other_user}", + "gists_url": "https://api.github.com/users/org-name/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/org-name", + "id": 10000000, + "login": "org-name", + "node_id": "ANONYMIZED_NODE_ID", + "organizations_url": "https://api.github.com/users/org-name/orgs", + "received_events_url": "https://api.github.com/users/org-name/received_events", + "repos_url": "https://api.github.com/users/org-name/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/org-name/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/org-name/subscriptions", + "type": "Organization", + "url": "https://api.github.com/users/org-name", + "user_view_type": "public" + }, + "private": false, + "pulls_url": "https://api.github.com/repos/org-name/repo-name/pulls{/number}", + "pushed_at": "2024-10-30T02:21:47Z", + "releases_url": "https://api.github.com/repos/org-name/repo-name/releases{/id}", + "size": 6466, + "ssh_url": "git@github.com:org-name/repo-name.git", + "stargazers_count": 1, + "stargazers_url": "https://api.github.com/repos/org-name/repo-name/stargazers", + "statuses_url": "https://api.github.com/repos/org-name/repo-name/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/org-name/repo-name/subscribers", + "subscription_url": "https://api.github.com/repos/org-name/repo-name/subscription", + "svn_url": "https://github.com/org-name/repo-name", + "tags_url": "https://api.github.com/repos/org-name/repo-name/tags", + "teams_url": "https://api.github.com/repos/org-name/repo-name/teams", + "topics": [], + "trees_url": "https://api.github.com/repos/org-name/repo-name/git/trees{/sha}", + "updated_at": "2024-10-30T01:08:06Z", + "url": "https://api.github.com/repos/org-name/repo-name", + "visibility": "public", + "watchers": 1, + "watchers_count": 1, + "web_commit_signoff_required": false + }, + "sender": { + "avatar_url": "https://example.com/placeholder-avatar.png", + "events_url": "https://api.github.com/users/username/events{/privacy}", + "followers_url": "https://api.github.com/users/username/followers", + "following_url": "https://api.github.com/users/username/following{/other_user}", + "gists_url": "https://api.github.com/users/username/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/username", + "id": 2000000, + "login": "username", + "node_id": "ANONYMIZED_NODE_ID", + "organizations_url": "https://api.github.com/users/username/orgs", + "received_events_url": "https://api.github.com/users/username/received_events", + "repos_url": "https://api.github.com/users/username/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/username/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/username/subscriptions", + "type": "User", + "url": "https://api.github.com/users/username", + "user_view_type": "public" + } +} \ No newline at end of file diff --git a/lib/tools/git/github/testdata/event_push.json b/lib/tools/git/github/testdata/event_push.json new file mode 100644 index 00000000..6d2f9550 --- /dev/null +++ b/lib/tools/git/github/testdata/event_push.json @@ -0,0 +1,200 @@ +{ + "after": "0000000000000000000000000000000000000000", + "base_ref": null, + "before": "0000000000000000000000000000000000000000", + "commits": [ + { + "author": { + "email": "user@example.com", + "name": "Example User", + "username": "username" + }, + "committer": { + "email": "user@example.com", + "name": "Example User", + "username": "username" + }, + "distinct": true, + "id": "0000000000000000000000000000000000000000", + "message": "wip: testing", + "timestamp": "2024-10-30T17:06:21-07:00", + "tree_id": "0000000000000000000000000000000000000000", + "url": "https://github.com/org-name/repo-name/commit/0000000000000000000000000000000000000000" + } + ], + "compare": "https://github.com/org-name/repo-name/compare/0000000000000000...0000000000000000", + "created": false, + "deleted": false, + "forced": false, + "head_commit": { + "author": { + "email": "user@example.com", + "name": "Example User", + "username": "username" + }, + "committer": { + "email": "user@example.com", + "name": "Example User", + "username": "username" + }, + "distinct": true, + "id": "0000000000000000000000000000000000000000", + "message": "wip: testing", + "timestamp": "2024-10-30T17:06:21-07:00", + "tree_id": "0000000000000000000000000000000000000000", + "url": "https://github.com/org-name/repo-name/commit/0000000000000000000000000000000000000000" + }, + "organization": { + "avatar_url": "https://example.com/placeholder-avatar.png", + "description": "", + "events_url": "https://api.github.com/orgs/org-name/events", + "hooks_url": "https://api.github.com/orgs/org-name/hooks", + "id": 10000000, + "issues_url": "https://api.github.com/orgs/org-name/issues", + "login": "org-name", + "members_url": "https://api.github.com/orgs/org-name/members{/member}", + "node_id": "ANONYMIZED_NODE_ID", + "public_members_url": "https://api.github.com/orgs/org-name/public_members{/member}", + "repos_url": "https://api.github.com/orgs/org-name/repos", + "url": "https://api.github.com/orgs/org-name" + }, + "pusher": { + "email": "user@example.com", + "name": "username" + }, + "ref": "refs/heads/feature-branch", + "repository": { + "allow_forking": true, + "archive_url": "https://api.github.com/repos/org-name/repo-name/{archive_format}{/ref}", + "archived": false, + "assignees_url": "https://api.github.com/repos/org-name/repo-name/assignees{/user}", + "blobs_url": "https://api.github.com/repos/org-name/repo-name/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/org-name/repo-name/branches{/branch}", + "clone_url": "https://github.com/org-name/repo-name.git", + "collaborators_url": "https://api.github.com/repos/org-name/repo-name/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/org-name/repo-name/comments{/number}", + "commits_url": "https://api.github.com/repos/org-name/repo-name/commits{/sha}", + "compare_url": "https://api.github.com/repos/org-name/repo-name/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/org-name/repo-name/contents/{+path}", + "contributors_url": "https://api.github.com/repos/org-name/repo-name/contributors", + "created_at": 1724453451, + "custom_properties": {}, + "default_branch": "master", + "deployments_url": "https://api.github.com/repos/org-name/repo-name/deployments", + "description": "Example repository description", + "disabled": false, + "downloads_url": "https://api.github.com/repos/org-name/repo-name/downloads", + "events_url": "https://api.github.com/repos/org-name/repo-name/events", + "fork": false, + "forks": 0, + "forks_count": 0, + "forks_url": "https://api.github.com/repos/org-name/repo-name/forks", + "full_name": "org-name/repo-name", + "git_commits_url": "https://api.github.com/repos/org-name/repo-name/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/org-name/repo-name/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/org-name/repo-name/git/tags{/sha}", + "git_url": "git://github.com/org-name/repo-name.git", + "has_discussions": false, + "has_downloads": true, + "has_issues": true, + "has_pages": true, + "has_projects": true, + "has_wiki": true, + "homepage": "https://org-name.github.io/repo-name/", + "hooks_url": "https://api.github.com/repos/org-name/repo-name/hooks", + "html_url": "https://github.com/org-name/repo-name", + "id": 800000000, + "is_template": false, + "issue_comment_url": "https://api.github.com/repos/org-name/repo-name/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/org-name/repo-name/issues/events{/number}", + "issues_url": "https://api.github.com/repos/org-name/repo-name/issues{/number}", + "keys_url": "https://api.github.com/repos/org-name/repo-name/keys{/key_id}", + "labels_url": "https://api.github.com/repos/org-name/repo-name/labels{/name}", + "language": "Go", + "languages_url": "https://api.github.com/repos/org-name/repo-name/languages", + "license": { + "key": "apache-2.0", + "name": "Apache License 2.0", + "node_id": "MDc6TGljZW5zZTI=", + "spdx_id": "Apache-2.0", + "url": "https://api.github.com/licenses/apache-2.0" + }, + "master_branch": "master", + "merges_url": "https://api.github.com/repos/org-name/repo-name/merges", + "milestones_url": "https://api.github.com/repos/org-name/repo-name/milestones{/number}", + "mirror_url": null, + "name": "repo-name", + "node_id": "ANONYMIZED_NODE_ID", + "notifications_url": "https://api.github.com/repos/org-name/repo-name/notifications{?since,all,participating}", + "open_issues": 10, + "open_issues_count": 10, + "organization": "org-name", + "owner": { + "avatar_url": "https://example.com/placeholder-avatar.png", + "email": "org@example.com", + "events_url": "https://api.github.com/users/org-name/events{/privacy}", + "followers_url": "https://api.github.com/users/org-name/followers", + "following_url": "https://api.github.com/users/org-name/following{/other_user}", + "gists_url": "https://api.github.com/users/org-name/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/org-name", + "id": 10000000, + "login": "org-name", + "name": "org-name", + "node_id": "ANONYMIZED_NODE_ID", + "organizations_url": "https://api.github.com/users/org-name/orgs", + "received_events_url": "https://api.github.com/users/org-name/received_events", + "repos_url": "https://api.github.com/users/org-name/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/org-name/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/org-name/subscriptions", + "type": "Organization", + "url": "https://api.github.com/users/org-name", + "user_view_type": "public" + }, + "private": false, + "pulls_url": "https://api.github.com/repos/org-name/repo-name/pulls{/number}", + "pushed_at": 1730333186, + "releases_url": "https://api.github.com/repos/org-name/repo-name/releases{/id}", + "size": 6469, + "ssh_url": "git@github.com:org-name/repo-name.git", + "stargazers": 1, + "stargazers_count": 1, + "stargazers_url": "https://api.github.com/repos/org-name/repo-name/stargazers", + "statuses_url": "https://api.github.com/repos/org-name/repo-name/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/org-name/repo-name/subscribers", + "subscription_url": "https://api.github.com/repos/org-name/repo-name/subscription", + "svn_url": "https://github.com/org-name/repo-name", + "tags_url": "https://api.github.com/repos/org-name/repo-name/tags", + "teams_url": "https://api.github.com/repos/org-name/repo-name/teams", + "topics": [], + "trees_url": "https://api.github.com/repos/org-name/repo-name/git/trees{/sha}", + "updated_at": "2024-10-30T01:08:06Z", + "url": "https://github.com/org-name/repo-name", + "visibility": "public", + "watchers": 1, + "watchers_count": 1, + "web_commit_signoff_required": false + }, + "sender": { + "avatar_url": "https://example.com/placeholder-avatar.png", + "events_url": "https://api.github.com/users/username/events{/privacy}", + "followers_url": "https://api.github.com/users/username/followers", + "following_url": "https://api.github.com/users/username/following{/other_user}", + "gists_url": "https://api.github.com/users/username/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/username", + "id": 2000000, + "login": "username", + "node_id": "ANONYMIZED_NODE_ID", + "organizations_url": "https://api.github.com/users/username/orgs", + "received_events_url": "https://api.github.com/users/username/received_events", + "repos_url": "https://api.github.com/users/username/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/username/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/username/subscriptions", + "type": "User", + "url": "https://api.github.com/users/username", + "user_view_type": "public" + } +} \ No newline at end of file diff --git a/lib/tools/git/repo/repo.go b/lib/tools/git/repo/repo.go index 1acabe67..47f505cc 100644 --- a/lib/tools/git/repo/repo.go +++ b/lib/tools/git/repo/repo.go @@ -297,6 +297,21 @@ func (g *GitRepo) Head() (*plumbing.Reference, error) { return g.raw.Head() } +// HeadCommit returns the HEAD commit of the current branch. +func (g *GitRepo) HeadCommit() (*object.Commit, error) { + head, err := g.raw.Head() + if err != nil { + return nil, fmt.Errorf("failed to get HEAD reference: %w", err) + } + + commit, err := g.raw.CommitObject(head.Hash()) + if err != nil { + return nil, fmt.Errorf("failed to get HEAD commit: %w", err) + } + + return commit, nil +} + // Init initializes a new repository at the given path. func (g *GitRepo) Init() error { storage := filesystem.NewStorage(g.gfs.Raw(), cache.NewObjectLRUDefault()) diff --git a/lib/tools/git/tag.go b/lib/tools/git/tag.go index da2b5809..d01258d8 100644 --- a/lib/tools/git/tag.go +++ b/lib/tools/git/tag.go @@ -15,11 +15,12 @@ var ( func GetTag(r *repo.GitRepo) (string, error) { var tag string var err error - env := github.NewGithubEnv(nil) + ghr := github.NewDefaultGithubRepo(nil) - if github.InCI() { - tag = env.GetTag() - if tag == "" { + if github.InGithubActions() { + var ok bool + tag, ok = ghr.GetTag() + if !ok { return "", ErrTagNotFound } } else {