@uxf/scripts 11.123.0 → 11.125.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,20 +19,20 @@ Most binaries are already available in the UXF Docker images, so CI jobs can cal
19
19
  yarn add -D @uxf/scripts
20
20
  ```
21
21
 
22
- Requires **Node.js >= 24** (`engines.node`). No peer dependencies. Runtime dependencies (`axios`, `cheerio`, `dayjs`, `fast-glob`, `got`, `madge`, `robots-txt-parser`, `semver`, `yaml`, `yargs`) are bundled.
22
+ Requires **Node.js >= 24** (`engines.node`). No peer dependencies. Runtime dependencies (`cheerio`, `dayjs`, `fast-glob`, `got`, `rev-dep`, `robots-txt-parser`, `semver`, `yaml`, `yargs`) are bundled.
23
23
 
24
24
  ## Commands
25
25
 
26
- | Command | Purpose |
27
- | --- | --- |
28
- | `uxf-release` | Generate release notes from Git commits, create a GitLab release, notify Slack / Google Chat. |
29
- | `uxf-push-notifier` | Notify Google Chat when a merge request is merged into (or code is pushed to) the default branch. |
30
- | `uxf-dependencies-check` | Detect version conflicts and missing/mismatched peer deps across `@uxf/*` packages. |
31
- | `uxf-audit` | Print a JSON report of Node / Yarn / Next / React and installed `@uxf/*` versions. |
32
- | `uxf-claude-sync` | Download global guidelines from a URL into a `CLAUDE.md` file. |
33
- | `uxf-i18n-namespaces-gen` | Generate an `i18n-pages.json` mapping routes to the translation namespaces they use. |
34
- | `uxf-sitemap-check` | Crawl a sitemap and check all URLs for HTTP errors, duplicates, and missing pages. |
35
- | `uxf-sitemap-meta-export` | Crawl a sitemap and export page meta tags (title, description, OG) to `export.csv`. |
26
+ | Command | Purpose |
27
+ | ------------------------- | ------------------------------------------------------------------------------------------------- |
28
+ | `uxf-release` | Generate release notes from Git commits, create a GitLab release, notify Slack / Google Chat. |
29
+ | `uxf-push-notifier` | Notify Google Chat when a merge request is merged into (or code is pushed to) the default branch. |
30
+ | `uxf-dependencies-check` | Detect version conflicts and missing/mismatched peer deps across `@uxf/*` packages. |
31
+ | `uxf-audit` | Print a JSON report of Node / Yarn / Next / React and installed `@uxf/*` versions. |
32
+ | `uxf-claude-sync` | Download global guidelines from a URL into a `CLAUDE.md` file. |
33
+ | `uxf-i18n-namespaces-gen` | Generate an `i18n-pages.json` mapping routes to the translation namespaces they use. |
34
+ | `uxf-sitemap-check` | Crawl a sitemap and check all URLs for HTTP errors, duplicates, and missing pages. |
35
+ | `uxf-sitemap-meta-export` | Crawl a sitemap and export page meta tags (title, description, OG) to `export.csv`. |
36
36
 
37
37
  Every command accepts `-h` / `--help`.
38
38
 
@@ -70,16 +70,16 @@ Set `GITLAB_TOKEN` in the GitLab CI/CD variables, then:
70
70
  ```yaml
71
71
  # .gitlab-ci.yml
72
72
  stages:
73
- - release
73
+ - release
74
74
 
75
75
  release:
76
- image: node:24
77
- stage: release
78
- before_script:
79
- - yarn global add @uxf/scripts
80
- script:
81
- # ... deploy project ...
82
- - uxf-release
76
+ image: node:24
77
+ stage: release
78
+ before_script:
79
+ - yarn global add @uxf/scripts
80
+ script:
81
+ # ... deploy project ...
82
+ - uxf-release
83
83
  ```
84
84
 
85
85
  ### uxf-push-notifier
@@ -100,17 +100,17 @@ Options:
100
100
 
101
101
  ```yaml
102
102
  stages:
103
- - notify
103
+ - notify
104
104
 
105
105
  notify-push:
106
- image: node:24
107
- stage: notify
108
- except: [schedules]
109
- only:
110
- - develop # default branch
111
- script:
112
- - yarn global add @uxf/scripts # already present in UXF Docker images
113
- - uxf-push-notifier --google-chat-webhook-url=$GOOGLE_CHAT_WEBHOOK
106
+ image: node:24
107
+ stage: notify
108
+ except: [schedules]
109
+ only:
110
+ - develop # default branch
111
+ script:
112
+ - yarn global add @uxf/scripts # already present in UXF Docker images
113
+ - uxf-push-notifier --google-chat-webhook-url=$GOOGLE_CHAT_WEBHOOK
114
114
  ```
115
115
 
116
116
  ### uxf-dependencies-check
@@ -159,10 +159,10 @@ For genuine declarer conflicts that you've consciously accepted — e.g. two `@u
159
159
 
160
160
  ```json
161
161
  {
162
- "name": "@uxf/wysiwyg",
163
- "uxf": {
164
- "ignoredConflicts": ["some-shared-dep"]
165
- }
162
+ "name": "@uxf/wysiwyg",
163
+ "uxf": {
164
+ "ignoredConflicts": ["some-shared-dep"]
165
+ }
166
166
  }
167
167
  ```
168
168
 
@@ -171,7 +171,7 @@ Ignored conflicts are still printed as informational output but do **not** cause
171
171
  #### What it checks
172
172
 
173
173
  - Parses the lock file (`yarn.lock` / `package-lock.json` / `pnpm-lock.yaml` / `bun.lock`) to find dependencies with multiple installed versions.
174
- - For each candidate, resolves the version actually seen by each *declarer* (the project itself and any `@uxf/*` package that directly declares the dep). A conflict is reported only when these declarers land on different versions — transitive-only duplicates from unrelated third-party packages are filtered out.
174
+ - For each candidate, resolves the version actually seen by each _declarer_ (the project itself and any `@uxf/*` package that directly declares the dep). A conflict is reported only when these declarers land on different versions — transitive-only duplicates from unrelated third-party packages are filtered out.
175
175
  - Scans all `@uxf/*` packages in your `dependencies` / `devDependencies`.
176
176
  - Reads each package's `peerDependencies` from `node_modules`.
177
177
  - Skips `@uxf/*`, `react`, `react-dom`, and `next` (you manage these yourself).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uxf/scripts",
3
- "version": "11.123.0",
3
+ "version": "11.125.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -30,12 +30,11 @@
30
30
  "node": ">=24"
31
31
  },
32
32
  "dependencies": {
33
- "axios": "^1.19.0",
34
33
  "cheerio": "1.2.0",
35
34
  "dayjs": "^1.11.21",
36
35
  "fast-glob": "3.3.3",
37
36
  "got": "15.1.0",
38
- "madge": "8.0.0",
37
+ "rev-dep": "3.0.0",
39
38
  "robots-txt-parser": "2.0.3",
40
39
  "semver": "^7.8.5",
41
40
  "yaml": "2.9.0",
@@ -46,7 +45,7 @@
46
45
  "@types/react": "18.3.31",
47
46
  "@types/react-dom": "18.3.7",
48
47
  "@types/semver": "^7.7.1",
49
- "@uxf/core": "11.123.0",
50
- "@uxf/ui": "11.123.0"
48
+ "@uxf/core": "11.124.0",
49
+ "@uxf/ui": "11.125.0"
51
50
  }
52
51
  }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Drobná náhrada yargsu pro notifiery, které musí běžet v CI bez `npm install`
3
+ * — viz `docs/decisions/package-manager.md`. Ostatní skripty v balíčku yargs
4
+ * používají dál; tenhle helper řeší jen `--help` a jednu hodnotovou option.
5
+ */
6
+
7
+ /**
8
+ * @param {string[]} args
9
+ * @param {string[]} names např. `["-h", "--help"]`
10
+ */
11
+ function hasFlag(args, names) {
12
+ return args.some((arg) => names.includes(arg));
13
+ }
14
+
15
+ /**
16
+ * Přečte hodnotu option ve tvaru `--name value`, `--name=value`, `-n value` i `-n=value`.
17
+ *
18
+ * @param {string[]} args
19
+ * @param {string[]} names např. `["-g", "--google-chat-webhook-url"]`
20
+ * @returns {string | undefined}
21
+ */
22
+ function readOption(args, names) {
23
+ for (let index = 0; index < args.length; index++) {
24
+ const arg = args[index];
25
+
26
+ if (names.includes(arg)) {
27
+ return args[index + 1];
28
+ }
29
+
30
+ const matched = names.find((name) => arg.startsWith(`${name}=`));
31
+
32
+ if (matched) {
33
+ return arg.slice(matched.length + 1);
34
+ }
35
+ }
36
+
37
+ return undefined;
38
+ }
39
+
40
+ module.exports = {
41
+ hasFlag,
42
+ readOption,
43
+ };
@@ -0,0 +1,61 @@
1
+ const { hasFlag, readOption } = require("./cli-args");
2
+
3
+ describe("hasFlag", () => {
4
+ it("finds the long form", () => {
5
+ expect(hasFlag(["--help"], ["-h", "--help"])).toBe(true);
6
+ });
7
+
8
+ it("finds the short form", () => {
9
+ expect(hasFlag(["-h"], ["-h", "--help"])).toBe(true);
10
+ });
11
+
12
+ it("finds the flag among other arguments", () => {
13
+ expect(hasFlag(["-g", "https://x", "--help"], ["-h", "--help"])).toBe(true);
14
+ });
15
+
16
+ it("returns false when absent", () => {
17
+ expect(hasFlag(["-g", "https://x"], ["-h", "--help"])).toBe(false);
18
+ expect(hasFlag([], ["-h", "--help"])).toBe(false);
19
+ });
20
+
21
+ it("does not match a flag that is only a prefix", () => {
22
+ expect(hasFlag(["--help-me"], ["-h", "--help"])).toBe(false);
23
+ });
24
+ });
25
+
26
+ describe("readOption", () => {
27
+ const NAMES = ["-g", "--google-chat-webhook-url"];
28
+
29
+ it("reads `--name value`", () => {
30
+ expect(readOption(["--google-chat-webhook-url", "https://x"], NAMES)).toBe("https://x");
31
+ });
32
+
33
+ it("reads `--name=value` — the form used in .gitlab-ci.yml", () => {
34
+ expect(readOption(["--google-chat-webhook-url=https://x"], NAMES)).toBe("https://x");
35
+ });
36
+
37
+ it("reads `-n value`", () => {
38
+ expect(readOption(["-g", "https://x"], NAMES)).toBe("https://x");
39
+ });
40
+
41
+ it("reads `-n=value`", () => {
42
+ expect(readOption(["-g=https://x"], NAMES)).toBe("https://x");
43
+ });
44
+
45
+ it("keeps `=` inside the value", () => {
46
+ expect(readOption(["--google-chat-webhook-url=https://x?key=abc"], NAMES)).toBe("https://x?key=abc");
47
+ });
48
+
49
+ it("finds the option among other arguments", () => {
50
+ expect(readOption(["--verbose", "-g", "https://x", "--other"], NAMES)).toBe("https://x");
51
+ });
52
+
53
+ it("returns undefined when the option is missing", () => {
54
+ expect(readOption(["--verbose"], NAMES)).toBeUndefined();
55
+ expect(readOption([], NAMES)).toBeUndefined();
56
+ });
57
+
58
+ it("returns undefined when the option is last and has no value", () => {
59
+ expect(readOption(["-g"], NAMES)).toBeUndefined();
60
+ });
61
+ });
@@ -1,24 +1,41 @@
1
1
  const { env } = require("process");
2
- const { create } = require("axios");
3
- const dayjs = require("dayjs");
2
+ const { request } = require("./http");
4
3
 
5
- const axios = create({
6
- baseURL: `${env.CI_SERVER_URL}/api/v4`,
7
- headers: {
8
- Authorization: `Bearer ${env.GITLAB_TOKEN}`,
9
- },
10
- });
4
+ /**
5
+ * @param {string} url
6
+ * @param {{method?: string, headers?: Record<string, string>, params?: Record<string, any>, body?: any}} options
7
+ */
8
+ function gitlabRequest(url, options = {}) {
9
+ return request(url, {
10
+ ...options,
11
+ baseUrl: `${env.CI_SERVER_URL}/api/v4`,
12
+ headers: { Authorization: `Bearer ${env.GITLAB_TOKEN}`, ...options.headers },
13
+ });
14
+ }
15
+
16
+ /** Lokální čas, stejně jako `dayjs().format(...)`, které tohle nahradilo. */
17
+ function formatTagTimestamp(date = new Date()) {
18
+ const pad = (value) => String(value).padStart(2, "0");
19
+
20
+ return [
21
+ date.getFullYear(),
22
+ pad(date.getMonth() + 1),
23
+ pad(date.getDate()),
24
+ pad(date.getHours()),
25
+ pad(date.getMinutes()),
26
+ ].join("-");
27
+ }
11
28
 
12
29
  /**
13
30
  *
14
31
  * @param {string} url
15
- * @param {axios.AxiosRequestConfig<any>} config
32
+ * @param {{params?: Record<string, any>}} config
16
33
  */
17
34
  async function getAll(url, config) {
18
35
  let nextPage = "1";
19
36
  const data = [];
20
37
  do {
21
- const response = await axios.get(url, {
38
+ const response = await gitlabRequest(url, {
22
39
  ...config,
23
40
  params: {
24
41
  ...config.params,
@@ -26,7 +43,7 @@ async function getAll(url, config) {
26
43
  page: nextPage,
27
44
  },
28
45
  });
29
- nextPage = response.headers["x-next-page"];
46
+ nextPage = response.headers.get("x-next-page");
30
47
  data.push(...response.data);
31
48
  } while (nextPage);
32
49
 
@@ -38,15 +55,15 @@ async function loadCommits(from, ref_name = "master") {
38
55
 
39
56
  let nextPage = "1";
40
57
  do {
41
- const response = await axios.get(`/projects/${env.CI_PROJECT_ID}/repository/commits`, {
58
+ const response = await gitlabRequest(`/projects/${env.CI_PROJECT_ID}/repository/commits`, {
42
59
  params: {
43
60
  ref_name,
44
- since: from ? dayjs(from).toISOString() : undefined,
61
+ since: from ? new Date(from).toISOString() : undefined,
45
62
  per_page: "100",
46
63
  page: nextPage,
47
64
  },
48
65
  });
49
- nextPage = response.headers["x-next-page"];
66
+ nextPage = response.headers.get("x-next-page");
50
67
  commits.push(...response.data);
51
68
  } while (nextPage);
52
69
 
@@ -61,7 +78,7 @@ async function findMigrationFiles(commits) {
61
78
  await Promise.all(
62
79
  commits.map(async (commit) => {
63
80
  try {
64
- const { data: diffs } = await axios.get(
81
+ const { data: diffs } = await gitlabRequest(
65
82
  `/projects/${env.CI_PROJECT_ID}/repository/commits/${commit.id}/diff`,
66
83
  );
67
84
  diffs.forEach(({ new_path }) => {
@@ -81,7 +98,7 @@ async function findMigrationFiles(commits) {
81
98
  }
82
99
 
83
100
  async function getLastTag(tagPrefix = "release-", ref_name = "master") {
84
- const { data: commits } = await axios.get(`/projects/${env.CI_PROJECT_ID}/repository/commits`, {
101
+ const { data: commits } = await gitlabRequest(`/projects/${env.CI_PROJECT_ID}/repository/commits`, {
85
102
  params: {
86
103
  ref_name,
87
104
  per_page: "100",
@@ -90,9 +107,11 @@ async function getLastTag(tagPrefix = "release-", ref_name = "master") {
90
107
 
91
108
  const commitIds = commits.map((commit) => commit.id);
92
109
 
93
- const { data: tags } = await axios.get(`/projects/${env.CI_PROJECT_ID}/repository/tags`, {
110
+ // Pozor: `per_page` tu záměrně chybí. V axios verzi bylo omylem mimo `params`,
111
+ // takže se na GitLab nikdy neposlalo a platí default (20 tagů). Doplnit ho by
112
+ // změnilo, který tag se najde, což je zásah do release flow — řešit zvlášť.
113
+ const { data: tags } = await gitlabRequest(`/projects/${env.CI_PROJECT_ID}/repository/tags`, {
94
114
  params: { search: `^${tagPrefix}` },
95
- per_page: "100",
96
115
  });
97
116
 
98
117
  const tag = tags.find((tag) => commitIds.includes(tag.commit.id)) ?? null;
@@ -107,20 +126,23 @@ async function getLastTag(tagPrefix = "release-", ref_name = "master") {
107
126
  }
108
127
 
109
128
  async function createRelease(description, tagPrefix = "release-", dryRun = false) {
110
- const tag = tagPrefix + dayjs().format("YYYY-MM-DD-HH-mm");
129
+ const tag = tagPrefix + formatTagTimestamp();
111
130
 
112
131
  if (dryRun) {
113
132
  console.log(`\n🎉🎉🎉 Release "${tag}" published (skipped in dry run)\n\n${description}`);
114
133
  return;
115
134
  }
116
135
 
117
- await axios.post(`/projects/${env.CI_PROJECT_ID}/releases`, { description, tag_name: tag, ref: "master" });
136
+ await gitlabRequest(`/projects/${env.CI_PROJECT_ID}/releases`, {
137
+ method: "POST",
138
+ body: { description, tag_name: tag, ref: "master" },
139
+ });
118
140
 
119
141
  console.log(`\n🎉🎉🎉 Release "${tag}" published\n\n${description}`);
120
142
  }
121
143
 
122
144
  function getSingleMergeRequestChanges(projectId, mr_iid) {
123
- return axios.get(`/projects/${projectId}/merge_requests/${mr_iid}/changes`).then((r) => r.data);
145
+ return gitlabRequest(`/projects/${projectId}/merge_requests/${mr_iid}/changes`).then((r) => r.data);
124
146
  }
125
147
 
126
148
  /**
@@ -0,0 +1,207 @@
1
+ const GitLab = require("./gitlab");
2
+
3
+ const originalFetch = globalThis.fetch;
4
+ const { CI_SERVER_URL, GITLAB_TOKEN, CI_PROJECT_ID } = process.env;
5
+
6
+ /**
7
+ * @param {Array<{body: any, nextPage?: string, status?: number}>} pages
8
+ * odpovědi v pořadí, v jakém je fetch vrátí
9
+ */
10
+ function stubFetch(pages) {
11
+ const calls = [];
12
+ let index = 0;
13
+
14
+ globalThis.fetch = async (url, init) => {
15
+ calls.push({ url, init });
16
+ const page = pages[Math.min(index++, pages.length - 1)];
17
+ const status = page.status ?? 200;
18
+
19
+ return {
20
+ ok: status >= 200 && status < 300,
21
+ status,
22
+ headers: {
23
+ get: (name) => {
24
+ if (name === "x-next-page") {
25
+ return page.nextPage ?? "";
26
+ }
27
+ return name.toLowerCase() === "content-type" ? "application/json" : null;
28
+ },
29
+ },
30
+ text: async () => JSON.stringify(page.body),
31
+ };
32
+ };
33
+
34
+ return calls;
35
+ }
36
+
37
+ beforeEach(() => {
38
+ process.env.CI_SERVER_URL = "https://git.test";
39
+ process.env.GITLAB_TOKEN = "test-token";
40
+ process.env.CI_PROJECT_ID = "42";
41
+ jest.spyOn(console, "log").mockImplementation(() => {});
42
+ });
43
+
44
+ afterEach(() => {
45
+ globalThis.fetch = originalFetch;
46
+ process.env.CI_SERVER_URL = CI_SERVER_URL;
47
+ process.env.GITLAB_TOKEN = GITLAB_TOKEN;
48
+ process.env.CI_PROJECT_ID = CI_PROJECT_ID;
49
+ jest.restoreAllMocks();
50
+ });
51
+
52
+ describe("authentication and base url", () => {
53
+ it("prefixes the API path and sends the bearer token", async () => {
54
+ const calls = stubFetch([{ body: [] }]);
55
+
56
+ await GitLab.getAllProjects();
57
+
58
+ expect(calls[0].url).toContain("https://git.test/api/v4/projects");
59
+ expect(calls[0].init.headers.Authorization).toBe("Bearer test-token");
60
+ });
61
+ });
62
+
63
+ describe("getAll pagination", () => {
64
+ it("follows x-next-page until it is empty and concatenates the pages", async () => {
65
+ const calls = stubFetch([
66
+ { body: [{ id: 1 }], nextPage: "2" },
67
+ { body: [{ id: 2 }], nextPage: "3" },
68
+ { body: [{ id: 3 }], nextPage: "" },
69
+ ]);
70
+
71
+ const projects = await GitLab.getAllProjects();
72
+
73
+ expect(projects).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
74
+ expect(calls).toHaveLength(3);
75
+ expect(calls[0].url).toContain("page=1");
76
+ expect(calls[1].url).toContain("page=2");
77
+ expect(calls[2].url).toContain("page=3");
78
+ });
79
+
80
+ it("stops after a single page when there is no next page", async () => {
81
+ const calls = stubFetch([{ body: [{ id: 1 }] }]);
82
+
83
+ await GitLab.getAllProjects();
84
+
85
+ expect(calls).toHaveLength(1);
86
+ });
87
+
88
+ it("always asks for 100 items per page", async () => {
89
+ const calls = stubFetch([{ body: [] }]);
90
+
91
+ await GitLab.getAllProjects();
92
+
93
+ expect(calls[0].url).toContain("per_page=100");
94
+ });
95
+ });
96
+
97
+ describe("getAllMergeRequests", () => {
98
+ it("excludes work in progress by default", async () => {
99
+ const calls = stubFetch([{ body: [] }]);
100
+
101
+ await GitLab.getAllMergeRequests(false);
102
+
103
+ expect(calls[0].url).toContain("wip=no");
104
+ });
105
+
106
+ it("omits the wip filter when work in progress is included", async () => {
107
+ const calls = stubFetch([{ body: [] }]);
108
+
109
+ await GitLab.getAllMergeRequests(true);
110
+
111
+ expect(calls[0].url).not.toContain("wip=");
112
+ });
113
+ });
114
+
115
+ describe("loadCommits", () => {
116
+ it("converts the `from` timestamp to ISO for the `since` parameter", async () => {
117
+ const calls = stubFetch([{ body: [] }]);
118
+
119
+ await GitLab.loadCommits("2026-01-02T03:04:05.000Z");
120
+
121
+ expect(decodeURIComponent(calls[0].url)).toContain("since=2026-01-02T03:04:05.000Z");
122
+ });
123
+
124
+ it("omits `since` when there is no starting point", async () => {
125
+ const calls = stubFetch([{ body: [] }]);
126
+
127
+ await GitLab.loadCommits(null);
128
+
129
+ expect(calls[0].url).not.toContain("since=");
130
+ });
131
+
132
+ it("drops the boundary commit so the last release is not counted twice", async () => {
133
+ const from = "2026-01-02T03:04:05.000Z";
134
+ stubFetch([
135
+ {
136
+ body: [
137
+ { id: "a", created_at: from },
138
+ { id: "b", created_at: "2026-01-03T00:00:00.000Z" },
139
+ ],
140
+ },
141
+ ]);
142
+
143
+ const commits = await GitLab.loadCommits(from);
144
+
145
+ expect(commits.map((c) => c.id)).toEqual(["b"]);
146
+ });
147
+
148
+ it("uses the requested ref", async () => {
149
+ const calls = stubFetch([{ body: [] }]);
150
+
151
+ await GitLab.loadCommits(null, "develop");
152
+
153
+ expect(calls[0].url).toContain("ref_name=develop");
154
+ });
155
+ });
156
+
157
+ describe("getSingleMergeRequestChanges", () => {
158
+ it("returns the response body", async () => {
159
+ stubFetch([{ body: { changes_count: "7" } }]);
160
+
161
+ await expect(GitLab.getSingleMergeRequestChanges(1, 2)).resolves.toEqual({ changes_count: "7" });
162
+ });
163
+ });
164
+
165
+ describe("createRelease", () => {
166
+ it("builds a tag from the prefix and a local timestamp", async () => {
167
+ const calls = stubFetch([{ body: {} }]);
168
+
169
+ await GitLab.createRelease("poznámky", "release-");
170
+
171
+ expect(calls[0].init.method).toBe("POST");
172
+ expect(JSON.parse(calls[0].init.body)).toEqual({
173
+ description: "poznámky",
174
+ tag_name: expect.stringMatching(/^release-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}$/),
175
+ ref: "master",
176
+ });
177
+ });
178
+
179
+ it("sends nothing in dry run mode", async () => {
180
+ const calls = stubFetch([{ body: {} }]);
181
+
182
+ await GitLab.createRelease("poznámky", "release-", true);
183
+
184
+ expect(calls).toHaveLength(0);
185
+ });
186
+ });
187
+
188
+ describe("findMigrationFiles", () => {
189
+ it("collects api migration files and ignores everything else", async () => {
190
+ stubFetch([
191
+ {
192
+ body: [{ new_path: "api/migrations/Version20260101120000.php" }, { new_path: "web/src/app.tsx" }],
193
+ },
194
+ ]);
195
+
196
+ await expect(GitLab.findMigrationFiles([{ id: "abc" }])).resolves.toEqual([
197
+ "api/migrations/Version20260101120000.php",
198
+ ]);
199
+ });
200
+
201
+ it("survives a failing commit diff instead of rejecting the whole run", async () => {
202
+ jest.spyOn(console, "error").mockImplementation(() => {});
203
+ stubFetch([{ body: { message: "nope" }, status: 500 }]);
204
+
205
+ await expect(GitLab.findMigrationFiles([{ id: "abc" }])).resolves.toEqual([]);
206
+ });
207
+ });
@@ -1,7 +1,5 @@
1
1
  const { env } = require("process");
2
- const { create } = require("axios");
3
-
4
- const axios = create({});
2
+ const { request } = require("./http");
5
3
 
6
4
  /**
7
5
  * @see https://developers.google.com/chat/api/reference/rest/v1/cards-v1
@@ -16,7 +14,7 @@ async function chatPostMessage(data, config = {}) {
16
14
 
17
15
  if (webhookUrl && !config.dryRun) {
18
16
  try {
19
- await axios.post(webhookUrl, { ...data });
17
+ await request(webhookUrl, { method: "POST", body: { ...data } });
20
18
  } catch (error) {
21
19
  process.stderr.write("GOOGLE CHAT: chat.postMessage - error");
22
20
  console.error(error);
@@ -30,16 +28,16 @@ async function chatPostMessage(data, config = {}) {
30
28
  *
31
29
  * @param cardsV2
32
30
  * @param {{webhookUrl?: string}} config
33
- * @returns {Promise<axios.AxiosResponse<any>>}
31
+ * @returns {Promise<any>}
34
32
  */
35
33
  async function messageCardsV2(cardsV2, config = {}) {
36
- return axios
37
- .post(
38
- config.webhookUrl ?? env.GOOGLE_WEBHOOK_URL,
39
- { cardsV2 },
40
- { headers: { "Content-Type": "application/json; charset=UTF-8" } },
41
- )
42
- .then((r) => r.data);
34
+ const response = await request(config.webhookUrl ?? env.GOOGLE_WEBHOOK_URL, {
35
+ method: "POST",
36
+ headers: { "Content-Type": "application/json; charset=UTF-8" },
37
+ body: { cardsV2 },
38
+ });
39
+
40
+ return response.data;
43
41
  }
44
42
 
45
43
  module.exports = {