1
0
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:
Guilhem Lettron 2021-07-24 05:23:59 +02:00 committed by GitHub
parent 7f40922ea2
commit 6ecddbd7e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 215 additions and 72 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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"`
}

View File

@ -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: