Skip to content

Commit a47a738

Browse files
authored
feat: adds Timoni release type (#97)
1 parent 3fe776b commit a47a738

File tree

9 files changed

+348
-15
lines changed

9 files changed

+348
-15
lines changed

actions/setup/action.yml

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ inputs:
1919
description: If true, skip authenticating to GitHub Container Registry
2020
required: false
2121
default: "false"
22+
skip_timoni:
23+
description: If true, skips installing Timoni CLI if the provider is configured
24+
required: false
25+
default: "false"
2226

2327
runs:
2428
using: composite
@@ -169,4 +173,28 @@ runs:
169173
if: steps.earthly.outputs.token != '' && steps.earthly.conclusion == 'success'
170174
shell: bash
171175
run: |
172-
earthly org select "${{ steps.earthly.outputs.org }}"
176+
earthly org select "${{ steps.earthly.outputs.org }}"
177+
178+
# Timoni Provider
179+
- name: Get Timoni provider configuration
180+
id: timoni
181+
if: inputs.skip_timoni == 'false'
182+
shell: bash
183+
run: |
184+
echo "==== Timoni Setup ====="
185+
BP=$(forge dump .)
186+
187+
TIMONI=$(echo "$BP" | jq -r .global.ci.providers.timoni.install)
188+
if [[ "$TIMONI" == "true" ]]; then
189+
INSTALL=1
190+
VERSION=$(echo "$BP" | jq -r .global.ci.providers.timoni.version)
191+
echo "install=$INSTALL" >> $GITHUB_OUTPUT
192+
echo "version=$VERSION" >> $GITHUB_OUTPUT
193+
else
194+
echo "Not installing Timoni CLI"
195+
fi
196+
- name: Install Timoni
197+
uses: stefanprodan/timoni/actions/setup@main
198+
if: steps.timoni.outputs.install && steps.timoni.conclusion == 'success'
199+
with:
200+
version: ${{ steps.timoni.outputs.version }}

cli/pkg/release/providers/github.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@ import (
1818
"github.com/spf13/afero"
1919
)
2020

21-
type GithubClient interface {
22-
RepositoriesGetReleaseByTag(ctx context.Context, owner, repo, tag string) (*github.RepositoryRelease, *github.Response, error)
23-
}
24-
2521
type GithubReleaserConfig struct {
2622
Prefix string `json:"prefix"`
2723
Name string `json:"name"`
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package providers
2+
3+
import (
4+
"fmt"
5+
"log/slog"
6+
7+
"github.com/input-output-hk/catalyst-forge/cli/pkg/events"
8+
"github.com/input-output-hk/catalyst-forge/cli/pkg/executor"
9+
"github.com/input-output-hk/catalyst-forge/cli/pkg/run"
10+
"github.com/input-output-hk/catalyst-forge/lib/project/project"
11+
"github.com/input-output-hk/catalyst-forge/lib/project/schema"
12+
)
13+
14+
const (
15+
TIMONI_BINARY = "timoni"
16+
)
17+
18+
type TimoniReleaserConfig struct {
19+
Container string `json:"container"`
20+
Tag string `json:"tag"`
21+
}
22+
23+
type TimoniReleaser struct {
24+
config TimoniReleaserConfig
25+
force bool
26+
handler events.EventHandler
27+
logger *slog.Logger
28+
project project.Project
29+
release schema.Release
30+
releaseName string
31+
timoni executor.WrappedExecuter
32+
}
33+
34+
func (r *TimoniReleaser) Release() error {
35+
if !r.handler.Firing(&r.project, r.project.GetReleaseEvents(r.releaseName)) && !r.force {
36+
r.logger.Info("No release event is firing, skipping release")
37+
return nil
38+
}
39+
40+
registries := r.project.Blueprint.Global.CI.Providers.Timoni.Registries
41+
if len(registries) == 0 {
42+
return fmt.Errorf("must specify at least one Timoni registry")
43+
}
44+
45+
container := r.config.Container
46+
if container == "" {
47+
r.logger.Debug("Defaulting container name")
48+
container = fmt.Sprintf("%s-%s", r.project.Name, "deployment")
49+
}
50+
51+
tag := r.config.Tag
52+
if tag == "" {
53+
return fmt.Errorf("no tag specified")
54+
}
55+
56+
for _, registry := range registries {
57+
fullContainer := fmt.Sprintf("oci://%s/%s", registry, container)
58+
path, err := r.project.GetRelativePath()
59+
if err != nil {
60+
return fmt.Errorf("failed to get relative path: %w", err)
61+
}
62+
63+
r.logger.Info("Publishing module", "path", path, "container", fullContainer, "tag", tag)
64+
out, err := r.timoni.Execute("mod", "push", "--version", tag, "--latest=false", path, fullContainer)
65+
if err != nil {
66+
r.logger.Error("Failed to push module", "module", fullContainer, "error", err, "output", string(out))
67+
return fmt.Errorf("failed to push module: %w", err)
68+
}
69+
}
70+
71+
return nil
72+
}
73+
74+
// NewTimoniReleaser creates a new Timoni release provider.
75+
func NewTimoniReleaser(ctx run.RunContext,
76+
project project.Project,
77+
name string,
78+
force bool,
79+
) (*TimoniReleaser, error) {
80+
release, ok := project.Blueprint.Project.Release[name]
81+
if !ok {
82+
return nil, fmt.Errorf("unknown release: %s", name)
83+
}
84+
85+
exec := executor.NewLocalExecutor(ctx.Logger)
86+
if _, ok := exec.LookPath(TIMONI_BINARY); ok != nil {
87+
return nil, fmt.Errorf("failed to find Timoni binary: %w", ok)
88+
}
89+
90+
var config TimoniReleaserConfig
91+
if err := parseConfig(&project, name, &config); err != nil {
92+
return nil, fmt.Errorf("failed to parse release config: %w", err)
93+
}
94+
95+
timoni := executor.NewLocalWrappedExecutor(exec, "timoni")
96+
handler := events.NewDefaultEventHandler(ctx.Logger)
97+
return &TimoniReleaser{
98+
config: config,
99+
force: force,
100+
handler: &handler,
101+
logger: ctx.Logger,
102+
project: project,
103+
release: release,
104+
releaseName: name,
105+
timoni: timoni,
106+
}, nil
107+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package providers
2+
3+
import (
4+
"testing"
5+
6+
"github.com/input-output-hk/catalyst-forge/lib/project/project"
7+
"github.com/input-output-hk/catalyst-forge/lib/project/schema"
8+
"github.com/input-output-hk/catalyst-forge/lib/tools/testutils"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestTimoniReleaserRelease(t *testing.T) {
14+
newProject := func(
15+
name string,
16+
registries []string,
17+
) project.Project {
18+
return project.Project{
19+
Name: name,
20+
Blueprint: schema.Blueprint{
21+
Global: schema.Global{
22+
CI: schema.GlobalCI{
23+
Providers: schema.Providers{
24+
Timoni: schema.TimoniProvider{
25+
Registries: registries,
26+
},
27+
},
28+
},
29+
},
30+
},
31+
}
32+
}
33+
34+
tests := []struct {
35+
name string
36+
project project.Project
37+
release schema.Release
38+
config TimoniReleaserConfig
39+
firing bool
40+
force bool
41+
failOn string
42+
validate func(t *testing.T, calls []string, err error)
43+
}{
44+
{
45+
name: "full",
46+
project: newProject("test", []string{"test.com"}),
47+
release: schema.Release{},
48+
config: TimoniReleaserConfig{
49+
Container: "test",
50+
Tag: "test",
51+
},
52+
firing: true,
53+
force: false,
54+
failOn: "",
55+
validate: func(t *testing.T, calls []string, err error) {
56+
require.NoError(t, err)
57+
assert.Contains(t, calls, "mod push --version test --latest=false . oci://test.com/test")
58+
},
59+
},
60+
{
61+
name: "no container",
62+
project: newProject("test", []string{"test.com"}),
63+
release: schema.Release{},
64+
config: TimoniReleaserConfig{
65+
Tag: "test",
66+
},
67+
firing: true,
68+
force: false,
69+
failOn: "",
70+
validate: func(t *testing.T, calls []string, err error) {
71+
require.NoError(t, err)
72+
assert.Contains(t, calls, "mod push --version test --latest=false . oci://test.com/test-deployment")
73+
},
74+
},
75+
{
76+
name: "not firing",
77+
project: newProject("test", []string{"test.com"}),
78+
firing: false,
79+
force: false,
80+
failOn: "",
81+
validate: func(t *testing.T, calls []string, err error) {
82+
require.NoError(t, err)
83+
assert.Len(t, calls, 0)
84+
},
85+
},
86+
{
87+
name: "forced",
88+
project: newProject("test", []string{"test.com"}),
89+
release: schema.Release{},
90+
config: TimoniReleaserConfig{
91+
Container: "test",
92+
Tag: "test",
93+
},
94+
firing: false,
95+
force: true,
96+
failOn: "",
97+
validate: func(t *testing.T, calls []string, err error) {
98+
require.NoError(t, err)
99+
assert.Contains(t, calls, "mod push --version test --latest=false . oci://test.com/test")
100+
},
101+
},
102+
{
103+
name: "push fails",
104+
project: newProject("test", []string{"test.com"}),
105+
release: schema.Release{},
106+
config: TimoniReleaserConfig{
107+
Container: "test",
108+
Tag: "test",
109+
},
110+
firing: true,
111+
force: false,
112+
failOn: "mod push",
113+
validate: func(t *testing.T, calls []string, err error) {
114+
require.Error(t, err)
115+
},
116+
},
117+
}
118+
119+
for _, tt := range tests {
120+
t.Run(tt.name, func(t *testing.T) {
121+
var calls []string
122+
timoni := TimoniReleaser{
123+
config: tt.config,
124+
force: tt.force,
125+
handler: newReleaseEventHandlerMock(tt.firing),
126+
logger: testutils.NewNoopLogger(),
127+
project: tt.project,
128+
release: tt.release,
129+
timoni: newWrappedExecuterMock(&calls, tt.failOn),
130+
}
131+
132+
err := timoni.Release()
133+
134+
tt.validate(t, calls, err)
135+
})
136+
}
137+
}

cli/pkg/release/releaser.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type ReleaserType string
1313
const (
1414
ReleaserTypeDocker ReleaserType = "docker"
1515
ReleaserTypeGithub ReleaserType = "github"
16+
ReleaserTypeTimoni ReleaserType = "timoni"
1617
)
1718

1819
type Releaser interface {
@@ -49,6 +50,9 @@ func NewDefaultReleaserStore() *ReleaserStore {
4950
ReleaserTypeGithub: func(ctx run.RunContext, project project.Project, name string, force bool) (Releaser, error) {
5051
return providers.NewGithubReleaser(ctx, project, name, force)
5152
},
53+
ReleaserTypeTimoni: func(ctx run.RunContext, project project.Project, name string, force bool) (Releaser, error) {
54+
return providers.NewTimoniReleaser(ctx, project, name, force)
55+
},
5256
},
5357
}
5458
}

lib/project/schema/_embed/schema.cue

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ package schema
7474
// Github contains the configuration for the Github provider.
7575
// +optional
7676
github?: #ProviderGithub @go(Github)
77+
78+
// Timoni contains the configuration for the Timoni provider.
79+
// +optional
80+
timoni?: #TimoniProvider @go(Timoni)
7781
}
7882

7983
// ProviderAWS contains the configuration for the AWS provider.
@@ -120,6 +124,17 @@ package schema
120124
// +optional
121125
credentials?: null | #Secret @go(Credentials,*Secret)
122126
}
127+
128+
// ProviderGithub contains the configuration for the Github provider.
129+
#ProviderGithub: {
130+
// Credentials contains the credentials to use for Github
131+
// +optional
132+
credentials?: #Secret @go(Credentials)
133+
134+
// Registry contains the Github registry to use.
135+
// +optional
136+
registry?: null | string @go(Registry,*string)
137+
}
123138
#TagStrategy: string
124139
#enumTagStrategy: #TagStrategyGitCommit
125140
#TagStrategyGitCommit: #TagStrategy & {
@@ -232,6 +247,9 @@ version: "1.0"
232247
// +optional
233248
target?: string @go(Target)
234249
}
250+
#Tagging: {
251+
strategy: "commit"
252+
}
235253
#GlobalRepo: {
236254
// Name contains the name of the repository (e.g. "owner/repo-name").
237255
name: string @go(Name)
@@ -265,15 +283,20 @@ version: "1.0"
265283
secrets?: [...#Secret] @go(Secrets,[]Secret)
266284
}
267285

268-
// ProviderGithub contains the configuration for the Github provider.
269-
#ProviderGithub: {
270-
// Credentials contains the credentials to use for Github
271-
// +optional
272-
credentials?: #Secret @go(Credentials)
286+
// TimoniProvider contains the configuration for the Timoni provider.
287+
#TimoniProvider: {
288+
// Install contains whether to install Timoni in the CI environment.
289+
// +optional
290+
install: (null | bool) & (_ | *true) @go(Install,*bool)
273291

274-
// Registry contains the Github registry to use.
292+
// Registries contains the registries to use for publishing Timoni modules
293+
registries: [...string] @go(Registries,[]string)
294+
295+
// The version of Timoni to use in CI.
275296
// +optional
276-
registry?: null | string @go(Registry,*string)
297+
version: (_ | *"latest") & {
298+
string
299+
} @go(Version)
277300
}
278301

279302
// Secret contains the secret provider and a list of mappings
@@ -300,6 +323,3 @@ version: "1.0"
300323
// Provider contains the provider to use for the secret.
301324
provider: string @go(Provider)
302325
}
303-
#Tagging: {
304-
strategy: "commit"
305-
}

0 commit comments

Comments
 (0)