Repository: goreleaser/nfpm (go)

Issue:
Ability to specify %config(missingok)
### Is your feature request related to a problem? Please describe.

It's great to have a `%config(noreplace)` when building RPMs. Now we stumbled over the need to specify `%config(missingok)`.

This is especially made for cases where the package manager is happy with the file being there or missing. Given files you only need within postInstall scripts, symlinks that vanish or others.

### Describe the solution you'd like

Would be great to simply be able to pass `type: config|missingok` to a "content".

### Describe alternatives you've considered

Writing spec files by hand and not using nfpm. Which is somewhat not a real alternative :innocent: 

### Search

- [X] I did search for other open and closed issues before opening this.

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct

### Additional context

https://rpm-software-management.github.io/rpm/manual/spec.html#missingok-since-rpm--414-in-standalone-form

Pull request description:
feat(rpm): support %config(missingok)
closes #853

Patch:
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 0c186ba..67fd0c7 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -20,7 +20,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
-      - uses: cachix/install-nix-action@V27
+      - uses: cachix/install-nix-action@V28
         with:
           nix_path: nixpkgs=channel:nixos-unstable
           github_access_token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index cb1e0fd..0a73087 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -38,7 +38,7 @@ jobs:
       - uses: anchore/sbom-action/download-syft@v0.17.2
       - uses: docker/setup-qemu-action@v3
       - uses: docker/setup-buildx-action@v3
-      - uses: cachix/install-nix-action@V27
+      - uses: cachix/install-nix-action@V28
         with:
           github_access_token: ${{ secrets.GITHUB_TOKEN }}
       - run: task setup
diff --git a/arch/arch.go b/arch/arch.go
index a35d021..a5cb3fa 100644
--- a/arch/arch.go
+++ b/arch/arch.go
@@ -359,7 +359,7 @@ func createPkginfo(info *nfpm.Info, tw *tar.Writer, totalSize int64) (*MtreeEntr
 	}
 
 	for _, content := range info.Contents {
-		if content.Type == files.TypeConfig || content.Type == files.TypeConfigNoReplace {
+		if content.Type == files.TypeConfig || content.Type == files.TypeConfigNoReplace || content.Type == files.TypeConfigMissingOK {
 			path := files.AsRelativePath(content.Destination)
 
 			if err := writeKVPair(buf, "backup", path); err != nil {
diff --git a/deb/deb.go b/deb/deb.go
index 96fc9cd..49de3b3 100644
--- a/deb/deb.go
+++ b/deb/deb.go
@@ -681,7 +681,7 @@ func conffiles(info *nfpm.Info) []byte {
 	var confs []string
 	for _, file := range info.Contents {
 		switch file.Type {
-		case files.TypeConfig, files.TypeConfigNoReplace:
+		case files.TypeConfig, files.TypeConfigNoReplace, files.TypeConfigMissingOK:
 			confs = append(confs, files.NormalizeAbsoluteFilePath(file.Destination))
 		}
 	}
diff --git a/files/files.go b/files/files.go
index 68fe6a3..cf9518d 100644
--- a/files/files.go
+++ b/files/files.go
@@ -32,9 +32,13 @@ const (
 	// user of the package.
 	TypeConfig = "config"
 	// TypeConfigNoReplace is like TypeConfig with an added noreplace directive
-	// that is respected by RPM-based distributions. For all other packages it
-	// is handled exactly like TypeConfig.
+	// that is respected by RPM-based distributions.
+	// For all other package formats it is handled exactly like TypeConfig.
 	TypeConfigNoReplace = "config|noreplace"
+	// TypeConfigMissingOK is like TypeConfig with an added missingok directive
+	// that is respected by RPM-based distributions.
+	// For all other package formats it is handled exactly like TypeConfig.
+	TypeConfigMissingOK = "config|missingok"
 	// TypeGhost is the type of an RPM ghost file which is ignored by other packagers.
 	TypeRPMGhost = "ghost"
 	// TypeRPMDoc is the type of an RPM doc file which is ignored by other packagers.
@@ -289,7 +293,7 @@ func PrepareForPackager(
 			if err != nil {
 				return nil, fmt.Errorf("add tree: %w", err)
 			}
-		case TypeConfig, TypeConfigNoReplace, TypeFile, "":
+		case TypeConfig, TypeConfigNoReplace, TypeConfigMissingOK, TypeFile, "":
 			globbed, err := glob.Glob(
 				filepath.ToSlash(content.Source),
 				filepath.ToSlash(content.Destination),
diff --git a/flake.nix b/flake.nix
index 2ef9292..0bdb5e3 100644
--- a/flake.nix
+++ b/flake.nix
@@ -43,7 +43,7 @@
 
         # nix develop .#docs
         devShells.docs = pkgs.mkShell {
-          packages = with pkgs; with staging-pkgs.python311Packages; [
+          packages = with pkgs; with pkgs.python311Packages; [
             (pkgs.writeScriptBin "ci-docs" "task docs:test")
             go-task
             htmltest
diff --git a/ipk/ipk.go b/ipk/ipk.go
index 14be0ab..5b588c0 100644
--- a/ipk/ipk.go
+++ b/ipk/ipk.go
@@ -205,7 +205,7 @@ func populateDataTar(info *nfpm.Info, tw *tar.Writer) (instSize int64, err error
 					ModTime:  modtime.Get(info.MTime),
 					Linkname: file.Source,
 				})
-		case files.TypeFile, files.TypeTree, files.TypeConfig, files.TypeConfigNoReplace:
+		case files.TypeFile, files.TypeTree, files.TypeConfig, files.TypeConfigNoReplace, files.TypeConfigMissingOK:
 			size, err = writeFile(tw, file)
 		default:
 			// ignore everything else
@@ -293,7 +293,7 @@ func conffiles(info *nfpm.Info) []byte {
 	var confs []string
 	for _, file := range info.Contents {
 		switch file.Type {
-		case files.TypeConfig, files.TypeConfigNoReplace:
+		case files.TypeConfig, files.TypeConfigNoReplace, files.TypeConfigMissingOK:
 			confs = append(confs, files.NormalizeAbsoluteFilePath(file.Destination))
 		}
 	}
diff --git a/rpm/rpm.go b/rpm/rpm.go
index 3e98b70..fed9913 100644
--- a/rpm/rpm.go
+++ b/rpm/rpm.go
@@ -373,6 +373,8 @@ func createFilesInsideRPM(info *nfpm.Info, rpm *rpmpack.RPM) (err error) {
 			file, err = asRPMFile(content, rpmpack.ConfigFile)
 		case files.TypeConfigNoReplace:
 			file, err = asRPMFile(content, rpmpack.ConfigFile|rpmpack.NoReplaceFile)
+		case files.TypeConfigMissingOK:
+			file, err = asRPMFile(content, rpmpack.ConfigFile|rpmpack.MissingOkFile)
 		case files.TypeRPMGhost:
 			if content.FileInfo.Mode == 0 {
 				content.FileInfo.Mode = os.FileMode(0o644)
diff --git a/www/docs/configuration.md b/www/docs/configuration.md
index 3d0a9e9..10bbc3c 100644
--- a/www/docs/configuration.md
+++ b/www/docs/configuration.md
@@ -209,6 +209,12 @@ contents:
     dst: /etc/bar.conf
     type: config|noreplace
 
+  # Corresponds to `%config(missingok)` if the packager is rpm, otherwise it
+  # is just a config file
+  - src: path/to/local/bar.conf
+    dst: /etc/bar.conf
+    type: config|missingok
+
   # These files are not actually present in the package, but the file names
   # are added to the package header. From the RPM directives documentation:
   #


Test patch:
diff --git a/apk/apk_test.go b/apk/apk_test.go
index a0ce0a2..5496152 100644
--- a/apk/apk_test.go
+++ b/apk/apk_test.go
@@ -81,6 +81,11 @@ func exampleInfo() *nfpm.Info {
 					Destination: "/etc/fake/fake2.conf",
 					Type:        files.TypeConfigNoReplace,
 				},
+				{
+					Source:      "../testdata/whatever.conf",
+					Destination: "/etc/fake/fake3.conf",
+					Type:        files.TypeConfigMissingOK,
+				},
 				{
 					Destination: "/var/log/whatever",
 					Type:        files.TypeDir,
diff --git a/deb/deb_test.go b/deb/deb_test.go
index d657d69..fb0a06f 100644
--- a/deb/deb_test.go
+++ b/deb/deb_test.go
@@ -85,6 +85,11 @@ func exampleInfo() *nfpm.Info {
 					Destination: "/etc/fake/fake2.conf",
 					Type:        files.TypeConfigNoReplace,
 				},
+				{
+					Source:      "../testdata/whatever.conf",
+					Destination: "/etc/fake/fake3.conf",
+					Type:        files.TypeConfigMissingOK,
+				},
 				{
 					Destination: "/var/log/whatever",
 					Type:        files.TypeDir,
diff --git a/ipk/ipk_test.go b/ipk/ipk_test.go
index 67402c5..f56262d 100644
--- a/ipk/ipk_test.go
+++ b/ipk/ipk_test.go
@@ -71,6 +71,11 @@ func exampleInfo() *nfpm.Info {
 					Destination: "/etc/fake/fake2.conf",
 					Type:        files.TypeConfigNoReplace,
 				},
+				{
+					Source:      "../testdata/whatever.conf",
+					Destination: "/etc/fake/fake3.conf",
+					Type:        files.TypeConfigNoReplace,
+				},
 				{
 					Destination: "/var/log/whatever",
 					Type:        files.TypeDir,
diff --git a/rpm/rpm_test.go b/rpm/rpm_test.go
index 7569871..d9a95f0 100644
--- a/rpm/rpm_test.go
+++ b/rpm/rpm_test.go
@@ -595,6 +595,42 @@ func TestArches(t *testing.T) {
 	})
 }
 
+func TestConfigMissingOK(t *testing.T) {
+	var (
+		buildConfigFile   = "../testdata/whatever.conf"
+		packageConfigFile = "/etc/fake/fake.conf"
+	)
+
+	info := &nfpm.Info{
+		Name:        "symlink-in-files",
+		Arch:        "amd64",
+		Description: "This package's config references a file via symlink.",
+		Version:     "1.0.0",
+		Maintainer:  "maintainer",
+		Overridables: nfpm.Overridables{
+			Contents: []*files.Content{
+				{
+					Source:      buildConfigFile,
+					Destination: packageConfigFile,
+					Type:        files.TypeConfigMissingOK,
+				},
+			},
+		},
+	}
+
+	var rpmFileBuffer bytes.Buffer
+	err := Default.Package(info, &rpmFileBuffer)
+	require.NoError(t, err)
+
+	expectedConfigContent, err := os.ReadFile(buildConfigFile)
+	require.NoError(t, err)
+
+	packageConfigContent, err := extractFileFromRpm(rpmFileBuffer.Bytes(), packageConfigFile)
+	require.NoError(t, err)
+
+	require.Equal(t, expectedConfigContent, packageConfigContent)
+}
+
 func TestConfigNoReplace(t *testing.T) {
 	var (
 		buildConfigFile   = "../testdata/whatever.conf"
diff --git a/www/docs/static/latest b/www/docs/static/latest
index 4e41fb3..8b360f7 100644
--- a/www/docs/static/latest
+++ b/www/docs/static/latest
@@ -1,1 +1,1 @@
-v2.39.0
+v2.40.0