Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds Timoni release type #97

Merged
merged 20 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ inputs:
description: If true, skip authenticating to GitHub Container Registry
required: false
default: "false"
skip_timoni:
description: If true, skips installing Timoni CLI if the provider is configured
required: false
default: "false"

runs:
using: composite
Expand Down Expand Up @@ -169,4 +173,28 @@ runs:
if: steps.earthly.outputs.token != '' && steps.earthly.conclusion == 'success'
shell: bash
run: |
earthly org select "${{ steps.earthly.outputs.org }}"
earthly org select "${{ steps.earthly.outputs.org }}"

# Timoni Provider
- name: Get Timoni provider configuration
id: timoni
if: inputs.skip_timoni == 'false'
shell: bash
run: |
echo "==== Timoni Setup ====="
BP=$(forge dump .)

TIMONI=$(echo "$BP" | jq -r .global.ci.providers.timoni.install)
if [[ "$TIMONI" == "true" ]]; then
INSTALL=1
VERSION=$(echo "$BP" | jq -r .global.ci.providers.timoni.version)
echo "install=$INSTALL" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
else
echo "Not installing Timoni CLI"
fi
- name: Install Timoni
uses: stefanprodan/timoni/actions/setup@main
if: steps.timoni.outputs.install && steps.timoni.conclusion == 'success'
with:
version: ${{ steps.timoni.outputs.version }}
4 changes: 0 additions & 4 deletions cli/pkg/release/providers/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ import (
"github.com/spf13/afero"
)

type GithubClient interface {
RepositoriesGetReleaseByTag(ctx context.Context, owner, repo, tag string) (*github.RepositoryRelease, *github.Response, error)
}

type GithubReleaserConfig struct {
Prefix string `json:"prefix"`
Name string `json:"name"`
Expand Down
107 changes: 107 additions & 0 deletions cli/pkg/release/providers/timoni.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package providers

import (
"fmt"
"log/slog"

"github.com/input-output-hk/catalyst-forge/cli/pkg/events"
"github.com/input-output-hk/catalyst-forge/cli/pkg/executor"
"github.com/input-output-hk/catalyst-forge/cli/pkg/run"
"github.com/input-output-hk/catalyst-forge/lib/project/project"
"github.com/input-output-hk/catalyst-forge/lib/project/schema"
)

const (
TIMONI_BINARY = "timoni"
)

type TimoniReleaserConfig struct {
Container string `json:"container"`
Tag string `json:"tag"`
}

type TimoniReleaser struct {
config TimoniReleaserConfig
force bool
handler events.EventHandler
logger *slog.Logger
project project.Project
release schema.Release
releaseName string
timoni executor.WrappedExecuter
}

func (r *TimoniReleaser) Release() error {
if !r.handler.Firing(&r.project, r.project.GetReleaseEvents(r.releaseName)) && !r.force {
r.logger.Info("No release event is firing, skipping release")
return nil
}

registries := r.project.Blueprint.Global.CI.Providers.Timoni.Registries
if len(registries) == 0 {
return fmt.Errorf("must specify at least one Timoni registry")
}

container := r.config.Container
if container == "" {
r.logger.Debug("Defaulting container name")
container = fmt.Sprintf("%s-%s", r.project.Name, "deployment")
}

tag := r.config.Tag
if tag == "" {
return fmt.Errorf("no tag specified")
}

for _, registry := range registries {
fullContainer := fmt.Sprintf("oci://%s/%s", registry, container)
path, err := r.project.GetRelativePath()
if err != nil {
return fmt.Errorf("failed to get relative path: %w", err)
}

r.logger.Info("Publishing module", "path", path, "container", fullContainer, "tag", tag)
out, err := r.timoni.Execute("mod", "push", "--version", tag, "--latest=false", path, fullContainer)
if err != nil {
r.logger.Error("Failed to push module", "module", fullContainer, "error", err, "output", string(out))
return fmt.Errorf("failed to push module: %w", err)
}
}

return nil
}

// NewTimoniReleaser creates a new Timoni release provider.
func NewTimoniReleaser(ctx run.RunContext,
project project.Project,
name string,
force bool,
) (*TimoniReleaser, error) {
release, ok := project.Blueprint.Project.Release[name]
if !ok {
return nil, fmt.Errorf("unknown release: %s", name)
}

exec := executor.NewLocalExecutor(ctx.Logger)
if _, ok := exec.LookPath(TIMONI_BINARY); ok != nil {
return nil, fmt.Errorf("failed to find Timoni binary: %w", ok)
}

var config TimoniReleaserConfig
if err := parseConfig(&project, name, &config); err != nil {
return nil, fmt.Errorf("failed to parse release config: %w", err)
}

timoni := executor.NewLocalWrappedExecutor(exec, "timoni")
handler := events.NewDefaultEventHandler(ctx.Logger)
return &TimoniReleaser{
config: config,
force: force,
handler: &handler,
logger: ctx.Logger,
project: project,
release: release,
releaseName: name,
timoni: timoni,
}, nil
}
137 changes: 137 additions & 0 deletions cli/pkg/release/providers/timoni_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package providers

import (
"testing"

"github.com/input-output-hk/catalyst-forge/lib/project/project"
"github.com/input-output-hk/catalyst-forge/lib/project/schema"
"github.com/input-output-hk/catalyst-forge/lib/tools/testutils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestTimoniReleaserRelease(t *testing.T) {
newProject := func(
name string,
registries []string,
) project.Project {
return project.Project{
Name: name,
Blueprint: schema.Blueprint{
Global: schema.Global{
CI: schema.GlobalCI{
Providers: schema.Providers{
Timoni: schema.TimoniProvider{
Registries: registries,
},
},
},
},
},
}
}

tests := []struct {
name string
project project.Project
release schema.Release
config TimoniReleaserConfig
firing bool
force bool
failOn string
validate func(t *testing.T, calls []string, err error)
}{
{
name: "full",
project: newProject("test", []string{"test.com"}),
release: schema.Release{},
config: TimoniReleaserConfig{
Container: "test",
Tag: "test",
},
firing: true,
force: false,
failOn: "",
validate: func(t *testing.T, calls []string, err error) {
require.NoError(t, err)
assert.Contains(t, calls, "mod push --version test --latest=false . oci://test.com/test")
},
},
{
name: "no container",
project: newProject("test", []string{"test.com"}),
release: schema.Release{},
config: TimoniReleaserConfig{
Tag: "test",
},
firing: true,
force: false,
failOn: "",
validate: func(t *testing.T, calls []string, err error) {
require.NoError(t, err)
assert.Contains(t, calls, "mod push --version test --latest=false . oci://test.com/test-deployment")
},
},
{
name: "not firing",
project: newProject("test", []string{"test.com"}),
firing: false,
force: false,
failOn: "",
validate: func(t *testing.T, calls []string, err error) {
require.NoError(t, err)
assert.Len(t, calls, 0)
},
},
{
name: "forced",
project: newProject("test", []string{"test.com"}),
release: schema.Release{},
config: TimoniReleaserConfig{
Container: "test",
Tag: "test",
},
firing: false,
force: true,
failOn: "",
validate: func(t *testing.T, calls []string, err error) {
require.NoError(t, err)
assert.Contains(t, calls, "mod push --version test --latest=false . oci://test.com/test")
},
},
{
name: "push fails",
project: newProject("test", []string{"test.com"}),
release: schema.Release{},
config: TimoniReleaserConfig{
Container: "test",
Tag: "test",
},
firing: true,
force: false,
failOn: "mod push",
validate: func(t *testing.T, calls []string, err error) {
require.Error(t, err)
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var calls []string
timoni := TimoniReleaser{
config: tt.config,
force: tt.force,
handler: newReleaseEventHandlerMock(tt.firing),
logger: testutils.NewNoopLogger(),
project: tt.project,
release: tt.release,
timoni: newWrappedExecuterMock(&calls, tt.failOn),
}

err := timoni.Release()

tt.validate(t, calls, err)
})
}
}
4 changes: 4 additions & 0 deletions cli/pkg/release/releaser.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type ReleaserType string
const (
ReleaserTypeDocker ReleaserType = "docker"
ReleaserTypeGithub ReleaserType = "github"
ReleaserTypeTimoni ReleaserType = "timoni"
)

type Releaser interface {
Expand Down Expand Up @@ -49,6 +50,9 @@ func NewDefaultReleaserStore() *ReleaserStore {
ReleaserTypeGithub: func(ctx run.RunContext, project project.Project, name string, force bool) (Releaser, error) {
return providers.NewGithubReleaser(ctx, project, name, force)
},
ReleaserTypeTimoni: func(ctx run.RunContext, project project.Project, name string, force bool) (Releaser, error) {
return providers.NewTimoniReleaser(ctx, project, name, force)
},
},
}
}
40 changes: 30 additions & 10 deletions lib/project/schema/_embed/schema.cue
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ package schema
// Github contains the configuration for the Github provider.
// +optional
github?: #ProviderGithub @go(Github)

// Timoni contains the configuration for the Timoni provider.
// +optional
timoni?: #TimoniProvider @go(Timoni)
}

// ProviderAWS contains the configuration for the AWS provider.
Expand Down Expand Up @@ -120,6 +124,17 @@ package schema
// +optional
credentials?: null | #Secret @go(Credentials,*Secret)
}

// ProviderGithub contains the configuration for the Github provider.
#ProviderGithub: {
// Credentials contains the credentials to use for Github
// +optional
credentials?: #Secret @go(Credentials)

// Registry contains the Github registry to use.
// +optional
registry?: null | string @go(Registry,*string)
}
#TagStrategy: string
#enumTagStrategy: #TagStrategyGitCommit
#TagStrategyGitCommit: #TagStrategy & {
Expand Down Expand Up @@ -232,6 +247,9 @@ version: "1.0"
// +optional
target?: string @go(Target)
}
#Tagging: {
strategy: "commit"
}
#GlobalRepo: {
// Name contains the name of the repository (e.g. "owner/repo-name").
name: string @go(Name)
Expand Down Expand Up @@ -265,15 +283,20 @@ version: "1.0"
secrets?: [...#Secret] @go(Secrets,[]Secret)
}

// ProviderGithub contains the configuration for the Github provider.
#ProviderGithub: {
// Credentials contains the credentials to use for Github
// +optional
credentials?: #Secret @go(Credentials)
// TimoniProvider contains the configuration for the Timoni provider.
#TimoniProvider: {
// Install contains whether to install Timoni in the CI environment.
// +optional
install: (null | bool) & (_ | *true) @go(Install,*bool)

// Registry contains the Github registry to use.
// Registries contains the registries to use for publishing Timoni modules
registries: [...string] @go(Registries,[]string)

// The version of Timoni to use in CI.
// +optional
registry?: null | string @go(Registry,*string)
version: (_ | *"latest") & {
string
} @go(Version)
}

// Secret contains the secret provider and a list of mappings
Expand All @@ -300,6 +323,3 @@ version: "1.0"
// Provider contains the provider to use for the secret.
provider: string @go(Provider)
}
#Tagging: {
strategy: "commit"
}
Loading