Skip to content

fix(archive): files dst is never passed through the template engine - #7118

Open
VXNCXNX wants to merge 2 commits into
goreleaser:mainfrom
VXNCXNX:fix/archive-files-dst-template
Open

VXNCXNX wants to merge 2 commits into
goreleaser:mainfrom
VXNCXNX:fix/archive-files-dst-template

Conversation

@VXNCXNX

@VXNCXNX VXNCXNX commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

archives.files[].dst never goes through the template engine, so a template in it lands in the archive verbatim.

files:
  - src: "docs/*"
    dst: "docs_{{ .Os }}"
$ tar tzf dist/mytool_0.0.1_linux_amd64.tar.gz

before: docs_{{ .Os }}/README.md
after:  docs_linux/README.md

src on the same entry is templated, and internal/pipe/nfpm/nfpm.go already does ApplyAll(&content.Source, &content.Destination, ...) for the equivalent contents entry, as does internal/pipe/srpm/srpm.go. This makes dst behave like its own src and like its nfpm counterpart.

Two things worth flagging before review rather than after.

A dst containing a literal {{ now errors instead of passing through. Nothing else does: Windows paths, $, backticks and single braces all still work, since Go templates only react to {{. The escape hatch is the usual one, dst: '{{ "{{" }}literal{{ "}}" }}' gives a directory named {{literal}}, which I ran.

The template context is not the same at every call site. internal/pipe/sourcearchive passes a bare tmpl.New(ctx), and internal/pipe/archive only attaches an artifact when the archive has binaries, so .Os is unavailable in source.files and in meta: true archives. That is not new: src already fails there the same way today, which I checked before writing this.

SRC with {{ .Os }}  -> map has no entry for key "Os"      (before this change)
DST with {{ .Os }}  -> map has no entry for key "Os"      (after this change)

So dst becomes consistent with src rather than gaining a new failure mode of its own. If you would rather .Os were bound in those two call sites, that is a separate change and I am happy to do it.

The apply sits next to the src one, above the glob, so a malformed dst aborts the release deterministically instead of depending on whether the glob happened to match anything on disk.

Verification: three subtests added, templated dst, templated dst error and templated dst error when the glob matches nothing, mirroring the existing templated src pair. Each fails without the production change and no other test changes state. go test is green on every package that depends on internal/archivefiles, and -race is clean on the archive pipes. The archive listing above is from a real goreleaser release --snapshot run with binaries built from this branch and from main.

No docs change: archives.md already carries # Templates: allowed. above files:, which covers every entry in the list.

AI disclosure: written with Claude Code. I ran the release before and after, checked the template context at each call site, and ran the mutations myself.

A template in archives.files[].dst landed in the archive verbatim, so
dst: docs_{{ .Os }} produced a directory literally named docs_{{ .Os }}.
The src on the same entry is already templated, and nfpm and srpm already
apply the template to both source and destination.

The apply sits below the no-match check so an entry whose glob matched
nothing is still skipped rather than aborting on a bad template.
@pull-request-size pull-request-size Bot added the size/M Denotes a PR that changes 30-99 lines, ignoring generated files. label Sep 7, 2026
@VXNCXNX

VXNCXNX commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Bump, with something specific rather than a generic ping.

The build workflow run for this branch has been sitting in action_required since 7 Sep, so no check has ever reported on the head commit:

$ gh pr checks 7118
no checks reported on the 'fix/archive-files-dst-template' branch

That is why it shows as blocked, on a missing required check rather than a failing one. It needs someone to approve the workflow run before CI can say anything either way. There are no conflicts with main otherwise.

If the change is not one you want, that is a perfectly good answer too and I will close it.

@caarlos0 caarlos0 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full diff against origin/main (merge base), plus every caller of archivefiles.Eval.

Verdict: the fix is correct and the approach is right — two small changes before merge.

What I verified, so it isn't re-litigated:

  • No double-apply. archivefiles.go:43 is the only Apply on Destination in the non-test tree. Post-Eval consumers only concatenate: sourcearchive/source.go:108, makeself/makeself.go:262, archive/archive.go:389.
  • No shared-state mutation. archivefiles.go:22 ranges by value, so f.Destination = dst writes a copy. That matters, because archive.go runs create() in parallel goroutines over one shared arch.Files backing array. go test -race ./internal/pipe/archive/... ./internal/archivefiles/... is clean.
  • No machine-generated dst. The defaults at archive/archive.go:84 set Source only; makeself.go:344 copies user config verbatim; archivefiles.go:64 is output, never fed back in.
  • The src/dst parity claim in the description holds. tmpl.New never populates .Os; only WithArtifact does, and archive.go:224 skips it for meta: true, as does sourcearchive/source.go:106. So dst gains no failure mode src did not already have.
  • The new subtests are real regression tests. Reverse-applying only the production hunk fails both: templated_dst gets var/{{ .Env.FOLDER }}/d.txt instead of var/d/d.txt, and templated_dst_error gets An error is expected but got nil. They call the real Eval with a real tmpl.Template, no mocks. All four affected packages pass.

One correction to the description: there is an escape hatch for a literal {{. dst: '{{ "{{" }}literal{{ "}}" }}' renders {{literal}} — I ran it. So the compatibility cost is smaller than stated.

Verification gaps worth stating:

  • Pro. templated_files[].dst is closed-source and not in this repo. Worth confirming Pro does not template Files[].dst itself before calling Eval; the OSS path is clean.
  • Integration coverage. There is no test anywhere in internal/pipe/{archive,sourcearchive,makeself} with a template in a Destination. All coverage of this change is unit-level in internal/archivefiles. That is proportionate for a six-line change, not a blocker.

The two requested changes are inline.

Reviewed by a bot.

Comment thread internal/archivefiles/archivefiles.go Outdated
Comment on lines +43 to +47
dst, err := template.Apply(f.Destination)
if err != nil {
return result, fmt.Errorf("failed to apply template %s: %w", f.Destination, err)
}
f.Destination = dst

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whether a bad dst template fails the release depends on what is on disk.

This block sits below the len(files) == 0 / continue at lines 35-41, so the template is only applied when the glob matched something. src is templated at line 23, before fileglob.Glob, so it always fails.

Trigger:

archives:
  - files:
      - src: "docs/*"
        dst: "docs_{{ .Env.NOPE }}"

With docs/ empty, the release succeeds and logs no files matched. Add one file to docs/ and the same config now aborts the release. Same config, two outcomes, decided by the working tree — so a typo can sit dormant in a repo for months and then break a release, or fail on one platform and pass on another when the glob is platform-dependent.

That is the exact src/dst asymmetry this PR sets out to remove, so it seems worth closing here rather than leaving a second one behind. The description argues the placement protects against "aborting the release on a bad template", but the len(files) == 0 skip exists to tolerate globs that match nothing (see #4013, which made it warn only for non-default globs) — not to tolerate malformed config. A malformed template is a config error and should be reported deterministically.

Smallest fix — move the apply up next to the src one, which also collapses it into the existing call:

glob, err := template.Apply(f.Source)
if err != nil {
	return result, fmt.Errorf("failed to apply template %s: %w", f.Source, err)
}
if err := template.ApplyAll(&f.Destination); err != nil {
	return result, err
}

That also matches internal/pipe/nfpm/nfpm.go, which does ApplyAll(&content.Source, &content.Destination, ...) in one go — the precedent cited in the description. Note the current error text also duplicates the template string, since tmpl errors already include it; ApplyAll avoids that too.

Needed test: a subtest with a glob that matches nothing and a bad dst template, asserting the error — e.g. src: "./testdata/nope/**/*", dst: "var/{{ .Env.NOPE }}/", testlib.RequireTemplateError. Nothing currently pins this behaviour in either direction; rlcp no results at archivefiles_test.go:198 uses a literal dst.

Comment on lines +126 to +127
#
# Templates: allowed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This annotation is redundant, and it is the only one of three pages that gets it.

Line 117 already carries # Templates: allowed. directly above files:, which covers every entry in the list including this one. The two sibling pages documenting the same dst through the same archivefiles.Evalpackage/source.md:35 and package/makeself.md:129 — use exactly that key-level annotation and have no per-entry one.

So after this hunk, archives.md annotates the same fact twice while the other two pages that gained the same capability say nothing extra, which reads as though dst templating is archives-only. It isn't: source.files[].dst and makeself.files[].dst both route through Eval (sourcearchive/source.go:106, makeself/makeself.go:257).

Smallest fix: drop the hunk. The existing line 117 is already true and now more completely so.

If the intent is to call out dst specifically, this file's own convention is field-level — see owner: at lines 138-139 — so it should sit directly above dst: and be added to all three pages, not above the list item in one of them.

Move dst template application to immediately after src apply,
before the file existence check, ensuring deterministic failure of
malformed templates regardless of glob matches. Use template.ApplyAll
to eliminate redundant error wrapping. Add test coverage for bad dst
template and remove redundant docs annotation.
@VXNCXNX

VXNCXNX commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Both changes are in 3d55025.

You were right that the outcome depended on the working tree. The apply now sits next to the src one and uses ApplyAll, which takes the redundant wrap with it. Reproduced with real --snapshot releases on dst: "docs_{{ .Env.NOPE }}", same config both times: as reviewed, an empty docs/ exits 0 with no files matched and exits 1 once a file lands there. After, both exit 1. That top-left cell is the dormant typo you described.

The new subtest is templated dst error when the glob matches nothing. Moving the apply back below the skip fails that one and only that one; removing it entirely fails all three dst subtests.

Dropping the wrap also cleans the message up, since tmpl.Error already quotes the string:

before  failed to apply template docs_{{ .Env.NOPE }}: template: failed to apply "docs_{{ .Env.NOPE }}": map has no entry for key "NOPE"
after   template: failed to apply "docs_{{ .Env.NOPE }}": map has no entry for key "NOPE"

I left the src wrap alone, since it has the same duplication but is pre-existing.

The docs hunk is dropped, so archives.md is byte-identical to main and the diff is four production lines and three subtests. Happy to annotate dst across all three pages as a follow-up if you want it called out.

On the escape hatch you are right and my description was wrong: dst: '{{ "{{" }}literal{{ "}}" }}' gives a directory named {{literal}}. Description corrected.

On Pro, I cannot check templated_files[].dst since it is not in this repo. If Pro templates Files[].dst before calling Eval, this becomes a double apply there, and that is the one thing I cannot rule out from here.

go test is green on every package that depends on internal/archivefiles, and -race is clean on the four archive pipes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Denotes a PR that changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants