Build Pipeline Hygiene

Keeping open-source license notices in a protected build

Nearly every permissive open-source license asks for the same small thing in return for the code: keep the copyright line and the permission notice with it when you pass it on. Your bundle is passing it on. And somewhere between your dependency tree and your shipped artifact, a step you did not think of as a legal step quietly removes them.

This is not a story about anybody behaving badly. It is a story about comments, which every stage of a modern JavaScript build treats as disposable, and about the fact that the notices in question are comments. Nothing errors. Nothing warns. The artifact is smaller and looks fine.

Where the notices go

Follow one dependency through a normal pipeline. It arrives with a header comment naming the copyright holder and reproducing the permission text. Your bundler concatenates it with forty others and, if it is being helpful, leaves that banner sitting immediately above the library's code somewhere in the middle of the output. A minifier then removes comments, typically keeping only those specially marked as legal. Then a protection step runs, and its job is to transform code rather than to curate comments.

By the time anything is uploaded, the notices are gone. The dependency is still there, doing its work, in an artifact you distribute to every visitor.

The reason this survives code review indefinitely is that there is nothing to review. No step announced that it had removed an attribution notice, because no step knew that is what it was.

What this engine preserves, precisely

Being exact here matters, because the rule is narrower than "comments are kept" and wider than "one comment is kept".

With the header-comment option enabled, the engine scans the file from its very first character. It skips whitespace, and it collects comments — line comments and block comments alike — one after another, for as long as that is what it finds. The moment it encounters a character that is neither whitespace nor the beginning of a comment, it stops looking. Everything it collected is prepended to the protected output. Everything else is subject to the transforms like any other part of the file.

Read that as a positive statement and it becomes a strategy rather than a limitation: whatever sits above the first statement survives, and it can be as many separate comment blocks as you like. The constraint is position, not quantity.

Why the bundler's arrangement fails

Now put those two facts together, because their interaction is the whole problem.

A bundler that leaves each notice inline, above the code it belongs to, has placed almost all of them after the first statement in the file. The first library's banner might be at the top and survive. The other thirty-nine are in the middle, and they do not. You end up preserving exactly one notice, and which one is decided by module concatenation order rather than by any decision of yours.

That is a worse outcome than it looks, because a single surviving notice makes the artifact look like it was handled deliberately. An auditor, or a maintainer whose name is missing, sees attribution present for one project and absent for the rest.

The arrangement that works

Aggregate, then prepend, then protect.

Generate the list of dependencies and their license texts from your package metadata rather than assembling it by hand — every major ecosystem has tooling for this, and hand-maintained lists go stale in exactly the way you would expect. Render that list into a single comment block. Prepend it to the bundle as the first thing in the file, above all code. Enable the header-comment option so the protection step preserves it.

The result matches the engine's rule exactly, and it has a property the inline arrangement never had: the notice block is now generated from your real dependency tree on every build, so adding a library adds its notice automatically instead of relying on somebody remembering.

Most bundlers will help with the prepending, either through a banner option or by extracting legal comments and letting you place them. The ordering constraint is the part to get right: the aggregation has to happen before the protection step, not after, because after is too late for anything to preserve.

The separate notices file

For a large dependency tree, a full set of license texts can be longer than the code. The common alternative is a short banner in the bundle that identifies the product and points to a notices file, with the complete texts in that file, distributed alongside the artifact.

This is a reasonable pattern with one condition attached: the file has to actually ship. A pointer to a document that lives only in your repository is worse than no pointer at all, because it presents as compliance while the recipient has been handed nothing. If your deployment copies a build directory, confirm the notices file is in it. If your artifact is published to a registry, confirm it is not excluded by your packaging rules — files omitted from a published package are a routine surprise.

Whether a pointer satisfies your obligations, rather than the full text travelling in the artifact, is a question for whoever advises you on licensing. The engineering half is simply: whatever you chose, verify it arrives.

A marker that exists to be found

It is worth naming the inversion here, because this site's instincts run the other way almost everywhere else.

Nearly every other thing in a protected bundle is something you would rather a reader did not find easily. An attribution notice is the opposite: its entire purpose is to be discovered by whoever received the code. Anything that hides it, shortens it or strips it is a defect, even when the same behaviour is a feature three lines earlier in your configuration. This is the same shape as the provenance marking obligation that appears in AI transparency rules — a marker whose value lies in surviving the build rather than in resisting it.

Treating notices as content that must survive, rather than as comments that may be dropped, is the mental adjustment that prevents the whole class of problem.

Your watermark lives in the same place

There is a practical consequence for anyone using build watermarking. The watermark is injected as a signed header comment, and it survives the transforms by exactly the mechanism described above — which is why enabling it forces the header-comment option on, and why a build that explicitly disabled that option refuses to run with a watermark rather than silently dropping the marker.

So your license banner and your provenance marker occupy the same leading block. That is convenient, and it means a single misconfigured post-processing step — a CDN transform, a late minification pass, a platform that re-minifies what you upload — removes your attribution and your ability to attribute a leaked copy in one go. If you check for one, check for both.

Copyleft is a different question

Everything above concerns attribution: keep the notice with the code. Copyleft terms ask for something else, about the availability of corresponding source to the people who receive your binary, and a transformed bundle does not answer that no matter how carefully its comments are managed.

That is a question about what you distribute and under what terms rather than about a build setting, and it deserves advice rather than a configuration flag. This site's article on whether obfuscation is legal covers the surrounding ground, including why the practical constraint usually shows up in the dependency tree rather than in your own code.

Make it a check, not a convention

Fix this once and it stays fixed only if something asserts it. The failure is silent in every direction, so a convention will not hold across a bundler upgrade or a new hire's pipeline change.

Add a step that reads the final artifact, the one you actually ship rather than an intermediate build output, and assert two things. First, that the expected notice text is present. Second, that the number of dependencies named in the banner matches the number your license tooling reports for the same tree. The first catches a build change that strips the header. The second catches a new dependency that never got a notice in the first place.

Both are a few lines. Both fail loudly, which is the entire point, because nothing else in this story does.

The short version

Comment removal deletes attribution notices, and a protected build preserves only the run of comments above the first statement in the file. Generate your notices from package metadata, aggregate them into one leading block, prepend it before the protection step, and turn the header-comment option on. Ship a notices file alongside if the full texts are too long, and verify that file is genuinely in what you distribute. Then assert all of it in your pipeline, because every failure mode here is quiet.

Frequently asked questions

Does obfuscating my bundle strip the open-source license notices in it?

Almost certainly, unless you did something specific to prevent it. Comment removal is a normal part of both minification and protection, and the attribution notices that permissive licenses ask you to preserve are comments. The usual sequence makes it worse rather than better: a bundler concatenates your dependencies and leaves a banner above each one, a minifier removes most comments, and then a protection step transforms what is left. By the time the artifact ships, the notices that were sitting above each vendored library are gone, and nobody noticed because nothing failed. It is worth checking your current output today rather than assuming.

What exactly survives in this engine?

The leading comment block, and the rule is more precise than it first sounds. When the keep-header-comment option is enabled, the engine scans from the very start of the file and collects consecutive comments, both line comments and block comments, skipping whitespace between them. It stops at the first character that is not whitespace and not the start of a comment. So everything stacked at the top of the file before the first statement is preserved, however many separate blocks that is, and everything after that first statement is not. That single rule tells you the whole strategy: whatever you need to keep has to be at the top, above any code.

So a banner above each bundled dependency does not survive?

Correct, and this is the specific failure that catches teams out. Bundlers commonly leave each library's notice inline, immediately above that library's code, which puts almost all of them somewhere in the middle of the file. Those are after the first statement, so the header rule does not reach them and they are removed. The notices that would have been preserved are only the ones that happen to sit above everything else. Relying on the default arrangement means keeping one notice out of forty, chosen by concatenation order rather than by you.

What is the fix, in practice?

Aggregate the notices into a single block at the very top of the artifact, and generate that block from your dependency data rather than writing it by hand. Most bundlers can either collect legal comments into a banner or extract them into a separate file, and license tooling in every major package ecosystem can produce the list of dependencies with their license texts. Take that output, render it as one comment block, prepend it to the bundle before the protection step, and enable the header-comment option. The result satisfies the rule exactly: one leading block, above the first statement, preserved through the build.

Can we ship a separate notices file instead of a giant comment?

Usually yes, and for a large dependency tree it is the more practical arrangement, but do not make it the only copy without checking your obligations. The common pattern is a short leading banner in the bundle that names the product and points to a notices file distributed alongside it, with the full texts in that file. The important part is that the file genuinely ships with the artifact and stays reachable, rather than existing only in your repository. A pointer to a document that was never distributed is worse than no pointer, because it looks like compliance while the recipient has nothing.

Is this a legal question or an engineering one?

Both, and the split is clean enough to be useful. The legal question is which notices you owe and in what form, which depends on the licenses in your tree and on how you distribute, and it is worth confirming with counsel rather than with a build tool. The engineering question is whether the notices you decided to include actually survive your pipeline and reach the recipient, and that one is entirely yours. This article is about the second. Teams generally get the first roughly right and then lose it in a build step nobody audits.

How does this interact with the watermark feature?

They share the same channel, which is a useful thing to know and an easy thing to trip over. The watermark is injected as a signed header comment and relies on the same header-preservation behaviour to survive the transforms, which is why enabling a watermark forces the header-comment option on and why the build refuses to run if you explicitly disabled it. The practical consequence is that your license banner and your watermark live in the same leading block and protect each other: anything that strips comments from the shipped artifact takes out your attribution and your provenance marker together.

Does copyleft need something different?

Yes, and it is a genuinely different obligation rather than a stricter version of the same one. Preserving a notice is about attribution. Copyleft terms concern the availability of corresponding source to recipients, which a transformed bundle does not provide no matter how carefully the comments are handled. That question is about what you distribute and on what terms, not about a build setting, so treat it separately and get advice on it. This site covers the surrounding ground in the article on whether obfuscation is legal.

How do we stop this regressing after we fix it once?

Make it a check rather than a convention, because it is invisible when it breaks. Add a step to your pipeline that reads the final shipped artifact, not an intermediate one, and asserts that the expected notice text is present and that the dependency count in the banner matches the count your license tooling reports. Both failure modes are then loud: a build change that strips the header, and a new dependency that never got a notice. Everything about this problem is silent by default, so the only reliable defence is an assertion that fails a build.

Related reading