mirror of
https://github.com/goreleaser/goreleaser.git
synced 2024-12-31 01:53:50 +02:00
feat(snapcraft): manage channel upload (#2361)
By default, snap store manage "edge", "beta", "candidate" and "stable". Default to all this channels when package is in grade stable. But only on "edge" and "beta" for grade "devel". Signed-off-by: Guilhem Lettron <guilhem@barpilot.io> Co-authored-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
This commit is contained in:
parent
7f40922ea2
commit
6ecddbd7e9
@ -4,6 +4,7 @@ package snapcraft
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@ -22,6 +23,8 @@ import (
|
||||
"github.com/goreleaser/goreleaser/pkg/context"
|
||||
)
|
||||
|
||||
const releasesExtra = "releases"
|
||||
|
||||
// ErrNoSnapcraft is shown when snapcraft cannot be found in $PATH.
|
||||
var ErrNoSnapcraft = errors.New("snapcraft not present in $PATH")
|
||||
|
||||
@ -80,6 +83,14 @@ func (Pipe) Default(ctx *context.Context) error {
|
||||
if snap.NameTemplate == "" {
|
||||
snap.NameTemplate = defaultNameTemplate
|
||||
}
|
||||
if len(snap.ChannelTemplates) == 0 {
|
||||
switch snap.Grade {
|
||||
case "devel":
|
||||
snap.ChannelTemplates = []string{"edge", "beta"}
|
||||
default:
|
||||
snap.ChannelTemplates = []string{"edge", "beta", "candidate", "stable"}
|
||||
}
|
||||
}
|
||||
if len(snap.Builds) == 0 {
|
||||
for _, b := range ctx.Config.Builds {
|
||||
snap.Builds = append(snap.Builds, b.ID)
|
||||
@ -172,6 +183,11 @@ func create(ctx *context.Context, snap config.Snapcraft, arch string, binaries [
|
||||
return err
|
||||
}
|
||||
|
||||
channels, err := processChannelsTemplates(ctx, snap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// prime is the directory that then will be compressed to make the .snap package.
|
||||
folderDir := filepath.Join(ctx.Config.Dist, folder)
|
||||
primeDir := filepath.Join(folderDir, "prime")
|
||||
@ -188,8 +204,9 @@ func create(ctx *context.Context, snap config.Snapcraft, arch string, binaries [
|
||||
if file.Mode == 0 {
|
||||
file.Mode = 0o644
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(primeDir, filepath.Dir(file.Destination)), 0o755); err != nil {
|
||||
return fmt.Errorf("failed to link extra file '%s': %w", file.Source, err)
|
||||
destinationDir := filepath.Join(primeDir, filepath.Dir(file.Destination))
|
||||
if err := os.MkdirAll(destinationDir, 0o755); err != nil {
|
||||
return fmt.Errorf("failed to create directory '%s': %w", destinationDir, err)
|
||||
}
|
||||
if err := link(file.Source, filepath.Join(primeDir, file.Destination), os.FileMode(file.Mode)); err != nil {
|
||||
return fmt.Errorf("failed to link extra file '%s': %w", file.Source, err)
|
||||
@ -251,12 +268,9 @@ func create(ctx *context.Context, snap config.Snapcraft, arch string, binaries [
|
||||
WithField("dst", destBinaryPath).
|
||||
Debug("linking")
|
||||
|
||||
if err = os.Link(binary.Path, destBinaryPath); err != nil {
|
||||
if err = copyFile(binary.Path, destBinaryPath, 0o555); err != nil {
|
||||
return fmt.Errorf("failed to link binary: %w", err)
|
||||
}
|
||||
if err := os.Chmod(destBinaryPath, 0o555); err != nil {
|
||||
return fmt.Errorf("failed to change binary permissions: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// setup the apps: directive for each binary
|
||||
@ -285,13 +299,12 @@ func create(ctx *context.Context, snap config.Snapcraft, arch string, binaries [
|
||||
}
|
||||
log.WithField("src", config.Completer).
|
||||
WithField("dst", destCompleterPath).
|
||||
Debug("linking")
|
||||
if err := os.Link(config.Completer, destCompleterPath); err != nil {
|
||||
return fmt.Errorf("failed to link completer: %w", err)
|
||||
}
|
||||
if err := os.Chmod(destCompleterPath, 0o644); err != nil {
|
||||
return fmt.Errorf("failed to change completer permissions: %w", err)
|
||||
Debug("copy")
|
||||
|
||||
if err := copyFile(config.Completer, destCompleterPath, 0o644); err != nil {
|
||||
return fmt.Errorf("failed to copy completer: %w", err)
|
||||
}
|
||||
|
||||
appMetadata.Completer = config.Completer
|
||||
}
|
||||
|
||||
@ -326,6 +339,9 @@ func create(ctx *context.Context, snap config.Snapcraft, arch string, binaries [
|
||||
Goos: binaries[0].Goos,
|
||||
Goarch: binaries[0].Goarch,
|
||||
Goarm: binaries[0].Goarm,
|
||||
Extra: map[string]interface{}{
|
||||
releasesExtra: channels,
|
||||
},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@ -338,10 +354,10 @@ const (
|
||||
|
||||
func push(ctx *context.Context, snap *artifact.Artifact) error {
|
||||
log := log.WithField("snap", snap.Name)
|
||||
log.Info("pushing snap")
|
||||
// TODO: customize --release based on snap.Grade?
|
||||
releases := snap.Extra[releasesExtra].([]string)
|
||||
/* #nosec */
|
||||
cmd := exec.CommandContext(ctx, "snapcraft", "upload", "--release=stable", snap.Path)
|
||||
cmd := exec.CommandContext(ctx, "snapcraft", "upload", "--release="+strings.Join(releases, ","), snap.Path)
|
||||
log.WithField("args", cmd.Args).Info("pushing snap")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
if strings.Contains(string(out), reviewWaitMsg) || strings.Contains(string(out), humanReviewMsg) || strings.Contains(string(out), needsReviewMsg) {
|
||||
log.Warn(reviewWaitMsg)
|
||||
@ -354,7 +370,7 @@ func push(ctx *context.Context, snap *artifact.Artifact) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// walks the src, recreating dirs and hard-linking files.
|
||||
// walks the src, recreating dirs and copying files.
|
||||
func link(src, dest string, mode os.FileMode) error {
|
||||
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
@ -373,9 +389,53 @@ func link(src, dest string, mode os.FileMode) error {
|
||||
if info.IsDir() {
|
||||
return os.MkdirAll(dst, info.Mode())
|
||||
}
|
||||
if err := os.Link(path, dst); err != nil {
|
||||
return err
|
||||
if err := copyFile(path, dst, mode); err != nil {
|
||||
return fmt.Errorf("fail copy file '%s': %w", path, err)
|
||||
}
|
||||
return os.Chmod(dst, mode)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func copyFile(src, dst string, mode os.FileMode) error {
|
||||
// Open original file
|
||||
original, err := os.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fail to open '%s': %w", src, err)
|
||||
}
|
||||
defer original.Close()
|
||||
|
||||
// Create new file
|
||||
new, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fail to open '%s': %w", dst, err)
|
||||
}
|
||||
defer new.Close()
|
||||
|
||||
// This will copy
|
||||
if _, err := io.Copy(new, original); err != nil {
|
||||
return fmt.Errorf("fail to copy: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func processChannelsTemplates(ctx *context.Context, snap config.Snapcraft) ([]string, error) {
|
||||
// nolint:prealloc
|
||||
var channels []string
|
||||
for _, channeltemplate := range snap.ChannelTemplates {
|
||||
channel, err := tmpl.New(ctx).Apply(channeltemplate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute channel template '%s': %w", err, err)
|
||||
}
|
||||
if channel == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
channels = append(channels, channel)
|
||||
}
|
||||
|
||||
if len(channels) == 0 {
|
||||
return channels, errors.New("no image templates found")
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/goreleaser/goreleaser/internal/artifact"
|
||||
@ -52,25 +51,28 @@ func TestRunPipe(t *testing.T) {
|
||||
Dist: dist,
|
||||
Snapcrafts: []config.Snapcraft{
|
||||
{
|
||||
NameTemplate: "foo_{{.Arch}}",
|
||||
Summary: "test summary",
|
||||
Description: "test description",
|
||||
Publish: true,
|
||||
Builds: []string{"foo"},
|
||||
NameTemplate: "foo_{{.Arch}}",
|
||||
Summary: "test summary",
|
||||
Description: "test description",
|
||||
Publish: true,
|
||||
Builds: []string{"foo"},
|
||||
ChannelTemplates: []string{"stable"},
|
||||
},
|
||||
{
|
||||
NameTemplate: "foo_and_bar_{{.Arch}}",
|
||||
Summary: "test summary",
|
||||
Description: "test description",
|
||||
Publish: true,
|
||||
Builds: []string{"foo", "bar"},
|
||||
NameTemplate: "foo_and_bar_{{.Arch}}",
|
||||
Summary: "test summary",
|
||||
Description: "test description",
|
||||
Publish: true,
|
||||
Builds: []string{"foo", "bar"},
|
||||
ChannelTemplates: []string{"stable"},
|
||||
},
|
||||
{
|
||||
NameTemplate: "bar_{{.Arch}}",
|
||||
Summary: "test summary",
|
||||
Description: "test description",
|
||||
Publish: true,
|
||||
Builds: []string{"bar"},
|
||||
NameTemplate: "bar_{{.Arch}}",
|
||||
Summary: "test summary",
|
||||
Description: "test description",
|
||||
Publish: true,
|
||||
Builds: []string{"bar"},
|
||||
ChannelTemplates: []string{"stable"},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -92,10 +94,11 @@ func TestRunPipeInvalidNameTemplate(t *testing.T) {
|
||||
Dist: dist,
|
||||
Snapcrafts: []config.Snapcraft{
|
||||
{
|
||||
NameTemplate: "foo_{{.Arch}",
|
||||
Summary: "test summary",
|
||||
Description: "test description",
|
||||
Builds: []string{"foo"},
|
||||
NameTemplate: "foo_{{.Arch}",
|
||||
Summary: "test summary",
|
||||
Description: "test description",
|
||||
Builds: []string{"foo"},
|
||||
ChannelTemplates: []string{"stable"},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -114,13 +117,14 @@ func TestRunPipeWithName(t *testing.T) {
|
||||
Dist: dist,
|
||||
Snapcrafts: []config.Snapcraft{
|
||||
{
|
||||
NameTemplate: "foo_{{.Arch}}",
|
||||
Name: "testsnapname",
|
||||
Base: "core18",
|
||||
License: "MIT",
|
||||
Summary: "test summary",
|
||||
Description: "test description",
|
||||
Builds: []string{"foo"},
|
||||
NameTemplate: "foo_{{.Arch}}",
|
||||
Name: "testsnapname",
|
||||
Base: "core18",
|
||||
License: "MIT",
|
||||
Summary: "test summary",
|
||||
Description: "test description",
|
||||
Builds: []string{"foo"},
|
||||
ChannelTemplates: []string{"stable"},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -168,7 +172,8 @@ func TestRunPipeMetadata(t *testing.T) {
|
||||
"read": []string{"$HOME/test"},
|
||||
},
|
||||
},
|
||||
Builds: []string{"foo"},
|
||||
Builds: []string{"foo"},
|
||||
ChannelTemplates: []string{"stable"},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -227,7 +232,8 @@ func TestRunNoArguments(t *testing.T) {
|
||||
Args: "",
|
||||
},
|
||||
},
|
||||
Builds: []string{"foo"},
|
||||
Builds: []string{"foo"},
|
||||
ChannelTemplates: []string{"stable"},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -262,7 +268,8 @@ func TestCompleter(t *testing.T) {
|
||||
Completer: "testdata/foo-completer.bash",
|
||||
},
|
||||
},
|
||||
Builds: []string{"foo", "bar"},
|
||||
Builds: []string{"foo", "bar"},
|
||||
ChannelTemplates: []string{"stable"},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -299,7 +306,8 @@ func TestCommand(t *testing.T) {
|
||||
Command: "foo",
|
||||
},
|
||||
},
|
||||
Builds: []string{"foo"},
|
||||
Builds: []string{"foo"},
|
||||
ChannelTemplates: []string{"stable"},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -337,7 +345,8 @@ func TestExtraFile(t *testing.T) {
|
||||
Source: "testdata/extra-file-2.txt",
|
||||
},
|
||||
},
|
||||
Builds: []string{"foo"},
|
||||
Builds: []string{"foo"},
|
||||
ChannelTemplates: []string{"stable"},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -350,7 +359,7 @@ func TestExtraFile(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
destFile, err := os.Stat(filepath.Join(dist, "foo_amd64", "prime", "a", "b", "c", "extra-file.txt"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, inode(srcFile), inode(destFile))
|
||||
require.Equal(t, srcFile.Size(), destFile.Size())
|
||||
require.Equal(t, destFile.Mode(), os.FileMode(0o755))
|
||||
|
||||
srcFile, err = os.Stat("testdata/extra-file-2.txt")
|
||||
@ -358,7 +367,7 @@ func TestExtraFile(t *testing.T) {
|
||||
destFileWithDefaults, err := os.Stat(filepath.Join(dist, "foo_amd64", "prime", "testdata", "extra-file-2.txt"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, destFileWithDefaults.Mode(), os.FileMode(0o644))
|
||||
require.Equal(t, inode(srcFile), inode(destFileWithDefaults))
|
||||
require.Equal(t, srcFile.Size(), destFileWithDefaults.Size())
|
||||
}
|
||||
|
||||
func TestDefault(t *testing.T) {
|
||||
@ -385,6 +394,9 @@ func TestPublish(t *testing.T) {
|
||||
Goarch: "amd64",
|
||||
Goos: "linux",
|
||||
Type: artifact.PublishableSnapcraft,
|
||||
Extra: map[string]interface{}{
|
||||
releasesExtra: []string{"stable", "candidate"},
|
||||
},
|
||||
})
|
||||
err := Pipe{}.Publish(ctx)
|
||||
require.Contains(t, err.Error(), "failed to push nope.snap package")
|
||||
@ -399,6 +411,9 @@ func TestPublishSkip(t *testing.T) {
|
||||
Goarch: "amd64",
|
||||
Goos: "linux",
|
||||
Type: artifact.PublishableSnapcraft,
|
||||
Extra: map[string]interface{}{
|
||||
releasesExtra: []string{"stable"},
|
||||
},
|
||||
})
|
||||
testlib.AssertSkipped(t, Pipe{}.Publish(ctx))
|
||||
}
|
||||
@ -407,12 +422,69 @@ func TestDefaultSet(t *testing.T) {
|
||||
ctx := context.New(config.Project{
|
||||
Snapcrafts: []config.Snapcraft{
|
||||
{
|
||||
ID: "devel",
|
||||
NameTemplate: "foo",
|
||||
Grade: "devel",
|
||||
},
|
||||
{
|
||||
ID: "stable",
|
||||
NameTemplate: "bar",
|
||||
Grade: "stable",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, Pipe{}.Default(ctx))
|
||||
require.Equal(t, "foo", ctx.Config.Snapcrafts[0].NameTemplate)
|
||||
require.Equal(t, []string{"edge", "beta"}, ctx.Config.Snapcrafts[0].ChannelTemplates)
|
||||
require.Equal(t, []string{"edge", "beta", "candidate", "stable"}, ctx.Config.Snapcrafts[1].ChannelTemplates)
|
||||
}
|
||||
|
||||
func Test_processChannelsTemplates(t *testing.T) {
|
||||
ctx := &context.Context{
|
||||
Config: config.Project{
|
||||
Builds: []config.Build{
|
||||
{
|
||||
ID: "default",
|
||||
},
|
||||
},
|
||||
Snapcrafts: []config.Snapcraft{
|
||||
{
|
||||
Name: "mybin",
|
||||
ChannelTemplates: []string{
|
||||
"{{.Major}}.{{.Minor}}/stable",
|
||||
"stable",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx.SkipPublish = true
|
||||
ctx.Env = map[string]string{
|
||||
"FOO": "123",
|
||||
}
|
||||
ctx.Version = "1.0.0"
|
||||
ctx.Git = context.GitInfo{
|
||||
CurrentTag: "v1.0.0",
|
||||
Commit: "a1b2c3d4",
|
||||
}
|
||||
ctx.Semver = context.Semver{
|
||||
Major: 1,
|
||||
Minor: 0,
|
||||
Patch: 0,
|
||||
}
|
||||
|
||||
require.NoError(t, Pipe{}.Default(ctx))
|
||||
|
||||
snap := ctx.Config.Snapcrafts[0]
|
||||
require.Equal(t, "mybin", snap.Name)
|
||||
|
||||
channels, err := processChannelsTemplates(ctx, snap)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{
|
||||
"1.0/stable",
|
||||
"stable",
|
||||
}, channels)
|
||||
}
|
||||
|
||||
func addBinaries(t *testing.T, ctx *context.Context, name, dist string) {
|
||||
@ -475,8 +547,3 @@ func Test_isValidArch(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func inode(info os.FileInfo) uint64 {
|
||||
stat := info.Sys().(*syscall.Stat_t)
|
||||
return stat.Ino
|
||||
}
|
||||
|
@ -503,18 +503,19 @@ type Snapcraft struct {
|
||||
Replacements map[string]string `yaml:",omitempty"`
|
||||
Publish bool `yaml:",omitempty"`
|
||||
|
||||
ID string `yaml:",omitempty"`
|
||||
Builds []string `yaml:",omitempty"`
|
||||
Name string `yaml:",omitempty"`
|
||||
Summary string `yaml:",omitempty"`
|
||||
Description string `yaml:",omitempty"`
|
||||
Base string `yaml:",omitempty"`
|
||||
License string `yaml:",omitempty"`
|
||||
Grade string `yaml:",omitempty"`
|
||||
Confinement string `yaml:",omitempty"`
|
||||
Layout map[string]SnapcraftLayoutMetadata `yaml:",omitempty"`
|
||||
Apps map[string]SnapcraftAppMetadata `yaml:",omitempty"`
|
||||
Plugs map[string]interface{} `yaml:",omitempty"`
|
||||
ID string `yaml:",omitempty"`
|
||||
Builds []string `yaml:",omitempty"`
|
||||
Name string `yaml:",omitempty"`
|
||||
Summary string `yaml:",omitempty"`
|
||||
Description string `yaml:",omitempty"`
|
||||
Base string `yaml:",omitempty"`
|
||||
License string `yaml:",omitempty"`
|
||||
Grade string `yaml:",omitempty"`
|
||||
ChannelTemplates []string `yaml:"channel_templates,omitempty"`
|
||||
Confinement string `yaml:",omitempty"`
|
||||
Layout map[string]SnapcraftLayoutMetadata `yaml:",omitempty"`
|
||||
Apps map[string]SnapcraftAppMetadata `yaml:",omitempty"`
|
||||
Plugs map[string]interface{} `yaml:",omitempty"`
|
||||
|
||||
Files []SnapcraftExtraFiles `yaml:"extra_files,omitempty"`
|
||||
}
|
||||
|
@ -60,12 +60,27 @@ snapcrafts:
|
||||
# store.
|
||||
description: This is the best drum roll application out there. Install it and awe!
|
||||
|
||||
# Channels in store where snap will be pushed.
|
||||
# Default depends on grade:
|
||||
# * `stable` = ["edge", "beta", "candidate", "stable"]
|
||||
# * `devel` = ["edge", "beta"]
|
||||
# More info about channels here:
|
||||
# https://snapcraft.io/docs/reference/channels
|
||||
channel_templates:
|
||||
- edge
|
||||
- beta
|
||||
- candidate
|
||||
- stable
|
||||
- {{ .Major }}.{{ .Minor }}/edge
|
||||
- {{ .Major }}.{{ .Minor }}/beta
|
||||
- {{ .Major }}.{{ .Minor }}/candidate
|
||||
- {{ .Major }}.{{ .Minor }}/stable
|
||||
|
||||
# A guardrail to prevent you from releasing a snap to all your users before
|
||||
# it is ready.
|
||||
# `devel` will let you release only to the `edge` and `beta` channels in the
|
||||
# store. `stable` will let you release also to the `candidate` and `stable`
|
||||
# channels. More info about channels here:
|
||||
# https://snapcraft.io/docs/reference/channels
|
||||
# channels.
|
||||
grade: stable
|
||||
|
||||
# Snaps can be setup to follow three different confinement policies:
|
||||
|
Loading…
Reference in New Issue
Block a user