fix(source): copy symlinks as symlinks, not through them ai:gpt-5.6-sol #84

Manually merged
heiko merged 1 commit from fix/deb-source-symlinks into master 2026-08-18 11:19:59 +02:00
Owner

Found while publishing a release with #83's build. The run aborted in source packaging:

packaging: source packaging: build deb source: deb source: copy tree: stat .../demo/server/dehydrated/private/_acme-challenge.elbtal.com: no such file or directory

Cause

The repository tracks that path as a symlink whose target its own .gitignore excludes — a TSIG key, deliberately kept out of the repo:

120000 …  demo/server/dehydrated/private/_acme-challenge.elbtal.com
        → _acme-challenge.elbtal.com.acme.schlittermann.de.tsig-key   (gitignored)

Nothing looks wrong in the maintainer's working tree, because there the target exists. But gogogo packages from a fresh clone, where the link necessarily dangles. copyTree delegated to fsutil.Copy, which begins with os.Stat(src) — that follows the link, fails, and takes the entire release down over a file that was never in the repository to begin with.

This is not an exotic layout: a tracked symlink into ignored or generated content is common, and it makes releases fail on the maintainer's machine only after a clone, which is the hardest place to notice.

Fix

copyTree now recreates symlinks with os.Readlink/os.Symlink, reproducing the target string verbatim. filepath.WalkDir reports the link itself rather than what it points at, so the target is never touched and a missing target is irrelevant.

A second, quieter bug goes with it: a symlink that did resolve was silently dereferenced and written as a regular file, so the generated source package no longer matched the tree it claimed to contain — a link became a duplicate copy of its target. Both cases are now preserved faithfully.

os.Remove before os.Symlink keeps a re-run into a populated destination from failing with EEXIST.

Tests

TestCopyTreePreservesSymlinks builds the failing shape — one link that resolves, one that dangles — and asserts the copy succeeds, both stay links, the target string is reproduced verbatim, regular files still copy, and the ignored target is not invented in the destination. TestCopyTreeReplacesExistingSymlink covers the re-run case.

Verification

go build ./..., go test ./..., go vet ./... pass; golangci-lint run --new-from-rev origin/master reports 0 issues.

Verified end-to-end: with this fix the previously failing gogogo publish gets past source packaging.

Independent of #83 — this branch is cut from master, so the two can land in either order.

(authored by ai:gpt-5.6-sol)

Found while publishing a release with #83's build. The run aborted in source packaging: ``` packaging: source packaging: build deb source: deb source: copy tree: stat .../demo/server/dehydrated/private/_acme-challenge.elbtal.com: no such file or directory ``` ## Cause The repository tracks that path as a **symlink whose target its own `.gitignore` excludes** — a TSIG key, deliberately kept out of the repo: ``` 120000 … demo/server/dehydrated/private/_acme-challenge.elbtal.com → _acme-challenge.elbtal.com.acme.schlittermann.de.tsig-key (gitignored) ``` Nothing looks wrong in the maintainer's working tree, because there the target exists. But gogogo packages from a **fresh clone**, where the link necessarily dangles. `copyTree` delegated to `fsutil.Copy`, which begins with `os.Stat(src)` — that follows the link, fails, and takes the entire release down over a file that was never in the repository to begin with. This is not an exotic layout: a tracked symlink into ignored or generated content is common, and it makes releases fail on the maintainer's machine only after a clone, which is the hardest place to notice. ## Fix `copyTree` now recreates symlinks with `os.Readlink`/`os.Symlink`, reproducing the target string verbatim. `filepath.WalkDir` reports the link itself rather than what it points at, so the target is never touched and a missing target is irrelevant. **A second, quieter bug goes with it:** a symlink that *did* resolve was silently dereferenced and written as a regular file, so the generated source package no longer matched the tree it claimed to contain — a link became a duplicate copy of its target. Both cases are now preserved faithfully. `os.Remove` before `os.Symlink` keeps a re-run into a populated destination from failing with `EEXIST`. ## Tests `TestCopyTreePreservesSymlinks` builds the failing shape — one link that resolves, one that dangles — and asserts the copy succeeds, both stay links, the target string is reproduced verbatim, regular files still copy, and the ignored target is not invented in the destination. `TestCopyTreeReplacesExistingSymlink` covers the re-run case. ## Verification `go build ./...`, `go test ./...`, `go vet ./...` pass; `golangci-lint run --new-from-rev origin/master` reports 0 issues. Verified end-to-end: with this fix the previously failing `gogogo publish` gets past source packaging. Independent of #83 — this branch is cut from `master`, so the two can land in either order. *(authored by ai:gpt-5.6-sol)*
Publishing cert-proxy aborted in the source-packaging stage:

  packaging: source packaging: build deb source: deb source: copy tree:
  stat .../demo/server/dehydrated/private/_acme-challenge.elbtal.com:
  no such file or directory

The repository tracks that path as a symlink to a file its own .gitignore
excludes (a TSIG key, kept out of the repo on purpose). The maintainer's
working tree has the target, so nothing looks wrong locally, but gogogo
packages from a fresh clone where the link necessarily dangles. copyTree
delegated to fsutil.Copy, which stats the source and therefore follows the
link, so the whole release failed on a file that was never meant to be in
the repository in the first place.

Symlinks are now recreated with their target string reproduced verbatim.
WalkDir reports the link itself rather than what it points at, so the target
is never touched and a missing one is irrelevant.

This also fixes a quieter bug: a symlink that *did* resolve was silently
flattened into a second copy of the file, so the source package no longer
matched the tree it claimed to contain.
heiko manually merged commit f8b62a100b into master 2026-08-18 11:19:59 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
heiko/gogogo!84
No description provided.