Repository: goreleaser/goreleaser
Pull request #2955 (2022-03-04 20:24:24)

Add missing Snapcraft app metadata fields
Following up on [this discussion](https://github.com/goreleaser/goreleaser/discussions/2952), this PR adds support for missing Snapcraft app metadata fields. These fields are documented [here](https://snapcraft.io/docs/snapcraft-app-and-service-metadata).

**Note:** Curiously, Snapcraft’s [docs](https://snapcraft.io/docs/snapcraft-app-and-service-metadata) are missing a few fields (for example `aliases` and `bus-mode`) that are mentioned in Snapcraft’s [JSON schema](https://github.com/snapcore/snapcraft/blob/383c1f1ce48778916a13f361f01d8badd5b9e406/schema/snapcraft.json) definition for their yaml config. Since these fields _are_ actually usable (the tests assert this because they invoke snapcraft, read the resulting `snap.yaml` and compare the contents of the app metadata), I opted to include them in this PR.

**Note 2:** Technically out of scope for this PR, but I added some explicit yaml struct tag contents to the `config.SnapcraftAppMetadata` type definition, which should fix faulty uppercasing in the JSON Schema files ([ref](https://github.com/goreleaser/goreleaser/blob/c52f9042458924929eeef5ceeacd039719edcc46/www/docs/static/schema.json#L2264-L2296)). 

codecov[bot]: # [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2955?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) Report
> Merging [#2955](https://codecov.io/gh/goreleaser/goreleaser/pull/2955?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (b74c0a3) into [main](https://codecov.io/gh/goreleaser/goreleaser/commit/c52f9042458924929eeef5ceeacd039719edcc46?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (c52f904) will **increase** coverage by `0.04%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/goreleaser/goreleaser/pull/2955/graphs/tree.svg?width=650&height=150&src=pr&token=3bJvv6b7hN&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser)](https://codecov.io/gh/goreleaser/goreleaser/pull/2955?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser)

```diff
@@            Coverage Diff             @@
##             main    #2955      +/-   ##
==========================================
+ Coverage   84.42%   84.47%   +0.04%     
==========================================
  Files         111      111              
  Lines        8976     9002      +26     
==========================================
+ Hits         7578     7604      +26     
  Misses       1124     1124              
  Partials      274      274              
```


| [Impacted Files](https://codecov.io/gh/goreleaser/goreleaser/pull/2955?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) | Coverage Δ | |
|---|---|---|
| [pkg/config/config.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2955/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-cGtnL2NvbmZpZy9jb25maWcuZ28=) | `87.15% <ø> (ø)` | |
| [internal/pipe/snapcraft/snapcraft.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2955/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-aW50ZXJuYWwvcGlwZS9zbmFwY3JhZnQvc25hcGNyYWZ0Lmdv) | `84.50% <100.00%> (+1.56%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2955?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2955?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser). Last update [c52f904...b74c0a3](https://codecov.io/gh/goreleaser/goreleaser/pull/2955?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser).


Patch:
diff --git a/internal/pipe/snapcraft/snapcraft.go b/internal/pipe/snapcraft/snapcraft.go
index 9f8989b4..b386ed26 100644
--- a/internal/pipe/snapcraft/snapcraft.go
+++ b/internal/pipe/snapcraft/snapcraft.go
@@ -52,12 +52,39 @@ type Metadata struct {
 }
 
 // AppMetadata for the binaries that will be in the snap package.
+// See: https://snapcraft.io/docs/snapcraft-app-and-service-metadata
 type AppMetadata struct {
-	Command          string
-	Plugs            []string `yaml:",omitempty"`
-	Daemon           string   `yaml:",omitempty"`
-	Completer        string   `yaml:",omitempty"`
-	RestartCondition string   `yaml:"restart-condition,omitempty"`
+	Command string
+
+	Adapter          string                 `yaml:",omitempty"`
+	After            []string               `yaml:",omitempty"`
+	Aliases          []string               `yaml:",omitempty"`
+	Autostart        string                 `yaml:",omitempty"`
+	Before           []string               `yaml:",omitempty"`
+	BusName          string                 `yaml:"bus-name,omitempty"`
+	CommandChain     []string               `yaml:"command-chain,omitempty"`
+	CommonID         string                 `yaml:"common-id,omitempty"`
+	Completer        string                 `yaml:",omitempty"`
+	Daemon           string                 `yaml:",omitempty"`
+	Desktop          string                 `yaml:",omitempty"`
+	Environment      map[string]interface{} `yaml:",omitempty"`
+	Extensions       []string               `yaml:",omitempty"`
+	InstallMode      string                 `yaml:"install-mode,omitempty"`
+	Passthrough      map[string]interface{} `yaml:",omitempty"`
+	Plugs            []string               `yaml:",omitempty"`
+	PostStopCommand  string                 `yaml:"post-stop-command,omitempty"`
+	RefreshMode      string                 `yaml:"refresh-mode,omitempty"`
+	ReloadCommand    string                 `yaml:"reload-command,omitempty"`
+	RestartCondition string                 `yaml:"restart-condition,omitempty"`
+	RestartDelay     string                 `yaml:"restart-delay,omitempty"`
+	Slots            []string               `yaml:",omitempty"`
+	Sockets          map[string]interface{} `yaml:",omitempty"`
+	StartTimeout     string                 `yaml:"start-timeout,omitempty"`
+	StopCommand      string                 `yaml:"stop-command,omitempty"`
+	StopMode         string                 `yaml:"stop-mode,omitempty"`
+	StopTimeout      string                 `yaml:"stop-timeout,omitempty"`
+	Timer            string                 `yaml:",omitempty"`
+	WatchdogTimeout  string                 `yaml:"watchdog-timeout,omitempty"`
 }
 
 type LayoutMetadata struct {
@@ -287,9 +314,35 @@ func create(ctx *context.Context, snap config.Snapcraft, arch string, binaries [
 				command,
 				config.Args,
 			}, " ")),
-			Plugs:            config.Plugs,
+			Adapter:          config.Adapter,
+			After:            config.After,
+			Aliases:          config.Aliases,
+			Autostart:        config.Autostart,
+			Before:           config.Before,
+			BusName:          config.BusName,
+			CommandChain:     config.CommandChain,
+			CommonID:         config.CommonID,
+			Completer:        config.Completer,
 			Daemon:           config.Daemon,
+			Desktop:          config.Desktop,
+			Environment:      config.Environment,
+			Extensions:       config.Extensions,
+			InstallMode:      config.InstallMode,
+			Passthrough:      config.Passthrough,
+			Plugs:            config.Plugs,
+			PostStopCommand:  config.PostStopCommand,
+			RefreshMode:      config.RefreshMode,
+			ReloadCommand:    config.ReloadCommand,
 			RestartCondition: config.RestartCondition,
+			RestartDelay:     config.RestartDelay,
+			Slots:            config.Slots,
+			Sockets:          config.Sockets,
+			StartTimeout:     config.StartTimeout,
+			StopCommand:      config.StopCommand,
+			StopMode:         config.StopMode,
+			StopTimeout:      config.StopTimeout,
+			Timer:            config.Timer,
+			WatchdogTimeout:  config.WatchdogTimeout,
 		}
 
 		if config.Completer != "" {
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 8aef1b8b..e00220e2 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -688,12 +688,38 @@ type Sign struct {
 
 // SnapcraftAppMetadata for the binaries that will be in the snap package.
 type SnapcraftAppMetadata struct {
-	Plugs            []string
-	Daemon           string
-	Args             string
-	Completer        string `yaml:"completer,omitempty"`
-	Command          string `yaml:"command"`
-	RestartCondition string `yaml:"restart_condition,omitempty"`
+	Command string `yaml:"command"`
+	Args    string `yaml:"args,omitempty"`
+
+	Adapter          string                 `yaml:"adapter,omitempty"`
+	After            []string               `yaml:"after,omitempty"`
+	Aliases          []string               `yaml:"aliases,omitempty"`
+	Autostart        string                 `yaml:"autostart,omitempty"`
+	Before           []string               `yaml:"before,omitempty"`
+	BusName          string                 `yaml:"bus_name,omitempty"`
+	CommandChain     []string               `yaml:"command_chain,omitempty"`
+	CommonID         string                 `yaml:"common_id,omitempty"`
+	Completer        string                 `yaml:"completer,omitempty"`
+	Daemon           string                 `yaml:"daemon,omitempty"`
+	Desktop          string                 `yaml:"desktop,omitempty"`
+	Environment      map[string]interface{} `yaml:"environment,omitempty"`
+	Extensions       []string               `yaml:"extensions,omitempty"`
+	InstallMode      string                 `yaml:"install_mode,omitempty"`
+	Passthrough      map[string]interface{} `yaml:"passthrough,omitempty"`
+	Plugs            []string               `yaml:"plugs,omitempty"`
+	PostStopCommand  string                 `yaml:"post_stop_command,omitempty"`
+	RefreshMode      string                 `yaml:"refresh_mode,omitempty"`
+	ReloadCommand    string                 `yaml:"reload_command,omitempty"`
+	RestartCondition string                 `yaml:"restart_condition,omitempty"`
+	RestartDelay     string                 `yaml:"restart_delay,omitempty"`
+	Slots            []string               `yaml:"slots,omitempty"`
+	Sockets          map[string]interface{} `yaml:"sockets,omitempty"`
+	StartTimeout     string                 `yaml:"start_timeout,omitempty"`
+	StopCommand      string                 `yaml:"stop_command,omitempty"`
+	StopMode         string                 `yaml:"stop_mode,omitempty"`
+	StopTimeout      string                 `yaml:"stop_timeout,omitempty"`
+	Timer            string                 `yaml:"timer,omitempty"`
+	WatchdogTimeout  string                 `yaml:"watchdog_timeout,omitempty"`
 }
 
 type SnapcraftLayoutMetadata struct {
diff --git a/www/docs/customization/snapcraft.md b/www/docs/customization/snapcraft.md
index 365bb7bc..1a8e7610 100644
--- a/www/docs/customization/snapcraft.md
+++ b/www/docs/customization/snapcraft.md
@@ -146,39 +146,156 @@ snapcrafts:
 
     # Each binary built by GoReleaser is an app inside the snap. In this section
     # you can declare extra details for those binaries. It is optional.
+    # See: https://snapcraft.io/docs/snapcraft-app-and-service-metadata
     apps:
 
       # The name of the app must be the same name as the binary built or the snapcraft name.
       drumroll:
 
-        # If your app requires extra permissions to work outside of its default
-        # confined space, declare them here.
-        # You can read the documentation about the available plugs and the
-        # things they allow:
-        # https://snapcraft.io/docs/reference/interfaces.
-        plugs: ["home", "network", "personal-files"]
-
-        # If you want your app to be autostarted and to always run in the
-        # background, you can make it a simple daemon.
-        daemon: simple
-
         # If you any to pass args to your binary, you can add them with the
         # args option.
         args: --foo
 
+        # The kind of wrapper to generate for the given command.
+        # Defaults to empty.
+        adapter: none
+
+        # List of applications that are ordered to be started after the current
+        # one.
+        # Defaults to empty.
+        after: ["postdrum"]
+
+        # Aliases for the app command.
+        # Defaults to empty.
+        # https://snapcraft.io/docs/commands-and-aliases#heading--aliases
+        aliases: ["droll"]
+
+        # Defines the name of the .desktop file used to start an application
+        # with the desktop session.
+        # Defaults to empty.
+        # https://snapcraft.io/docs/snap-format#heading--autostart
+        autostart: drumroll.desktop
+
+        # List of applications that are ordered to be started before the current
+        # one.
+        # Defaults to empty.
+        before: ["predrum"]
+
+        # D-Bus name this service is reachable as. Mandatory if daemon=dbus.
+        # Defaults to empty.
+        bus_name: drumbus
+
+        # A list of commands to be executed in order before the command of this
+        # app.
+        # Defaults to empty.
+        command_chain: ["foo", "bar", "baz"]
+
+        # An identifier to a desktop-id within an external appstream file.
+        # https://snapcraft.io/docs/using-external-metadata
+        common_id: "com.example.drumroll"
+
         # Bash completion snippet. More information about completion here:
+        # Defaults to empty.
         # https://docs.snapcraft.io/tab-completion-for-snaps.
         completer: drumroll-completion.bash
 
         # You can override the command name.
-        # Defaults is the app name.
+        # Default is the app name.
         command: bin/drumroll.wrapper
 
+        # If you want your app to be autostarted and to always run in the
+        # background, you can make it a simple daemon.
+        # Defaults to empty.
+        daemon: simple
+
+        # Location of the .desktop file.
+        # Defaults to empty.
+        desktop: usr/share/applications/drumroll.desktop
+
+        # A set of key-value pairs specifying environment variables.
+        # Defaults to empty.
+        environment:
+          foo: bar
+          baz: quo
+
+        # A list of Snapcraft extensions this app depends on.
+        # Defaults to empty.
+        # https://snapcraft.io/docs/snapcraft-extensions
+        extensions: ["gnome-3-38"]
+
+        # Defines whether a freshly installed daemon is started automatically,
+        # or whether startup control is deferred to the snap.
+        # Defaults to empty. Requires `daemon` to be set.
+        install_mode: "disable"
+
+        # A set of key-value attributes passed through to snap.yaml without
+        # snapcraft validation.
+        # Defaults to empty.
+        # https://snapcraft.io/docs/using-in-development-features
+        passthrough:
+          foo: bar
+
+        # If your app requires extra permissions to work outside of its default
+        # confined space, declare them here.
+        # You can read the documentation about the available plugs and the
+        # things they allow:
+        # https://snapcraft.io/docs/reference/interfaces.
+        plugs: ["home", "network", "personal-files"]
+
+        # Sets a command to run from inside the snap after a service stops.
+        # Defaults to empty.
+        post_stop_command: foo
+
+        # Controls whether the daemon should be restarted during a snap refresh.
+        # Defaults to empty.
+        refresh_mode: endure
+
+        # Command to use to ask the service to reload its configuration.
+        # Defaults to empty. Requires `daemon` to be set.
+        reload_command: foo
+
         # Restart condition of the snap.
         # Defaults to empty.
         # https://snapcraft.io/docs/snapcraft-yaml-reference
         restart_condition: "always"
 
+        # List of slots for interfaces to connect to.
+        # Defaults to empty.
+        slots: ["foo", "bar", "baz"]
+
+        # Maps a daemon’s sockets to services and activates them.
+        # Defaults to empty. Requires `plugs` to contain `network-bind`.
+        sockets:
+          sock:
+            listen-stream: $SNAP_COMMON/socket
+            socket-group: socket-group
+            socket-mode: 416
+
+        # Time to wait for daemon to start.
+        # Defaults to empty.
+        start_timeout: 42ms
+
+        # Command to use to stop the service.
+        # Defaults to empty. Requires `daemon` to be set.
+        stop_command: foo
+
+        # Controls how the daemon should be stopped.
+        # Defaults to empty. Requires `daemon` to be set.
+        stop_mode: sigterm
+
+        # Time to wait for daemon to stop.
+        # Defaults to empty.
+        stop_timeout: 42ms
+
+        # Schedules when, or how often, to run a service or command.
+        # Defaults to empty. Requires `daemon` to be set.
+        # https://snapcraft.io/docs/services-and-daemons
+        timer: "00:00-24:00/24"
+
+        # Declares the service watchdog timeout.
+        # Defaults to empty. Requires `plugs` to contain `daemon-notify`.
+        watchdog_timeout: 42ms
+
     # Allows plugs to be configured. Plugs like system-files and personal-files
     # require this.
     # Default is empty.


Test patch:
diff --git a/internal/pipe/snapcraft/snapcraft_test.go b/internal/pipe/snapcraft/snapcraft_test.go
index 2d3ea8f3..78fdddc4 100644
--- a/internal/pipe/snapcraft/snapcraft_test.go
+++ b/internal/pipe/snapcraft/snapcraft_test.go
@@ -165,11 +165,55 @@ func TestRunPipeMetadata(t *testing.T) {
 					"/etc/testprojectname": {Bind: "$SNAP_DATA/etc"},
 				},
 				Apps: map[string]config.SnapcraftAppMetadata{
+					"before-foo": {
+						Before:  []string{"foo"},
+						Command: "foo",
+						Daemon:  "notify",
+					},
+					"after-foo": {
+						After:   []string{"foo"},
+						Command: "foo",
+						Daemon:  "notify",
+					},
 					"foo": {
-						Plugs:            []string{"home", "network", "personal-files"},
-						Daemon:           "simple",
-						Args:             "--foo --bar",
+						Args:         "--foo --bar",
+						Adapter:      "foo_adapter",
+						Aliases:      []string{"dummy_alias"},
+						Autostart:    "foobar.desktop",
+						BusName:      "foo_busname",
+						CommandChain: []string{"foo_cmd_chain"},
+						CommonID:     "foo_common_id",
+						Completer:    "", // Separately tested in TestCompleter
+						Daemon:       "simple",
+						Desktop:      "foo_desktop",
+						Environment: map[string]interface{}{
+							"foo": "bar",
+						},
+						Extensions:  []string{"foo_extension"},
+						InstallMode: "disable",
+						Passthrough: map[string]interface{}{
+							"planet": "saturn",
+						},
+						Plugs:            []string{"home", "network", "network-bind", "personal-files"},
+						PostStopCommand:  "foo",
+						RefreshMode:      "endure",
+						ReloadCommand:    "foo",
 						RestartCondition: "always",
+						RestartDelay:     "42ms",
+						Slots:            []string{"foo_slot"},
+						Sockets: map[string]interface{}{
+							"sock": map[string]interface{}{
+								"listen-stream": "$SNAP_COMMON/socket",
+								"socket-group":  "socket-group",
+								"socket-mode":   0o640,
+							},
+						},
+						StartTimeout:    "43ms",
+						StopCommand:     "foo",
+						StopMode:        "sigterm",
+						StopTimeout:     "44ms",
+						Timer:           "00:00-24:00/24",
+						WatchdogTimeout: "45ms",
 					},
 				},
 				Plugs: map[string]interface{}{
@@ -191,14 +235,59 @@ func TestRunPipeMetadata(t *testing.T) {
 	var metadata Metadata
 	err = yaml.Unmarshal(yamlFile, &metadata)
 	require.NoError(t, err)
-	require.Equal(t, []string{"home", "network", "personal-files"}, metadata.Apps["foo"].Plugs)
-	require.Equal(t, "simple", metadata.Apps["foo"].Daemon)
-	require.Equal(t, "foo --foo --bar", metadata.Apps["foo"].Command)
-	require.Equal(t, []string{"home", "network", "personal-files"}, metadata.Apps["foo"].Plugs)
-	require.Equal(t, "simple", metadata.Apps["foo"].Daemon)
-	require.Equal(t, "foo --foo --bar", metadata.Apps["foo"].Command)
+	require.Equal(t, map[string]AppMetadata{
+		"before-foo": {
+			Before:  []string{"foo"},
+			Command: "foo",
+			Daemon:  "notify",
+		},
+		"after-foo": {
+			After:   []string{"foo"},
+			Command: "foo",
+			Daemon:  "notify",
+		},
+		"foo": {
+			Adapter:      "foo_adapter",
+			Aliases:      []string{"dummy_alias"},
+			Autostart:    "foobar.desktop",
+			BusName:      "foo_busname",
+			Command:      "foo --foo --bar",
+			CommandChain: []string{"foo_cmd_chain"},
+			CommonID:     "foo_common_id",
+			Completer:    "",
+			Daemon:       "simple",
+			Desktop:      "foo_desktop",
+			Environment: map[string]interface{}{
+				"foo": "bar",
+			},
+			Extensions:  []string{"foo_extension"},
+			InstallMode: "disable",
+			Passthrough: map[string]interface{}{
+				"planet": "saturn",
+			},
+			Plugs:            []string{"home", "network", "network-bind", "personal-files"},
+			PostStopCommand:  "foo",
+			RefreshMode:      "endure",
+			ReloadCommand:    "foo",
+			RestartCondition: "always",
+			RestartDelay:     "42ms",
+			Slots:            []string{"foo_slot"},
+			Sockets: map[string]interface{}{
+				"sock": map[interface{}]interface{}{
+					"listen-stream": "$SNAP_COMMON/socket",
+					"socket-group":  "socket-group",
+					"socket-mode":   0o640,
+				},
+			},
+			StartTimeout:    "43ms",
+			StopCommand:     "foo",
+			StopMode:        "sigterm",
+			StopTimeout:     "44ms",
+			Timer:           "00:00-24:00/24",
+			WatchdogTimeout: "45ms",
+		},
+	}, metadata.Apps)
 	require.Equal(t, map[interface{}]interface{}{"read": []interface{}{"$HOME/test"}}, metadata.Plugs["personal-files"])
-	require.Equal(t, "always", metadata.Apps["foo"].RestartCondition)
 	require.Equal(t, "$SNAP_DATA/etc", metadata.Layout["/etc/testprojectname"].Bind)
 }