Repository: goreleaser/nfpm
Pull request #178 (2020-07-25 15:57:52)

feat: Add support for DEB triggers.
This pull request adds support for triggers in .deb files. These triggers are commonly used for tasks like rebuilding man pages or `initramfs`. Packages can activate a trigger for other packages or claim interest on triggers activated by other packages. For further information see https://wiki.debian.org/DpkgTriggers, https://man7.org/linux/man-pages/man5/deb-triggers.5.html.

In addition to the feature support, this pull request adds **unit tests**, an **acceptance test** and **documentation**. However, the following changes probably need some feedback before merging:
* The trigger names internally use a `-` (e.g. `activate-noawait`), however I used a `_` (e.g. `activate_noawait`)for the `nfpm` configuration to adhere to yaml conventions.
* `createControl` is now 2 or 3 lines above the line limit enforced by `golangci-lint:funlen`. I think it is still perfectly readable and simple enough, so I decided to ignore the warning by adding `// nolint:funlen`. I hope that's okay.
* In the acceptance test, the package only claims interest for one trigger which is then triggered manually. Activating a trigger is not explicitly tested. The main reason is that `accept()` currently does not support building two packages for the Docker container which would be necessary to test both ways of the trigger mechanism. However, the mechanism is simple enough that I think this test is sufficient, especially since the other cases are somewhat covered by the unit test.


vercel[bot]: 
This pull request is being automatically deployed with Vercel ([learn more](https://vercel.link/github-learn-more)).
To see the status of your deployment, click on the icon next to each commit.
	
erikgeiser: Yes, fixed it. I did a last minute change to adhere to the yaml conventions and I missed the docs.
caarlos0: can you resolve the conflicts as well? other than that I think we are good to merge :) 
erikgeiser: Done.

Patch:
diff --git a/deb/deb.go b/deb/deb.go
index 1c99595..3579f1a 100644
--- a/deb/deb.go
+++ b/deb/deb.go
@@ -278,6 +278,7 @@ func formatChangelog(info *nfpm.Info) (string, error) {
 	return strings.TrimSpace(formattedChangelog) + "\n", nil
 }
 
+// nolint:funlen
 func createControl(instSize int64, md5sums []byte, info *nfpm.Info) (controlTarGz []byte, err error) {
 	var buf bytes.Buffer
 	var compress = gzip.NewWriter(&buf)
@@ -310,6 +311,11 @@ func createControl(instSize int64, md5sums []byte, info *nfpm.Info) (controlTarG
 		filesToCreate["changelog"] = []byte(changeLogData)
 	}
 
+	triggers := createTriggers(info)
+	if len(triggers) > 0 {
+		filesToCreate["triggers"] = triggers
+	}
+
 	for name, content := range filesToCreate {
 		if err := newFileInsideTarGz(out, name, content); err != nil {
 			return nil, err
@@ -430,6 +436,31 @@ func conffiles(info *nfpm.Info) []byte {
 	return []byte(strings.Join(confs, "\n") + "\n")
 }
 
+func createTriggers(info *nfpm.Info) []byte {
+	var buffer bytes.Buffer
+
+	// https://man7.org/linux/man-pages/man5/deb-triggers.5.html
+	triggerEntries := []struct {
+		Directive    string
+		TriggerNames *[]string
+	}{
+		{"interest", &info.Deb.Triggers.Interest},
+		{"interest-await", &info.Deb.Triggers.InterestAwait},
+		{"interest-noawait", &info.Deb.Triggers.InterestNoAwait},
+		{"activate", &info.Deb.Triggers.Activate},
+		{"activate-await", &info.Deb.Triggers.ActivateAwait},
+		{"activate-noawait", &info.Deb.Triggers.ActivateNoAwait},
+	}
+
+	for _, triggerEntry := range triggerEntries {
+		for _, triggerName := range *triggerEntry.TriggerNames {
+			fmt.Fprintf(&buffer, "%s %s\n", triggerEntry.Directive, triggerName)
+		}
+	}
+
+	return buffer.Bytes()
+}
+
 const controlTemplate = `
 {{- /* Mandatory fields */ -}}
 Package: {{.Info.Name}}
diff --git a/nfpm.go b/nfpm.go
index f67db71..8311964 100644
--- a/nfpm.go
+++ b/nfpm.go
@@ -167,8 +167,21 @@ type RPM struct {
 
 // Deb is custom configs that are only available on deb packages.
 type Deb struct {
-	Scripts         DebScripts `yaml:"scripts,omitempty"`
-	VersionMetadata string     `yaml:"metadata,omitempty"`
+	Scripts         DebScripts  `yaml:"scripts,omitempty"`
+	Triggers        DebTriggers `yaml:"triggers,omitempty"`
+	VersionMetadata string      `yaml:"metadata,omitempty"`
+}
+
+// DebTriggers contains triggers only available for deb packages.
+// https://wiki.debian.org/DpkgTriggers
+// https://man7.org/linux/man-pages/man5/deb-triggers.5.html
+type DebTriggers struct {
+	Interest        []string `yaml:"interest,omitempty"`
+	InterestAwait   []string `yaml:"interest_await,omitempty"`
+	InterestNoAwait []string `yaml:"interest_noawait,omitempty"`
+	Activate        []string `yaml:"activate,omitempty"`
+	ActivateAwait   []string `yaml:"activate_await,omitempty"`
+	ActivateNoAwait []string `yaml:"activate_noawait,omitempty"`
 }
 
 // DebScripts is scripts only available on deb packages.
diff --git a/www/docs/configuration.md b/www/docs/configuration.md
index 850f65b..749d42e 100644
--- a/www/docs/configuration.md
+++ b/www/docs/configuration.md
@@ -125,6 +125,17 @@ deb:
   # Custom deb rules script.
   scripts:
     rules: foo.sh
+
+  # Custom deb triggers
+  triggers:
+    # register interrest on a trigger activated by another package
+    # (also available: interest_await, interest_noawait)
+    interest:
+      - some-trigger-name
+    # activate a trigger for another package
+    # (also available: activate_await, activate_noawait)
+    activate:
+      - another-trigger-name
 ```
 
 ## Templating


Test patch:
diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go
index ebc4e76..41cf169 100644
--- a/acceptance/acceptance_test.go
+++ b/acceptance/acceptance_test.go
@@ -196,6 +196,18 @@ func TestChangelog(t *testing.T) {
 	}
 }
 
+func TestDebTriggers(t *testing.T) {
+	t.Run("triggers-deb", func(t *testing.T) {
+		t.Parallel()
+		accept(t, acceptParms{
+			Name:       "triggers-deb",
+			Conf:       "triggers.yaml",
+			Format:     "deb",
+			Dockerfile: "deb.triggers.dockerfile",
+		})
+	})
+}
+
 type acceptParms struct {
 	Name       string
 	Conf       string
diff --git a/acceptance/testdata/deb.triggers.dockerfile b/acceptance/testdata/deb.triggers.dockerfile
new file mode 100644
index 0000000..4b0c8d5
--- /dev/null
+++ b/acceptance/testdata/deb.triggers.dockerfile
@@ -0,0 +1,12 @@
+FROM ubuntu
+ARG package
+COPY ${package} /tmp/foo.deb
+RUN dpkg -i /tmp/foo.deb
+
+# simulate another package that activates the trigger
+RUN dpkg-trigger --by-package foo manual-trigger
+RUN dpkg --triggers-only foo
+
+RUN test -f /tmp/trigger-proof
+
+RUN dpkg -r foo
diff --git a/acceptance/testdata/scripts/postinstall_trigger.sh b/acceptance/testdata/scripts/postinstall_trigger.sh
new file mode 100644
index 0000000..0e69ef2
--- /dev/null
+++ b/acceptance/testdata/scripts/postinstall_trigger.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+if [ "$1" = "triggered" ] && [ "$2" = "manual-trigger" ]; then
+    echo "Ok" > /tmp/trigger-proof
+fi
diff --git a/acceptance/testdata/triggers.yaml b/acceptance/testdata/triggers.yaml
new file mode 100644
index 0000000..99a6c59
--- /dev/null
+++ b/acceptance/testdata/triggers.yaml
@@ -0,0 +1,15 @@
+name: "foo"
+arch: "amd64"
+platform: "linux"
+version: "v1.2.3-beta"
+maintainer: "Foo Bar"
+vendor: "foobar"
+license: "MIT"
+files:
+  ../testdata/fake: "/usr/local/bin/fake"
+scripts:
+  postinstall: ./testdata/scripts/postinstall_trigger.sh
+deb:
+  triggers:
+    interest:
+      - manual-trigger
diff --git a/deb/deb_test.go b/deb/deb_test.go
index 17329bb..332e0bf 100644
--- a/deb/deb_test.go
+++ b/deb/deb_test.go
@@ -496,6 +496,71 @@ func TestDebNoChangelogDataWithoutChangelogConfigured(t *testing.T) {
 	assert.EqualError(t, err, os.ErrNotExist.Error())
 }
 
+func TestDebTriggers(t *testing.T) {
+	info := &nfpm.Info{
+		Name:        "no-triggers-test",
+		Arch:        "amd64",
+		Description: "This package has multiple triggers.",
+		Version:     "1.0.0",
+		Overridables: nfpm.Overridables{
+			Deb: nfpm.Deb{
+				Triggers: nfpm.DebTriggers{
+					Interest:      []string{"trigger1", "trigger2"},
+					InterestAwait: []string{"trigger3"},
+					// InterestNoAwait omitted
+					// Activate omitted
+					ActivateAwait:   []string{"trigger4"},
+					ActivateNoAwait: []string{"trigger5", "trigger6"},
+				},
+			},
+		},
+	}
+
+	controlTarGz, err := createControl(0, []byte{}, info)
+	assert.NoError(t, err)
+
+	controlTriggers, err := extractFileFromTarGz(controlTarGz, "triggers")
+	assert.NoError(t, err)
+
+	goldenTriggers := createTriggers(info)
+
+	assert.Equal(t, string(goldenTriggers), string(controlTriggers))
+
+	// check if specified triggers are included and also that
+	// no remnants of triggers that were not specified are included
+	assert.True(t, bytes.Contains(controlTriggers,
+		[]byte("interest trigger1\n")))
+	assert.True(t, bytes.Contains(controlTriggers,
+		[]byte("interest trigger2\n")))
+	assert.True(t, bytes.Contains(controlTriggers,
+		[]byte("interest-await trigger3\n")))
+	assert.False(t, bytes.Contains(controlTriggers,
+		[]byte("interest-noawait ")))
+	assert.False(t, bytes.Contains(controlTriggers,
+		[]byte("activate ")))
+	assert.True(t, bytes.Contains(controlTriggers,
+		[]byte("activate-await trigger4\n")))
+	assert.True(t, bytes.Contains(controlTriggers,
+		[]byte("activate-noawait trigger5\n")))
+	assert.True(t, bytes.Contains(controlTriggers,
+		[]byte("activate-noawait trigger6\n")))
+}
+
+func TestDebNoTriggersInControlIfNoneProvided(t *testing.T) {
+	info := &nfpm.Info{
+		Name:        "no-triggers-test",
+		Arch:        "amd64",
+		Description: "This package has explicitly no triggers.",
+		Version:     "1.0.0",
+	}
+
+	controlTarGz, err := createControl(0, []byte{}, info)
+	assert.NoError(t, err)
+
+	_, err = extractFileFromTarGz(controlTarGz, "triggers")
+	assert.EqualError(t, err, os.ErrNotExist.Error())
+}
+
 func TestSymlinkInFiles(t *testing.T) {
 	var (
 		symlinkTarget  = "../testdata/whatever.conf"