Repository navigation
Expand file tree
/
Copy pathpatch-workflow.html
More file actions
590 lines (531 loc) · 32.6 KB
/
Copy pathpatch-workflow.html
File metadata and controls
590 lines (531 loc) · 32.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Patch Workflow</title>
<style>
:root {
--bg: #f7f7f5; --surface: #ffffff; --surface-2: #f1f0ec; --border: #dedbd2;
--text: #1c1b18; --muted: #6b6860; --accent: #1f6f4a; --accent-soft: #e3f0e8;
--warn: #8a5a00; --warn-soft: #fdf1dc; --danger: #a32a1e; --danger-soft: #fbe6e3;
--code-bg: #1c1b18; --code-text: #eceae4;
--radius: 10px; --shadow: 0 1px 2px rgba(0,0,0,.05), 0 8px 24px -16px rgba(0,0,0,.25);
--font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #14140f; --surface: #1c1c17; --surface-2: #23231d; --border: #34342c;
--text: #eceae4; --muted: #9a978c; --accent: #6cc79a; --accent-soft: #16301f;
--warn: #e0aa54; --warn-soft: #2e2513; --danger: #ef8878; --danger-soft: #331a16;
--code-bg: #0e0e0a; --code-text: #eceae4;
}
}
:root[data-theme="dark"] {
--bg: #14140f; --surface: #1c1c17; --surface-2: #23231d; --border: #34342c;
--text: #eceae4; --muted: #9a978c; --accent: #6cc79a; --accent-soft: #16301f;
--warn: #e0aa54; --warn-soft: #2e2513; --danger: #ef8878; --danger-soft: #331a16;
--code-bg: #0e0e0a; --code-text: #eceae4;
}
* { box-sizing: border-box; }
body {
margin: 0; background: var(--bg); color: var(--text);
font-family: var(--font); font-size: 16px; line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 62rem; margin: 0 auto; padding: 3rem 1.25rem 6rem; }
h1 { font-size: clamp(1.75rem, 4vw, 2.5rem); line-height: 1.15; letter-spacing: -.02em; margin: 0 0 .5rem; }
h2 { font-size: 1.3rem; letter-spacing: -.01em; margin: 3.5rem 0 .35rem; }
h3 { font-size: 1rem; margin: 0 0 .35rem; }
p { margin: 0 0 1rem; }
.lede { font-size: 1.1rem; color: var(--muted); max-width: 46rem; margin-bottom: 2rem; }
.section-note { color: var(--muted); margin: 0 0 1.5rem; max-width: 46rem; }
code { font-family: var(--mono); font-size: .875em; background: var(--surface-2);
border: 1px solid var(--border); border-radius: 4px; padding: .1em .35em; }
pre { background: var(--code-bg); color: var(--code-text); border-radius: var(--radius);
padding: 1rem 1.1rem; overflow-x: auto; margin: 0 0 1rem; font-size: .85rem; line-height: 1.65; }
pre code { background: none; border: 0; padding: 0; color: inherit; font-size: inherit; }
pre .c { color: #8b8878; }
pre .k { color: #6cc79a; }
.tag { display: inline-block; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase;
font-weight: 600; padding: .2rem .5rem; border-radius: 999px; background: var(--surface-2);
color: var(--muted); border: 1px solid var(--border); }
.tag.local { background: var(--accent-soft); color: var(--accent); border-color: transparent; }
.tag.live { background: var(--warn-soft); color: var(--warn); border-color: transparent; }
/* ---------- graph ---------- */
.graph { margin: 1.5rem 0 1rem; }
.lane { margin-bottom: 1.25rem; }
.lane-head { display: flex; align-items: center; gap: .6rem; margin-bottom: .6rem; }
.lane-head h3 { margin: 0; font-size: .95rem; }
.lane-head .where { color: var(--muted); font-size: .85rem; }
.flow { display: flex; align-items: stretch; gap: 0; overflow-x: auto; padding-bottom: .5rem; }
.flow::-webkit-scrollbar { height: 8px; }
.flow::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
.node {
flex: 0 0 12.5rem; text-align: left; cursor: pointer;
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
padding: .85rem .9rem; font: inherit; color: inherit; box-shadow: var(--shadow);
transition: border-color .12s, transform .12s;
}
.node:hover, .node:focus-visible { border-color: var(--accent); transform: translateY(-2px); outline: none; }
.node .n { font-size: .7rem; color: var(--muted); font-weight: 600; letter-spacing: .06em; }
.node .t { display: block; font-family: var(--mono); font-size: .82rem; font-weight: 600; margin: .2rem 0 .3rem; }
.node .d { display: block; font-size: .78rem; color: var(--muted); line-height: 1.45; }
.arrow { flex: 0 0 2.25rem; display: grid; place-items: center; color: var(--border); }
.arrow svg { width: 100%; height: 18px; }
.arrow.down { flex: none; width: 12.5rem; height: 1.75rem; }
.arrow.down svg { width: 18px; height: 100%; }
.bridge {
display: flex; align-items: center; gap: .75rem; margin: .25rem 0 1.5rem;
padding: .7rem .9rem; border-radius: var(--radius);
background: var(--surface-2); border: 1px dashed var(--border);
}
.bridge .t { font-family: var(--mono); font-size: .82rem; font-weight: 600; }
.bridge .d { font-size: .8rem; color: var(--muted); }
/* ---------- cards ---------- */
.cards { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); margin: 1.5rem 0; }
.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
padding: 1.1rem; box-shadow: var(--shadow); }
.card h3 { font-family: var(--mono); font-size: .88rem; }
.card p { font-size: .875rem; color: var(--muted); margin: 0; }
.card p + p { margin-top: .6rem; }
.card.accent { border-left: 3px solid var(--accent); }
.card.warn { border-left: 3px solid var(--warn); }
.card.danger { border-left: 3px solid var(--danger); }
/* ---------- details ---------- */
details { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
margin-bottom: .6rem; overflow: hidden; }
summary { cursor: pointer; padding: .8rem 1rem; font-weight: 600; font-size: .92rem;
list-style: none; display: flex; align-items: center; gap: .6rem; }
summary::-webkit-details-marker { display: none; }
summary::before { content: ""; width: .45rem; height: .45rem; flex: none;
border-right: 2px solid var(--muted); border-bottom: 2px solid var(--muted);
transform: rotate(-45deg); transition: transform .15s; }
details[open] > summary::before { transform: rotate(45deg); }
summary code { font-size: .85rem; }
.details-body { padding: 0 1rem 1rem; border-top: 1px solid var(--border); padding-top: 1rem; }
.details-body > :last-child { margin-bottom: 0; }
.details-body p { font-size: .9rem; }
/* ---------- dialog ---------- */
dialog {
border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface);
color: var(--text); padding: 0; max-width: 40rem; width: calc(100% - 2rem);
box-shadow: 0 24px 64px -24px rgba(0,0,0,.5);
}
dialog::backdrop { background: rgba(0,0,0,.5); backdrop-filter: blur(2px); }
.dlg-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem;
padding: 1.1rem 1.25rem .75rem; }
.dlg-head h3 { font-family: var(--mono); font-size: .95rem; margin: 0; }
.dlg-head .sub { font-size: .8rem; color: var(--muted); margin-top: .2rem; }
.dlg-body { padding: 0 1.25rem 1.25rem; }
.dlg-body > :last-child { margin-bottom: 0; }
.dlg-body p { font-size: .9rem; }
.x { background: none; border: 0; color: var(--muted); font-size: 1.4rem; line-height: 1;
cursor: pointer; padding: 0 .2rem; }
.x:hover { color: var(--text); }
ul { margin: 0 0 1rem; padding-left: 1.15rem; }
li { margin-bottom: .35rem; font-size: .9rem; }
a { color: var(--accent); }
.foot { margin-top: 4rem; padding-top: 1.5rem; border-top: 1px solid var(--border);
color: var(--muted); font-size: .85rem; }
</style>
</head>
<body>
<div class="wrap">
<h1>Patch workflow</h1>
<p class="lede">
One command to pull a security release in, one to apply it, one to ship it.
Every patch lives in the repo, so every shop you maintain goes through the same three steps.
</p>
<div class="card accent" style="margin-bottom:2.5rem">
<h3>Tonight, per shop</h3>
<pre><code><span class="c"># local</span>
tools/patches find qoliber/magento-open-source-VULN-39341
tools/patches add qoliber/magento-open-source-VULN-39341 2026-09-001/cweagans/2.4.9
tools/patches apply
<span class="c"># test the shop, then</span>
git add patches patches.json patches.lock.json && git commit && git push
<span class="c"># live</span>
git pull && tools/patches deploy</code></pre>
<p>The <code>add</code> line is the only one that changes between releases. Everything else is identical on every project.</p>
</div>
<h2>How it hangs together</h2>
<p class="section-note">
Click any step to see what it actually runs and what can go wrong there.
</p>
<div class="graph">
<div class="lane">
<div class="lane-head">
<span class="tag local">Local</span>
<h3>MacBook</h3>
<span class="where">your copy of the customer's shop</span>
</div>
<div class="flow">
<button class="node" data-dlg="d-find">
<span class="n">01</span><span class="t">patches find</span>
<span class="d">List the folders in a patch repo that match this shop's Magento version.</span>
</button>
<span class="arrow" aria-hidden="true"><svg viewBox="0 0 36 18"><path d="M2 9h28M26 4l6 5-6 5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<button class="node" data-dlg="d-add">
<span class="n">02</span><span class="t">patches add</span>
<span class="d">Download that folder into <code>patches/</code> and rebuild the patch list.</span>
</button>
<span class="arrow" aria-hidden="true"><svg viewBox="0 0 36 18"><path d="M2 9h28M26 4l6 5-6 5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<button class="node" data-dlg="d-apply">
<span class="n">03</span><span class="t">patches apply</span>
<span class="d">Reinstall every patched package, patch it, then prove all patches are in.</span>
</button>
<span class="arrow" aria-hidden="true"><svg viewBox="0 0 36 18"><path d="M2 9h28M26 4l6 5-6 5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<button class="node" data-dlg="d-test">
<span class="n">04</span><span class="t">test the shop</span>
<span class="d">Cache flush, click the areas the release touched, check the logs.</span>
</button>
<span class="arrow" aria-hidden="true"><svg viewBox="0 0 36 18"><path d="M2 9h28M26 4l6 5-6 5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<button class="node" data-dlg="d-commit">
<span class="n">05</span><span class="t">commit and push</span>
<span class="d">Four things travel: the patch files, the list, the lock, the tool.</span>
</button>
</div>
</div>
<div class="bridge">
<span class="tag">git</span>
<span>
<span class="t">patches/ patches.json patches.lock.json</span><br>
<span class="d">The lock is what the server obeys. It is generated locally and never regenerated on the server.</span>
</span>
</div>
<div class="lane">
<div class="lane-head">
<span class="tag live">Live</span>
<h3>Production server</h3>
<span class="where">no thinking, no decisions</span>
</div>
<div class="flow">
<button class="node" data-dlg="d-pull">
<span class="n">06</span><span class="t">git pull</span>
<span class="d">Brings the patch files and the lock that you already tested.</span>
</button>
<span class="arrow" aria-hidden="true"><svg viewBox="0 0 36 18"><path d="M2 9h28M26 4l6 5-6 5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<button class="node" data-dlg="d-deploy">
<span class="n">07</span><span class="t">patches deploy</span>
<span class="d">install, repatch, verify, upgrade, di:compile, static content, cache flush.</span>
</button>
<span class="arrow" aria-hidden="true"><svg viewBox="0 0 36 18"><path d="M2 9h28M26 4l6 5-6 5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<button class="node" data-dlg="d-verify">
<span class="n">08</span><span class="t">patches verify</span>
<span class="d">Reverse-applies every locked patch as a dry run. Green means it is really in the code.</span>
</button>
</div>
</div>
</div>
<h2>What a patch store looks like</h2>
<p class="section-note">
Four things in the repo, nothing outside it. No path repository, no wrapper package, no symlink into <code>vendor/</code>.
</p>
<div class="cards">
<div class="card">
<h3>patches/</h3>
<p>One folder per release, containing the <code>.patch</code> files exactly as the upstream repo ships them.</p>
<p><code>source.json</code> keeps the upstream descriptions. <code>origin.json</code> records where the folder came from.</p>
</div>
<div class="card">
<h3>patches.json</h3>
<p>Generated. Maps every patch file to its Composer package with a sha256. This is the file <code>cweagans/composer-patches</code> v2 reads by default, so <code>composer.json</code> needs no <code>extra.patches</code> block at all.</p>
</div>
<div class="card">
<h3>patches.lock.json</h3>
<p>Generated by Composer. The authoritative list. On the server the plugin reads this and nothing else, so what you tested is exactly what ships.</p>
</div>
<div class="card">
<h3>tools/patches</h3>
<p>The script. Copy it into each shop's repo. It has no configuration: it reads the Magento version out of <code>composer.lock</code> itself.</p>
</div>
</div>
<h2>Commands</h2>
<details>
<summary><code>tools/patches find <owner/repo></code></summary>
<div class="details-body">
<p>Walks a GitHub repo's tree, finds every folder that holds <code>.patch</code> files, and keeps the ones matching this shop's Magento version. Prefers the <code>cweagans</code> flavour, because that is the plugin in use here.</p>
<pre><code>$ tools/patches find qoliber/magento-open-source-VULN-39341
Magento 2.4.9<span class="c">, patch folders in qoliber/magento-open-source-VULN-39341:</span>
2026-09-001/cweagans/2.4.9 cweagans
VULN-39341/cweagans/2.4.9 cweagans</code></pre>
<p>Needs the <code>gh</code> CLI. If you already know the folder, skip straight to <code>add</code>.</p>
</div>
</details>
<details>
<summary><code>tools/patches add</code></summary>
<div class="details-body">
<p>Two forms, whichever is quicker to reach for:</p>
<pre><code><span class="c"># paste the GitHub folder URL straight from the browser</span>
tools/patches add https://github.com/qoliber/magento-open-source-VULN-39341/tree/main/2026-09-001/cweagans/2.4.9
<span class="c"># or repo plus path</span>
tools/patches add qoliber/magento-open-source-VULN-39341 2026-09-001/cweagans/2.4.9</code></pre>
<p>The release name is guessed from the path (<code>2026-09-001</code>, <code>APSB26-92</code>, <code>VULN-39341</code>). Pass your own as the last argument if the guess is wrong. Running <code>add</code> again for the same release replaces it, which is how you take an upstream correction.</p>
<p>It ends by running <code>sync</code>, so after <code>add</code> the list and the lock are already current.</p>
</div>
</details>
<details>
<summary><code>tools/patches sync</code></summary>
<div class="details-body">
<p>Rebuilds <code>patches.json</code> from whatever is on disk under <code>patches/</code>, then runs <code>composer patches-relock</code>.</p>
<p>Package names and descriptions come from the release's <code>source.json</code> when it has one. Otherwise they are derived from the filename: <code>module-cms.patch</code> becomes <code>magento/module-cms</code>, <code>magento_framework.patch</code> becomes <code>magento/framework</code>, and <code>vendor--package.patch</code> becomes <code>vendor/package</code>.</p>
<p>A patch aimed at a package this shop does not have installed is skipped with a warning rather than blowing up. That is what makes one release folder safe to drop into three shops with different module sets.</p>
</div>
</details>
<details>
<summary><code>tools/patches apply</code></summary>
<div class="details-body">
<p><code>sync</code>, then <code>composer patches-repatch</code>, then <code>verify</code>.</p>
<p>Repatch is the important part. A plain <code>composer install</code> only patches packages it actually reinstalls, so if <code>composer.lock</code> did not change, nothing gets patched and nothing complains. Repatch deletes every patched package first, which forces the reinstall and the patching.</p>
<p>If a patch does not apply, the command stops there with the real error. Nothing is swallowed.</p>
</div>
</details>
<details>
<summary><code>tools/patches verify</code></summary>
<div class="details-body">
<p>For every patch in the lock, tries to reverse-apply it as a dry run against the installed package. Success means the change is present in the code.</p>
<ul>
<li><strong>green</strong> the patch is applied</li>
<li><strong>red</strong> the patch is not applied but would apply cleanly, so run <code>apply</code></li>
<li><strong>yellow</strong> neither, which usually means a partly applied patch or a package version that moved underneath it</li>
</ul>
<p>Exits 2 when anything is not green, so it works in a deploy script or a cron check.</p>
</div>
</details>
<details>
<summary><code>tools/patches status</code></summary>
<div class="details-body">
<p>Prints the Magento version, the releases in the store with their patch counts and origins, whether the two generated files exist, and where the locked patches came from. Useful as the first thing you run on a shop you have not touched in a while.</p>
</div>
</details>
<details>
<summary><code>tools/patches deploy</code></summary>
<div class="details-body">
<p>The production sequence, in order, stopping at the first failure:</p>
<pre><code>composer install --no-dev
composer patches-repatch
tools/patches verify <span class="c"># stops the deploy if anything is missing</span>
bin/magento setup:upgrade --keep-generated
bin/magento setup:di:compile
bin/magento setup:static-content:deploy -f nl_NL en_US
bin/magento cache:flush</code></pre>
<p>Static content locales default to <code>nl_NL en_US</code>. Override per shop:</p>
<pre><code>PATCHES_LOCALES="nl_NL de_DE" tools/patches deploy</code></pre>
<p>It does not run <code>git pull</code>. That stays yours, so the deploy never moves code you did not ask it to.</p>
</div>
</details>
<details>
<summary><code>tools/patches import <vendor/package></code></summary>
<div class="details-body">
<p>One time only, for a shop still using a wrapper package such as <code>siteation/magento2-security-patches-249</code>. Copies the patch files out of the installed package into <code>patches/</code> and carries the descriptions across.</p>
<pre><code>tools/patches import siteation/magento2-security-patches-249
composer remove siteation/magento2-security-patches-249
tools/patches sync
tools/patches verify</code></pre>
<p>Nothing gets patched twice along the way. The plugin drops duplicates by sha256, so the same patch reaching it from two places is applied once.</p>
</div>
</details>
<details>
<summary><code>tools/patches remove <release></code></summary>
<div class="details-body">
<p>Deletes the release folder and re-syncs. The code in <code>vendor/</code> is still patched at that point, so follow it with <code>composer patches-repatch</code> to get clean packages back.</p>
</div>
</details>
<h2>Rolling this out to another shop</h2>
<p class="section-note">Ten minutes each, once.</p>
<div class="cards">
<div class="card">
<h3>1. Install the tool</h3>
<pre><code>mkdir -p tools
curl -fsSL https://raw.githubusercontent.com/\
Siteation/magento2-patch-workflow/main/patches \
-o tools/patches
chmod +x tools/patches</code></pre>
<p>No edits needed. It reads the Magento version from that shop's own <code>composer.lock</code>. Later, <code>tools/patches update</code> pulls a newer tool and doc.</p>
</div>
<div class="card">
<h3>2. Import what is there</h3>
<p>If the shop uses a wrapper patch package, <code>import</code> it and then <code>composer remove</code> it. If it uses <code>extra.patches</code> in <code>composer.json</code>, move the files into <code>patches/<release>/</code> by hand and delete the block.</p>
</div>
<div class="card">
<h3>3. Add the new release</h3>
<p>Same <code>add</code> line as here, if the shop is also on 2.4.9. If it is on 2.4.8-p5, <code>find</code> will show you that folder instead.</p>
</div>
<div class="card">
<h3>4. Apply, test, ship</h3>
<p><code>tools/patches apply</code>, click through the shop, commit, push, then <code>tools/patches deploy</code> on the server.</p>
</div>
</div>
<h2>Things that bite</h2>
<details>
<summary>A plain <code>composer install</code> does not apply new patches</summary>
<div class="details-body">
<p>The plugin patches a package at the moment it installs it. Add a patch without touching <code>composer.lock</code> and <code>composer install</code> has nothing to install, so nothing is patched, and there is no error to notice. This is the single biggest way a shop ends up believing it is patched when it is not.</p>
<p><code>composer patches-repatch</code> is the fix, and it is why <code>deploy</code> runs it and then verifies.</p>
</div>
</details>
<details>
<summary><code>composer-exit-on-patch-failure</code> does nothing in v2</summary>
<div class="details-body">
<p>That key belongs to <code>cweagans/composer-patches</code> v1. Version 2 reads its configuration from <code>extra.composer-patches.*</code> and throws on any failed patch unconditionally, so the guard is built in. The stale key was removed from <code>composer.json</code> here because leaving it suggests protection that is coming from somewhere else entirely.</p>
</div>
</details>
<details>
<summary>The lock file is law on the server</summary>
<div class="details-body">
<p>When <code>patches.lock.json</code> exists, the plugin reads it and never re-resolves from <code>patches.json</code>. Good, because the server then applies exactly what you tested. The catch: edit <code>patches.json</code> by hand and forget to relock, and your change is invisible. Always go through <code>tools/patches sync</code>, which relocks for you.</p>
</div>
</details>
<details>
<summary><code>patches-repatch</code> ignores <code>--no-dev</code></summary>
<div class="details-body">
<p>Repatch runs <code>composer install</code> internally with no way to pass flags, so on a production box it would happily pull in dev dependencies. <code>deploy</code> exports <code>COMPOSER_NO_DEV=1</code> before calling it, which Composer honours. If you ever run repatch by hand on a server, do the same:</p>
<pre><code>COMPOSER_NO_DEV=1 composer patches-repatch</code></pre>
</div>
</details>
<details>
<summary><code>magento/magento2-base</code> patches also touch project root files</summary>
<div class="details-body">
<p>Some Adobe patches land on files that <code>magento/magento-composer-installer</code> copies from the package into the project root, <code>lib/web/underscore.js</code> being the usual one. Patching <code>vendor/</code> alone is not enough: the copy has to run again afterwards.</p>
<p>Repatch uninstalls and reinstalls the package, so the copy does re-run. Another reason not to rely on a plain <code>composer install</code> here.</p>
<p>With <code>magento-force: override</code> set, that reinstall can also overwrite root files you customised. Run <code>git status</code> after <code>apply</code>. If <code>.htaccess</code> or similar shows up as modified, check the diff before committing.</p>
</div>
</details>
<details>
<summary>Patch flavours: cweagans and vaimo</summary>
<div class="details-body">
<p>Adobe ships its patches rooted at the project root, which neither plugin can apply, since both run the patcher from inside the installed package. Qoliber republishes each release in two forms:</p>
<ul>
<li><strong>cweagans</strong> one patch per Composer package, path prefixes stripped, applies at <code>-p1</code> from inside the package</li>
<li><strong>vaimo</strong> Adobe's original file untouched, applied from the project root by <code>vaimo/composer-patches</code></li>
</ul>
<p>This project runs <code>cweagans/composer-patches</code> v2, so always take the <code>cweagans/</code> folder. <code>tools/patches find</code> already prefers it.</p>
</div>
</details>
<details>
<summary>Isolated patches are cumulative</summary>
<div class="details-body">
<p>Within a release line, the August set builds on the July set, and both assume you are already on the latest security-only patch release. Keep every release folder in <code>patches/</code> rather than replacing the old one with the new one. The store here holds four of them and that is correct.</p>
</div>
</details>
<h2>Where the patches come from</h2>
<div class="cards">
<div class="card">
<h3>qoliber</h3>
<p>Kuba's repos, published in both cweagans and vaimo form.</p>
<p>
<a href="https://github.com/qoliber/magento-open-source-VULN-39341">magento-open-source-VULN-39341</a> (VULN-39341, 2026-09-001)<br>
<a href="https://github.com/qoliber/APSB26-92-patches">APSB26-92-patches</a> (2026-08-001)<br>
<a href="https://github.com/qoliber/2026-07-001-CE-cweagans-patches">2026-07-001-CE-cweagans-patches</a>
</p>
</div>
<div class="card">
<h3>Siteation</h3>
<p><a href="https://github.com/Siteation/magento2-security-patches-249">magento2-security-patches-249</a>, the wrapper package this project used before. Still a fine place to read a patch from, just no longer installed as a dependency.</p>
</div>
<div class="card">
<h3>Adobe</h3>
<p>The bulletins themselves, for deciding whether a release is urgent: <a href="https://helpx.adobe.com/security/security-bulletin.html">helpx.adobe.com/security</a>.</p>
</div>
</div>
<p class="foot">
<a href="https://github.com/Siteation/magento2-patch-workflow">Siteation/magento2-patch-workflow</a>.
Run <code>tools/patches status</code> to see what this particular shop is carrying.
</p>
</div>
<dialog id="d-find">
<div class="dlg-head"><div><h3>tools/patches find</h3><div class="sub">Local, read only</div></div><button class="x" data-close>×</button></div>
<div class="dlg-body">
<p>Asks GitHub for the whole file tree of a patch repo, keeps every folder containing <code>.patch</code> files, and filters to the ones naming this shop's Magento version.</p>
<pre><code>tools/patches find qoliber/magento-open-source-VULN-39341</code></pre>
<p>It prints the folder paths you feed to <code>add</code>. Skip this step whenever you already have the folder URL open in a browser tab.</p>
<p><strong>If it finds nothing:</strong> the repo may name versions differently, for example <code>2-4-9-aug-2026</code> instead of <code>2.4.9</code>. The command falls back to listing everything so you can pick by eye.</p>
</div>
</dialog>
<dialog id="d-add">
<div class="dlg-head"><div><h3>tools/patches add</h3><div class="sub">Local, writes to patches/</div></div><button class="x" data-close>×</button></div>
<div class="dlg-body">
<p>Downloads the <code>.patch</code> files, keeps the upstream <code>composer.json</code> fragment as <code>source.json</code> for its descriptions, notes the origin, then re-runs <code>sync</code>.</p>
<pre><code>tools/patches add qoliber/magento-open-source-VULN-39341 2026-09-001/cweagans/2.4.9</code></pre>
<p>Nothing is applied yet. At this point the release is in the repo and in the lock, and <code>verify</code> will show it red.</p>
<p><strong>Watch for:</strong> take the <code>cweagans</code> folder, not the <code>vaimo</code> one. The patch files differ in where they expect to be applied from.</p>
</div>
</dialog>
<dialog id="d-apply">
<div class="dlg-head"><div><h3>tools/patches apply</h3><div class="sub">Local, rewrites vendor/</div></div><button class="x" data-close>×</button></div>
<div class="dlg-body">
<p>Runs <code>sync</code>, then <code>composer patches-repatch</code>, then <code>verify</code>. Repatch deletes every patched package and reinstalls it, which is the only reliable way to get a new patch into code that Composer otherwise has no reason to touch.</p>
<p>Takes a minute or two on a full Magento install. Ends with a list of every patch and a green tick.</p>
<p><strong>If a patch fails</strong> the command stops with Composer's own error. Usually the release does not match the Magento version, so check what <code>find</code> reported.</p>
<p><strong>Afterwards</strong> run <code>git status</code>. The <code>magento2-base</code> reinstall can overwrite customised root files.</p>
</div>
</dialog>
<dialog id="d-test">
<div class="dlg-head"><div><h3>Test the shop</h3><div class="sub">Local, the part no script can do</div></div><button class="x" data-close>×</button></div>
<div class="dlg-body">
<pre><code>mage cache:flush
mage setup:di:compile</code></pre>
<p>Then click the areas the release actually touched. Read the descriptions in <code>tools/patches verify</code> output: they name the change. For 2026-09-001 that means admin order create, PayPal checkout, GraphQL customer queries, instant purchase, and the export file grid.</p>
<p>Check <code>var/log/system.log</code> and <code>var/log/exception.log</code> afterwards. A patch that applied cleanly but broke a plugin or a preference shows up here rather than on screen.</p>
</div>
</dialog>
<dialog id="d-commit">
<div class="dlg-head"><div><h3>Commit and push</h3><div class="sub">Local</div></div><button class="x" data-close>×</button></div>
<div class="dlg-body">
<pre><code>git add patches patches.json patches.lock.json tools/patches
git commit -m "ADD 2026-09-001 security patches"
git push</code></pre>
<p>All four belong in the repo. <code>patches.lock.json</code> especially: it is what the server reads, and committing it is what makes the live result identical to the one you just tested.</p>
<p><code>vendor/</code> is not committed. The server rebuilds it from <code>composer.lock</code> and re-patches from the lock.</p>
</div>
</dialog>
<dialog id="d-pull">
<div class="dlg-head"><div><h3>git pull</h3><div class="sub">Live</div></div><button class="x" data-close>×</button></div>
<div class="dlg-body">
<p>Brings across the patch files, the list, the lock, and the tool. Nothing is applied by the pull itself.</p>
<p>Run <code>tools/patches status</code> straight after if you want to see what arrived before changing anything.</p>
</div>
</dialog>
<dialog id="d-deploy">
<div class="dlg-head"><div><h3>tools/patches deploy</h3><div class="sub">Live, changes the running shop</div></div><button class="x" data-close>×</button></div>
<div class="dlg-body">
<p>Seven steps, stopping at the first failure:</p>
<pre><code>composer install --no-dev
COMPOSER_NO_DEV=1 composer patches-repatch
tools/patches verify
bin/magento setup:upgrade --keep-generated
bin/magento setup:di:compile
bin/magento setup:static-content:deploy -f $PATCHES_LOCALES
bin/magento cache:flush</code></pre>
<p>The verify step sits deliberately before <code>setup:upgrade</code>. If a patch did not land, the deploy stops while the shop is still running the old code rather than half way through a schema upgrade.</p>
<p><strong>Maintenance mode:</strong> the script does not enable it. Add <code>bin/magento maintenance:enable</code> around it yourself if a shop needs it.</p>
</div>
</dialog>
<dialog id="d-verify">
<div class="dlg-head"><div><h3>tools/patches verify</h3><div class="sub">Anywhere, read only</div></div><button class="x" data-close>×</button></div>
<div class="dlg-body">
<p>Takes every patch in <code>patches.lock.json</code> and tries to reverse it as a dry run against the installed package. If reversing works, the patch is in. This checks the code on disk, not what Composer believes about it.</p>
<pre><code>tools/patches verify; echo $?</code></pre>
<p>Exit 0 means all applied, exit 2 means at least one is not. Safe to run on a live server at any time, and worth putting on a cron so a silent unpatch gets noticed.</p>
</div>
</dialog>
<script>
document.querySelectorAll('.node[data-dlg]').forEach(function (btn) {
btn.addEventListener('click', function () {
var dlg = document.getElementById(btn.dataset.dlg);
if (dlg) { dlg.showModal(); }
});
});
document.querySelectorAll('dialog').forEach(function (dlg) {
dlg.querySelectorAll('[data-close]').forEach(function (x) {
x.addEventListener('click', function () { dlg.close(); });
});
dlg.addEventListener('click', function (e) {
if (e.target === dlg) { dlg.close(); }
});
});
</script>
</body>
</html>