DEV Community

Ashton Du Toit
Ashton Du Toit

Posted on

My release pipeline was about to ship my private notes

sell a small digital product, and the delivery side of it is deliberately
boring. Push a git tag, a GitHub Action zips up the product folder, the zip
gets attached to a release, buyer downloads it. Wrote the whole thing in
about four minutes and never opened it again.

Then I went looking through the product folder for something unrelated and
noticed what was actually in there.

Two files, sitting next to the guide and the configs people pay for.
source-material.md, which is my raw notes — unedited, real paths off my
machine, half-finished thoughts, the ugly version of everything the
polished guide says properly. And SANITIZATION-CHECKLIST.md, which is the
checklist I use to make sure none of that ever leaks.

The zip step said, roughly, put this folder in a zip. Both files were in
that folder. So the checklist for preventing the leak was part of the leak.

Nobody had bought anything yet. That's the only reason I'm writing this up
instead of emailing someone an apology.

The step:

- name: Zip the product folder
  run: |
    SLUG=${GITHUB_REF_NAME%-v*}
    cd products/$SLUG && zip -r ../../$GITHUB_REF_NAME.zip .
Enter fullscreen mode Exit fullscreen mode

There's no bug in it. It does what it says. What it says is "ship the whole
directory", and directories fill up.

Obvious patch:

zip -r ../../$GITHUB_REF_NAME.zip . \
  -x "*.git*" -x "*source-material.md" -x "*SANITIZATION-CHECKLIST.md"
Enter fullscreen mode Exit fullscreen mode

I used *source-material.md rather than the bare filename so a copy in a
subfolder gets caught too. Small thing, but that's the sort of gap a
denylist has.

Except the exclude list is really just a list of the mistakes I've already
thought of. It does nothing about the next one. notes.md. A TODO.md. An
.env.example that stopped being an example three commits ago. A
screenshot with a browser tab visible in the corner. The build ships
whatever's present and what's present grows every time I touch the product.

Two better options depending on how organised you are. Either name what
ships and let everything else be invisible by default, so adding a new file
to the release takes one deliberate line — which feels like the right
amount of friction for something going to customers. Or keep working files
and shippable files in separate directories so the build only ever sees the
clean one and there's nothing to exclude.

I haven't restructured mine yet, honestly. What I did add is a check that
looks at the finished zip instead of trusting the step that made it:

- name: Refuse to ship internal files
  run: |
    if unzip -l $GITHUB_REF_NAME.zip | grep -Ei 'source-material|SANITIZATION'; then
      echo "Internal file found in the release zip. Refusing to publish." >&2
      exit 1
    fi
Enter fullscreen mode Exit fullscreen mode

Four lines, and a failed build is a much cheaper way to find out than a
customer email.

If you want to know what your own release actually contains, don't read
the workflow, read the output:

unzip -l dist/whatever-you-ship.zip
Enter fullscreen mode Exit fullscreen mode

Run it against the last release you cut rather than the next one. I went
in expecting a tidy list of product files and about a third of the way
down there was a file I'd written strictly for myself.

Packaging steps get written early, when the folder has three files in it
and the whole thing is obviously fine, and then they sit there unchanged
while the folder fills up with the debris of actually doing the work.
Nobody re-reads a build step that's never failed. The question isn't
whether your build is correct, it's what your build includes today. Those
had the same answer for me in April and a different one in July.

Top comments (0)