Skip to content

Commit

Permalink
feat: adds Timoni release type (#97)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmgilman authored Nov 26, 2024
1 parent 3fe776b commit a47a738
Show file tree
Hide file tree
Showing 9 changed files with 348 additions and 15 deletions.
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

0 comments on commit a47a738

Please sign in to comment.