Skip to content

Commit 5ecd8a8

Browse files
committed
feat: add process docs and scripts for yarn.lock only backports
Signed-off-by: Jessica He <jhe@redhat.com>
1 parent c5d9f95 commit 5ecd8a8

7 files changed

Lines changed: 984 additions & 8 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules/
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# yarnlock-backport
2+
3+
Generate `0-cve-yarn-lock.patch` and `cve-backports.yaml` for overlay workspaces without bumping `source.json:repo-ref`.
4+
5+
Full workflow: [user-guide/06-patch-management.md](../../user-guide/06-patch-management.md).
6+
7+
```bash
8+
cd scripts/yarnlock-backport && npm install
9+
10+
export OVERLAY_WORKSPACE=<absolute-path>/workspaces/orchestrator
11+
export PLUGINS_REPO=<absolute-path>
12+
13+
yarnlock-backport prepare --release 1.10 --overlay-workspace "$OVERLAY_WORKSPACE" --plugins-repo "$PLUGINS_REPO"
14+
# … yarn up in plugins workspace …
15+
yarnlock-backport generate --release 1.10 --overlay-workspace "$OVERLAY_WORKSPACE" --plugins-repo "$PLUGINS_REPO" --cve 'CVE-…,CVE-…'
16+
```
17+
18+
Requires: Node.js, `git`, `yarn`, `patch`, `diff`, `npm`. `--overlay-workspace` and `--plugins-repo` must be **absolute** paths.
19+
20+
Fork clones need an `upstream` remote pointing at `https://github.com/redhat-developer/rhdh-plugin-export-overlays.git` (step 0 syncs the release branch from there).
21+
22+
```bash
23+
npm test
24+
```
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { describe, it } from 'node:test';
2+
import assert from 'node:assert/strict';
3+
4+
import { normalizeCveId, parseCveArg, parseCveCliToken, parseCveDetails, releaseBranch, requireLockfileChange, resolveOverlayGitRemote, resolvePluginsGitRemote } from './backport.ts';
5+
6+
describe('requireLockfileChange', () => {
7+
it('accepts when baseline and patched differ', () => {
8+
assert.doesNotThrow(() => requireLockfileChange('a', 'b'));
9+
});
10+
11+
it('rejects when lockfile is unchanged from repo-ref', () => {
12+
assert.throws(() => requireLockfileChange('same', 'same'), /yarn.lock unchanged from repo-ref baseline/);
13+
});
14+
});
15+
16+
describe('resolveOverlayGitRemote', () => {
17+
const forkRemotes = `origin\tgit@github.com:JessicaJHee/rhdh-plugin-export-overlays.git (fetch)
18+
origin\tgit@github.com:JessicaJHee/rhdh-plugin-export-overlays.git (push)
19+
upstream\thttps://github.com/redhat-developer/rhdh-plugin-export-overlays.git (fetch)
20+
upstream\thttps://github.com/redhat-developer/rhdh-plugin-export-overlays.git (push)`;
21+
22+
it('prefers upstream on fork clones', () => {
23+
assert.equal(
24+
resolveOverlayGitRemote(forkRemotes, 'git@github.com:JessicaJHee/rhdh-plugin-export-overlays.git'),
25+
'upstream',
26+
);
27+
});
28+
29+
it('uses origin for direct upstream clones', () => {
30+
const remotes = `origin\thttps://github.com/redhat-developer/rhdh-plugin-export-overlays.git (fetch)`;
31+
assert.equal(
32+
resolveOverlayGitRemote(remotes, 'https://github.com/redhat-developer/rhdh-plugin-export-overlays.git'),
33+
'origin',
34+
);
35+
});
36+
37+
it('errors when fork has no upstream remote', () => {
38+
const remotes = `origin\tgit@github.com:JessicaJHee/rhdh-plugin-export-overlays.git (fetch)`;
39+
assert.throws(
40+
() => resolveOverlayGitRemote(remotes, 'git@github.com:JessicaJHee/rhdh-plugin-export-overlays.git'),
41+
/no upstream remote/,
42+
);
43+
});
44+
});
45+
46+
describe('resolvePluginsGitRemote', () => {
47+
it('prefers upstream on fork clones', () => {
48+
const remotes = `origin\tgit@github.com:JessicaJHee/rhdh-plugins.git (fetch)
49+
upstream\thttps://github.com/redhat-developer/rhdh-plugins.git (fetch)`;
50+
assert.equal(resolvePluginsGitRemote(remotes, 'git@github.com:JessicaJHee/rhdh-plugins.git'), 'upstream');
51+
});
52+
});
53+
54+
describe('releaseBranch', () => {
55+
it('maps release version to branch name', () => {
56+
assert.equal(releaseBranch('1.10'), 'release-1.10');
57+
});
58+
59+
it('accepts branch name as-is', () => {
60+
assert.equal(releaseBranch('release-1.10'), 'release-1.10');
61+
});
62+
63+
it('rejects empty release', () => {
64+
assert.throws(() => releaseBranch(' '), /release is empty/);
65+
});
66+
});
67+
68+
describe('normalizeCveId', () => {
69+
it('accepts valid CVE ids', () => {
70+
assert.equal(normalizeCveId('cve-2026-1234'), 'CVE-2026-1234');
71+
});
72+
73+
it('rejects invalid CVE ids', () => {
74+
assert.throws(() => normalizeCveId('not-a-cve'), /not a CVE id/);
75+
});
76+
});
77+
78+
describe('parseCveCliToken', () => {
79+
it('parses CVE id without package override', () => {
80+
assert.deepEqual(parseCveCliToken('CVE-2026-1234'), ['CVE-2026-1234', []]);
81+
});
82+
83+
it('parses package override after slash', () => {
84+
assert.deepEqual(parseCveCliToken('CVE-2026-1234/axios'), ['CVE-2026-1234', ['axios']]);
85+
});
86+
});
87+
88+
describe('parseCveArg', () => {
89+
it('rejects duplicate CVE ids', () => {
90+
assert.throws(() => parseCveArg('CVE-2026-1234,CVE-2026-1234'), /duplicate CVE/);
91+
});
92+
});
93+
94+
describe('parseCveDetails', () => {
95+
it('returns empty details for rejected CVE records', () => {
96+
const result = parseCveDetails({ cveMetadata: { state: 'REJECTED' }, containers: { cna: {} } });
97+
assert.equal(result.name, '');
98+
assert.deepEqual(result.patch_versions, []);
99+
});
100+
101+
it('extracts lessThan fix version', () => {
102+
const result = parseCveDetails({
103+
cveMetadata: { state: 'PUBLISHED' },
104+
containers: {
105+
cna: {
106+
affected: [{ packageName: 'axios', versions: [{ status: 'affected', version: '0', lessThan: '1.18.1' }] }],
107+
},
108+
},
109+
});
110+
assert.equal(result.name, 'axios');
111+
assert.deepEqual(result.patch_versions, ['1.18.1']);
112+
});
113+
});
114+
115+
describe('maxVersion', () => {
116+
it('orders semver-like versions numerically', () => {
117+
const sorted = ['1.10.0', '1.9.0', '1.18.1'].sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
118+
assert.deepEqual(sorted, ['1.9.0', '1.10.0', '1.18.1']);
119+
});
120+
});

0 commit comments

Comments
 (0)