How and Why to Use Patches
Table of Contents
- Background
- What is a Patch?
- Why and When to Patch?
-
How to Create and Apply a Patch
- Create wrapper install command
- Initialize blank-slate state for third-party dependencies
- Make changes until third-party dependency is functional
- Create and assess initial patchfile result
- Refine patchfile content
- Create post-install patching step
-
List of Patching Commands
- Command for Creating a Patch
- Command for Applying a Patch
Background
At one of my previous jobs, one of our major headaches was installing and maintaining a single sign-on authentication PHP module called simpleSAMLphp. SSO is a positive on many fronts: end-users don’t have to save a password for a new software application and can trust the fine folks at Google or Microsoft for keeping their accounts secure and setting up 2-factor-authentication, and we as developers don’t have to dedicate resources towards maintaining and enforcing secure login practices and software architecture.
But the way that we set up simpleSAMLphp was more complicated than it needed to be. The major woe we ran into were how these config files were first maintained. The block of simpleSAMLphp code was copied into our codebase and then directly modified in order to work with the rest of the codebase. This was perfectly fine on the first installation, but then the upgrades would come later. What if we had modified the configuration over time? How do we know what changes need to be applied to the module’s newer version after it’s put in place? Oftentimes, configuration changes that were made in a previous simpleSAMLphp version were missed on upgrade, and the team would scramble trying to find out what steps had been missed in the midst of a login outage.
It’s this kind of scenario where patches come into play. We could have minimized these headaches had we followed the process of:
- Documenting the basic installation process of the simpleSAMLphp codebase into our repo
- Maintained patchfiles that would apply all the custom changes we need to make to get simpleSAMLphp working with our existing code
- Upgrades mean changing the version of simpleSAMLphp installed in the first bullet followed by recreating our patchfile content against the newer version where possible
What is a Patch?
A patch is a dedicated file that dictates how a code change should be applied to an existing file. In pratice, the content of a patchfile looks like a diff
(because it’s generated using the diff
Linux command). Here’s an example that modifies the content of cli.js
in the doiuse-email NPM package to start with the line #!/usr/bin/env node
.
--- node_modules/doiuse-email/bin/cli.js 2024-05-24 15:26:26
+++ cli.js 2024-05-24 15:26:02
@@ -1,3 +1,4 @@
+#!/usr/bin/env node
import { program } from 'commander';
import * as fs from 'node:fs';
import process from 'node:process';
Node projects install third-party dependencies via the npm install
command. On my end, I need to either update how package.json
is configured (which informs how npm install
should run) or I need to make a bundled command such that:
- My dependencies are installed
- Then my patches are applied.
Why and When to Patch?
Patches are best used in scenarios where we need to keep what changes we’re making very clearly defined from what the base codebase of a third-party dependency is. In the Background section, it would have been beneficial for my team to keep our team’s configuration changes separate from the defaults provided to us by the simpleSAMLphp codebase being imported.
Patches are also useful if we’re making manual modifications to any codebase that is being auto-generated, i.e. if we’re using openapi-generator off of an OpenAPI spec to create clients and scaffolded server codebases. We would want to maintain the patch as a way to ensure that subsequent re-generations of the code don’t wipe out any manual changes we have to make in-between.
Always think about the future: if in 2 years I need to make an update to some part of our codebase that creates / updates a large swath of code, how likely will I remember this tiny manual change I made in one file?
Additionally, in cases where the third-party dependency being imported is open-source, consider whether the change that needs to be made would be beneficial to others using the codebase. If the change is specific to us, then leaving it as a patch is best. If the change is actually resolving a bug that other people using the third-party dependency are encountering, then consider being a good open-source software samaritan and start a PR with the contents of the patch applied. For efficiency:
- Create the patchfile with the bugfix on your team’s repository
- Create the PR on the open-source repository and work through their validations and reviews
- Once the fix is released and the new version of the open-source dependency is tagged, update your team’s repository with the newer version and remove the patchfile
Here’s a helpful flowchart to navigate for deciding whether to create a patch:
flowchart TD
one[Import 'third-party' dependency into our codebase]
two{Does 'third-party' dependency work with our codebase?}
three[No Patch needed]
four[Figure out what code changes are needed to get it to work]
five{Are the code changes specific to only our team?}
six{Is the 'third-party' dependency open-source & would the code changes be beneficial as an update against it?}
seven[Create Patches of the code changes against the 'third-party' dependency]
eight[Create a PR against the 'third-party' dependency's repository]
one --> two
two -->|Yes| three
two -->|No| four
four --> five
five -->|No| six
five -->|Yes| seven
six -->|Yes| eight
How to Create and Apply a Patch
Create wrapper install command
To get started, assume that there’s a third-party dependency that is imported and whose contents you can and need to manipulate. For this section, I will walkthrough a scenario where that dependency is an NPM package. But this could apply to other language’s package managers (like PHP’s packagist) or where a giant .zip
of the codebase needs to be unzipped and hooked up (as was the case with simpleSAMLphp). In this scenario, node_modules/
will also be .gitignore
d and does not need to stay in the codebase for the patch to be applied.
Start by wrapping how all third-party dependencies are installed in one singular, bundled command (i.e. create a Makefile command):
.PHONY: install
install: ## Install all third-party dependencies.
rm -rf node_modules
npm install
Note: your Makefile will need to update the indent in this codeblock from 4-spaces to 1-tab.
Initialize blank-slate state for third-party dependencies
Run make install
to start with what the “default” state will be for your dependencies. At this point, your third-party dependencies are installed but not properly working with your codebase. I will refer to the NPM package that needs to be patched as third-party/
.
Make changes until third-party dependency is functional
Work inside the node_modules/third-party/
folder in coordination with manual testing and automated unit-testing until the codebase behaves as we need it to. Don’t worry about being as reckless as possible or having to keep track of which files are being modified. If the code is too destroyed, the make install
command can be re-run to return to the blank-slate state.
In this example, node_modules/third-party/
was finally functional after a number of changes were applied to the following 3 files:
bin/cli.js
src/main.js
src/config.json
Create and assess initial patchfile result
The diff
Linux command is used to create the actual content of the patchfiles. In order to figure out what changes were actually performed, we’ll need to diff
the entire folder we changed against its blank-slate state.
Copy the changed folder to a different location:
mkdir third-party-fixed
cp -r node_modules/third-party .
(Optional) Rename the copied folder to clearly distinguish it in later steps:
mv third-party third-party-fixed
Re-install our dependencies back to blank-slate:
make install
Create and save a diff
of both folders:
-
-r
is to run thediff
command recursively on both folders (or elseresult.patch
just lists the similar folders 1-level deep in each folder). -
-u
is to specify that thediff
content returned is in the Unified diff format. Without a number passed in with-u
, the output diff will also show 3 (default) lines of matching content before-and-after each altered line.
diff -r -u node_modules/third-party third-party-fixed > result.patch
The content of result.patch
will show the full updates that were made in the Make changes until third-party dependency is functional section. Here is an example of this full patch with same list of files changed from earlier:
result.patch without refinement
diff --color -r -u node_modules/third-party/bin/cli.js third-party-fixed/bin/cli.js
--- node_modules/third-party/bin/cli.js 2025-06-24 17:32:22
+++ third-party-fixed/bin/cli.js 2025-06-24 17:36:23
@@ -9,6 +9,7 @@
.option('--supported-features', 'output the supported email features for the email clients specified')
.argument('[file]', 'the path to the HTML file to check')
.action((file, options) => {
+ throw new Error("TEST - stopping here to check how the prior methods work.");
if (options.supportedFeatures !== undefined && file !== undefined) {
throw new Error('Only one of `file` or `--supported-features` should be provided.');
}
diff --color -r -u node_modules/third-party/src/config.json third-party-fixed/src/config.json
--- node_modules/third-party/src/config.json 2025-06-24 17:33:00
+++ third-party-fixed/src/config.json 2025-06-24 17:35:23
@@ -9,7 +9,7 @@
"_sessionKey": "MyReallySecretPassword1",
"_port": 443,
"_aliasPort": 443,
- "_redirPort": 80,
+ "_redirPort": 121,
"_redirAliasPort": 80
},
"domains": {
@@ -17,7 +17,7 @@
"_title": "MyServer",
"_title2": "Servername",
"_minify": true,
- "_newAccounts": true,
+ "_newAccounts": false,
"_userNameIsEmail": true
}
},
diff --color -r -u node_modules/third-party/src/main.js third-party-fixed/src/main.js
--- node_modules/third-party/src/main.js 2025-06-24 17:33:40
+++ third-party-fixed/src/main.js 2025-06-24 17:35:02
@@ -91,6 +91,7 @@
// Convert a string into a blob
module.exports.data2blob = function (data) {
+ console.log("TEST - AT START OF module.exports.data2blob");
var bytes = new Array(data.length);
for (var i = 0; i < data.length; i++) bytes[i] = data.charCodeAt(i);
var blob = new Blob([new Uint8Array(bytes)]);
@@ -170,9 +171,13 @@
return doc;
};
module.exports.unEscapeLinksFieldName = function (doc) {
+ console.log("TEST - 0 - module.exports.unEscapeLinksFieldName");
if (doc.links != null) { for (var j in doc.links) { var ue = module.exports.unEscapeFieldName(j); if (ue !== j) { doc.links[ue] = doc.links[j]; delete doc.links[j]; } } }
+ console.log("TEST - 1 - module.exports.unEscapeLinksFieldName");
if (doc.ssh != null) { for (var j in doc.ssh) { var ue = module.exports.unEscapeFieldName(j); if (ue !== j) { doc.ssh[ue] = doc.ssh[j]; delete doc.ssh[j]; } } }
+ console.log("TEST - 2 - module.exports.unEscapeLinksFieldName");
if (doc.rdp != null) { for (var j in doc.rdp) { var ue = module.exports.unEscapeFieldName(j); if (ue !== j) { doc.rdp[ue] = doc.rdp[j]; delete doc.rdp[j]; } } }
+ console.log("TEST - 3 - module.exports.unEscapeLinksFieldName");
return doc;
};
//module.exports.escapeAllLinksFieldName = function (docs) { for (var i in docs) { module.exports.escapeLinksFieldName(docs[i]); } return docs; };
@@ -194,13 +199,13 @@
module.exports.validateUsername = function (username, minlen, maxlen) { return (module.exports.validateString(username, minlen, maxlen) && (username.indexOf(' ') == -1) && (username.indexOf('"') == -1) && (username.indexOf(',') == -1)); };
module.exports.isAlphaNumeric = function (str) { return (str.match(/^[A-Za-z0-9]+$/) != null); };
module.exports.validateAlphaNumericArray = function (array, minlen, maxlen) { if (((array != null) && Array.isArray(array)) == false) return false; for (var i in array) { if ((typeof array[i] != 'string') || (module.exports.isAlphaNumeric(array[i]) == false) || ((minlen != null) && (array[i].length < minlen)) || ((maxlen != null) && (array[i].length > maxlen)) ) return false; } return true; };
-module.exports.getEmailDomain = function(email) {
- if (!module.exports.validateEmail(email, 1, 1024)) {
- return '';
- }
- const i = email.indexOf('@');
- return email.substring(i + 1).toLowerCase();
-}
+// module.exports.getEmailDomain = function(email) {
+// if (!module.exports.validateEmail(email, 1, 1024)) {
+// return '';
+// }
+// const i = email.indexOf('@');
+// return email.substring(i + 1).toLowerCase();
+// }
module.exports.validateEmailDomain = function(email, allowedDomains) {
// Check if this request is for an allows email domain
For this example, I can see the 3 files that were changed earlier and what was done to them:
- In
bin/cli.js
, I put a temporarythrow new Error()
for debugging purposes - In
src/main.js
, I added a handful ofconsole.log()
s for debugging purposes and commented out thegetEmailDomain()
function. - In
src/config.json
, I changed the values for two keys in that config_redirPort
and_newAccounts
.
If instead you know which specific files were affected and only want to get the patchfile content for a single file, rerun the command without the -r
and with each argument against a single file:
diff -u node_modules/third-party/src/config.json third-party-fixed/src/config.json > file.patch
Refine patchfile content
It’s essential that the content of the patch itself is minimal and essential in order to make it much easier to re-apply to updated versions of the dependency in the future. If a patch in the future needs to be created that overlaps with an existing patch, those two patches should be combined to form one new patch to avoid a complicated layering of changes and potential conflicts.
In the example, after reviewing the giant patchfile created, I’ve determined that only the changes in src/config.json
are needed. The immediate benefits of the patchfile are now clear: the two config changes we need to maintain are small and easy-to-miss. It’s easy to see how future upgrades may neglect to update these two values and re-encounter the same issues just worked through:
diff --color -r -u node_modules/third-party/src/config.json third-party-fixed/src/config.json
--- node_modules/third-party/src/config.json 2025-06-24 17:33:00
+++ third-party-fixed/src/config.json 2025-06-24 17:35:23
@@ -9,7 +9,7 @@
"_sessionKey": "MyReallySecretPassword1",
"_port": 443,
"_aliasPort": 443,
- "_redirPort": 80,
+ "_redirPort": 121,
"_redirAliasPort": 80
},
"domains": {
@@ -17,7 +17,7 @@
"_title": "MyServer",
"_title2": "Servername",
"_minify": true,
- "_newAccounts": true,
+ "_newAccounts": false,
"_userNameIsEmail": true
}
},
Whether you use one patchfile to apply changes across a number of files or separate patchfiles for each file changed is up-to-you. I personally have only had to write patches for changes against one file. I also would personally lean towards one-patchfile-per-file as a way to better understand the scope of the changes and to more easily work through upgrades in cases where one of the files is removed or dramatically altered with that upgrade.
Create post-install patching step
Your repository should contain a new folder patches/
that contains all of the patchfiles need to be applied after installing third-party dependencies. The organization of this folder entirely depends on how complex of a patching setup is needed.
If there are a lot of third-party dependencies that each need a lot of patches, then consider setting up a more complex folder structure to more easily navigate and understand where changes are being applied. But for our example and for most use cases, storing all patchfiles flat in the patches/
folder is fine.
I recommend the following naming convention by joining the following identifiers:
- Ticket ID where the patch needs to be created (i.e. the Ticket ID to install and configure the third-party dependency)
- Name of the third-party dependency
- Version of that third-party dependency where this patch is applied
- File path to where the change is applied with
/
forward-slashes replaced with_
underscores
For our previous example, result.patch
should be moved into patches/
with the name PROJ-123_third-party_1.0.0_src_config.json.patch
. Also optionally, the content of this patch can be modified to remove the temporary path where the secondary file was stored. This is entirely for cosmetic purposes:
- diff --color -r -u node_modules/third-party/src/config.json third-party-fixed/src/config.json
+ diff --color -r -u node_modules/third-party/src/config.json config.json
--- node_modules/third-party/src/config.json 2025-06-24 17:33:00
- +++ third-party-fixed/src/config.json 2025-06-24 17:35:23
+ +++ config.json 2025-06-24 17:35:23
@@ -9,7 +9,7 @@
"_sessionKey": "MyReallySecretPassword1",
"_port": 443,
"_aliasPort": 443,
- "_redirPort": 80,
+ "_redirPort": 121,
"_redirAliasPort": 80
},
"domains": {
@@ -17,7 +17,7 @@
"_title": "MyServer",
"_title2": "Servername",
"_minify": true,
- "_newAccounts": true,
+ "_newAccounts": false,
"_userNameIsEmail": true
}
}
I’m happy to have had the opportunity to make a diff
on a diff
here LOL.
Now, the wrapper install command from earlier needs to be modified to apply the patch:
.PHONY: install
install: ## Install all third-party dependencies.
rm -rf node_modules
npm install
+ patch node_modules/third-party/src/config.json patches/PROJ-123_third-party_1.0.0_src_config.json.patch
Note: your Makefile will need to update the indent in this codeblock from 4-spaces to 1-tab.
Verify that the installation and patching is successfully by performing the following steps:
- Run
make install
to reset the dependencies back to blank-slate state and then programmatically apply the patch. - Manually verify that the files specified in the patchfile contain those changes in your third-party dependency folder (i.e. check that changes that need to be applied to
config.json
appear insidenode_modules/third-party/src/config.json
). - Verify that your full codebase implementation now works without any other manual changes needed.
- If the third-party dependency is still unsuccessful, repeat steps Make changes until third-party dependency is functional through to this current step.
List of Patching Commands
In short, these are the two key Unix terminal commands:
Command for Creating a Patch
diff
command arguments explained:
- first argument is the “before” file left un-touched from what is provided in the third-party dependency codebase
- second argument is the “after” file with manual changes applied
-
-r
is to run thediff
command recursively on both folders (or elseresult.patch
just lists the similar folders 1-level deep in each folder). -
-u
is to specify that thediff
content returned is in the Unified diff format. Without a number passed in with-u
, the output diff will also show 3 (default) lines of matching content before-and-after each altered line.
diff
two files and save the result in one patchfile:
diff -u node_modules/third-party/src/config.json config-fixed.json > file.patch
diff
two folders and save the result in one patchfile:
diff -r -u node_modules/third-party third-party-fixed > result.patch
Command for Applying a Patch
patch
command arguments explained:
- first argument is the original file left un-touched and located where the third-party dependency will be installed
- second argument is the patchfile that specifies how the “first argument” file should be altered
patch
example with a single file in node_modules/
with the patch stored in the patches/
folder:
patch node_modules/third-party/src/config.json patches/PROJ-123_third-party_1.0.0_src_config.json.patch