Skip to content

Commit d748ef1

Browse files
authored
Merge branch 'master' into copilot/add-check-for-obsolete-config-files
2 parents acc65c6 + 479641b commit d748ef1

4 files changed

Lines changed: 114 additions & 19 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ Example:
2727
-->
2828
### **WORK IN PROGRESS**
2929
- (@copilot) Added [W5048] check: warns about obsolete eslint/prettier config files (`.eslintignore`, `.eslintrc.json`, `.prettierignore`, `.prettierrc.js`, `.prettierrc.json`) when `@iobroker/eslint-config` is used as a devDependency.
30+
- (@copilot) Added [W4047]: warn when adapter is found in the latest repository but not yet available in the stable repository. Related to [#820].
31+
- (@copilot) Added `[E9506]`: error when an i18n directory is explicitly excluded by `.npmignore`, which would cause translations to be missing from the npm package.
32+
- (@copilot) Added `[E9507]`: error when an i18n directory is present in the repository but not covered by the `"files"` field in `package.json`, which would cause translations to be missing from the npm package.
3033

3134
### 5.9.1 (2026-04-08)
3235
- (@copilot) Added `isNewAdapter` flag: set to `true` when adapter is not listed in the latest repository, with an info log when set.

lib/M3000_Testing.js

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const yaml = require('js-yaml');
1111
const common = require('./common.js');
1212

1313
const WORKFLOW_FILE = '/.github/workflows/test-and-release.yml';
14+
const WORKFLOW_FILE_SHORT = 'test-and-release.yml';
1415

1516
/**
1617
* Returns true if the given file path exists in the context's filesList,
@@ -168,7 +169,7 @@ async function checkTests(context) {
168169
// --- W3004: check workflow name ---
169170
if (workflow.name !== 'Test and Release') {
170171
context.warnings.push(
171-
`[W3004] Workflow "name" in "${WORKFLOW_FILE}" must be "Test and Release" (found: "${workflow.name || '(not set)'}" ).`,
172+
`[W3004] Workflow "name" in "${WORKFLOW_FILE_SHORT}" must be "Test and Release" (found: "${workflow.name || '(not set)'}" ).`,
172173
);
173174
} else {
174175
context.checks.push('Workflow name is "Test and Release".');
@@ -180,7 +181,7 @@ async function checkTests(context) {
180181
// --- W3005: check for pull_request in 'on' ---
181182
if (!onAttr || !Object.prototype.hasOwnProperty.call(onAttr, 'pull_request')) {
182183
context.warnings.push(
183-
`[W3005] Workflow "${WORKFLOW_FILE}" should have "pull_request: {}" in the "on" trigger configuration.`,
184+
`[W3005] Workflow "${WORKFLOW_FILE_SHORT}" should have "pull_request: {}" in the "on" trigger configuration.`,
184185
);
185186
} else {
186187
context.checks.push('Workflow trigger includes "pull_request".');
@@ -189,7 +190,7 @@ async function checkTests(context) {
189190
// --- E3006 / E3007 / E3008: check for push in 'on' ---
190191
if (!onAttr || !Object.prototype.hasOwnProperty.call(onAttr, 'push')) {
191192
context.errors.push(
192-
`[E3006] Workflow "${WORKFLOW_FILE}" is missing the "push" trigger in the "on" configuration.`,
193+
`[E3006] Workflow "${WORKFLOW_FILE_SHORT}" is missing the "push" trigger in the "on" configuration.`,
193194
);
194195
} else {
195196
context.checks.push('Workflow trigger includes "push".');
@@ -200,7 +201,7 @@ async function checkTests(context) {
200201
const branches = pushConfig.branches;
201202
if (!branches || !Array.isArray(branches) || branches.length === 0) {
202203
context.errors.push(
203-
`[E3007] Workflow "${WORKFLOW_FILE}": "push" trigger is missing "branches" configuration. ` +
204+
`[E3007] Workflow "${WORKFLOW_FILE_SHORT}": "push" trigger is missing "branches" configuration. ` +
204205
`Must include "*" or the default branch ("${context.branch}").`,
205206
);
206207
} else {
@@ -209,7 +210,7 @@ async function checkTests(context) {
209210
const hasDefaultBranch = branches.includes(defaultBranch);
210211
if (!hasWildcard && !hasDefaultBranch) {
211212
context.errors.push(
212-
`[E3007] Workflow "${WORKFLOW_FILE}": "push" trigger "branches" must include "*" or the default branch "${defaultBranch}". ` +
213+
`[E3007] Workflow "${WORKFLOW_FILE_SHORT}": "push" trigger "branches" must include "*" or the default branch "${defaultBranch}". ` +
213214
`Found: ${JSON.stringify(branches)}`,
214215
);
215216
} else {
@@ -222,15 +223,15 @@ async function checkTests(context) {
222223
const requiredTagPatterns = ['v[0-9]+.[0-9]+.[0-9]+', 'v[0-9]+.[0-9]+.[0-9]+-**'];
223224
if (!tags || !Array.isArray(tags)) {
224225
context.errors.push(
225-
`[E3008] Workflow "${WORKFLOW_FILE}": "push" trigger is missing "tags" configuration. ` +
226+
`[E3008] Workflow "${WORKFLOW_FILE_SHORT}": "push" trigger is missing "tags" configuration. ` +
226227
`Required tag patterns: ${requiredTagPatterns.join(', ')}`,
227228
);
228229
} else {
229230
const cleanedTags = pushConfig.tags.map(tag => tag.replaceAll('v?', 'v'));
230231
const missingPatterns = requiredTagPatterns.filter(pat => !cleanedTags.includes(pat));
231232
if (missingPatterns.length > 0) {
232233
context.errors.push(
233-
`[E3008] Workflow "${WORKFLOW_FILE}": "push" trigger "tags" is missing required pattern(s): ${missingPatterns.join(', ')}`,
234+
`[E3008] Workflow "${WORKFLOW_FILE_SHORT}": "push" trigger "tags" is missing required pattern(s): ${missingPatterns.join(', ')}`,
234235
);
235236
} else {
236237
context.checks.push('Workflow "push" trigger tags are correctly configured.');
@@ -242,7 +243,7 @@ async function checkTests(context) {
242243
const concurrency = workflow.concurrency;
243244
if (!concurrency || concurrency.group !== '${{ github.ref }}' || concurrency['cancel-in-progress'] !== true) {
244245
context.warnings.push(
245-
`[W3009] Workflow "${WORKFLOW_FILE}" is missing recommended concurrency configuration. See ` +
246+
`[W3009] Workflow "${WORKFLOW_FILE_SHORT}" is missing recommended concurrency configuration. See ` +
246247
`"https://github.com/ioBroker/ioBroker.example/blob/e7db900495bb3c2b89dc35d863dda4ccf33f5def/JavaScript/.github/workflows/test-and-release.yml#L17" for details.`,
247248
);
248249
} else {
@@ -258,11 +259,11 @@ async function checkTests(context) {
258259
if (!jobs['check-and-lint']) {
259260
if (!context.cfg.onlyWWW) {
260261
context.errors.push(
261-
`[E3010] Workflow "${WORKFLOW_FILE}": job "check-and-lint" is missing. Please add it.`,
262+
`[E3010] Workflow "${WORKFLOW_FILE_SHORT}": job "check-and-lint" is missing. Please add it.`,
262263
);
263264
} else {
264265
context.warnings.push(
265-
`[S3010] Workflow "${WORKFLOW_FILE}": job "check-and-lint" is missing. Consider adding it.`,
266+
`[S3010] Workflow "${WORKFLOW_FILE_SHORT}": job "check-and-lint" is missing. Consider adding it.`,
266267
);
267268
}
268269
} else {
@@ -271,7 +272,7 @@ async function checkTests(context) {
271272
// W3013: check-and-lint must use ioBroker/testing-action-check@v1
272273
if (!jobHasStepWithAction(jobs['check-and-lint'], 'ioBroker/testing-action-check@')) {
273274
context.warnings.push(
274-
`[W3013] Workflow "${WORKFLOW_FILE}": job "check-and-lint" should contain a step using "ioBroker/testing-action-check@v1".`,
275+
`[W3013] Workflow "${WORKFLOW_FILE_SHORT}": job "check-and-lint" should contain a step using "ioBroker/testing-action-check@v1".`,
275276
);
276277
} else {
277278
context.checks.push('Job "check-and-lint" uses "ioBroker/testing-action-check@v1".');
@@ -282,14 +283,14 @@ async function checkTests(context) {
282283
if (context.cfg.onlyWWW) {
283284
context.checks.push('Job "adapter-tests" check skipped (onlyWWW adapter).');
284285
} else if (!jobs['adapter-tests']) {
285-
context.errors.push(`[E3011] Workflow "${WORKFLOW_FILE}": job "adapter-tests" is missing. Please add it.`);
286+
context.errors.push(`[E3011] Workflow "${WORKFLOW_FILE_SHORT}": job "adapter-tests" is missing. Please add it.`);
286287
} else {
287288
context.checks.push('Job "adapter-tests" found.');
288289

289290
// S3014: adapter-tests should need check-and-lint
290291
if (!jobNeedsAll(jobs['adapter-tests'], ['check-and-lint'])) {
291292
context.warnings.push(
292-
`[S3014] Workflow "${WORKFLOW_FILE}": job "adapter-tests" should declare "needs: check-and-lint" to run after linting.`,
293+
`[S3014] Workflow "${WORKFLOW_FILE_SHORT}": job "adapter-tests" should declare "needs: check-and-lint" to run after linting.`,
293294
);
294295
} else {
295296
context.checks.push('Job "adapter-tests" correctly requires "check-and-lint".');
@@ -298,7 +299,7 @@ async function checkTests(context) {
298299
// W3015: adapter-tests must use ioBroker/testing-action-adapter@v1
299300
if (!jobHasStepWithAction(jobs['adapter-tests'], 'ioBroker/testing-action-adapter@')) {
300301
context.warnings.push(
301-
`[W3015] Workflow "${WORKFLOW_FILE}": job "adapter-tests" should contain a step using "ioBroker/testing-action-adapter@v1".`,
302+
`[W3015] Workflow "${WORKFLOW_FILE_SHORT}": job "adapter-tests" should contain a step using "ioBroker/testing-action-adapter@v1".`,
302303
);
303304
} else {
304305
context.checks.push('Job "adapter-tests" uses "ioBroker/testing-action-adapter@v1".');
@@ -308,7 +309,7 @@ async function checkTests(context) {
308309
// S3012: deploy job (suggestion if missing)
309310
if (!jobs['deploy']) {
310311
context.warnings.push(
311-
`[S3012] Workflow "${WORKFLOW_FILE}": job "deploy" is not defined. Consider adding it for automated releases.`,
312+
`[S3012] Workflow "${WORKFLOW_FILE_SHORT}": job "deploy" is not defined. Consider adding it for automated releases.`,
312313
);
313314
} else {
314315
context.checks.push('Job "deploy" found.');
@@ -318,7 +319,7 @@ async function checkTests(context) {
318319
const requiredDependencies = ['check-and-lint', 'adapter-tests'].filter(j => jobs[j]);
319320
if (!requiredDependencies.every(req => jobDependsOn(jobs, 'deploy', req))) {
320321
context.errors.push(
321-
`[E3016] Workflow "${WORKFLOW_FILE}": job "deploy" must declare "needs" for both "check-and-lint" and "adapter-tests".`,
322+
`[E3016] Workflow "${WORKFLOW_FILE_SHORT}": job "deploy" must declare "needs" for both "check-and-lint" and "adapter-tests".`,
322323
);
323324
} else {
324325
context.checks.push('Job "deploy" correctly requires "check-and-lint" and "adapter-tests".');
@@ -327,7 +328,7 @@ async function checkTests(context) {
327328
// W3017: deploy must use ioBroker/testing-action-deploy@v1
328329
if (!jobHasStepWithAction(jobs['deploy'], 'ioBroker/testing-action-deploy@')) {
329330
context.warnings.push(
330-
`[W3017] Workflow "${WORKFLOW_FILE}": job "deploy" should contain a step using "ioBroker/testing-action-deploy@v1".`,
331+
`[W3017] Workflow "${WORKFLOW_FILE_SHORT}": job "deploy" should contain a step using "ioBroker/testing-action-deploy@v1".`,
331332
);
332333
} else {
333334
context.checks.push('Job "deploy" uses "ioBroker/testing-action-deploy@v1".');
@@ -338,7 +339,7 @@ async function checkTests(context) {
338339
const hasIdTokenWrite = permissions && permissions['id-token'] === 'write';
339340
if (!hasContentsWrite || !hasIdTokenWrite) {
340341
context.warnings.push(
341-
`[W3018] Workflow "${WORKFLOW_FILE}": job "deploy" is missing required permissions "contents: write" and "id-token: write" at job level. ` +
342+
`[W3018] Workflow "${WORKFLOW_FILE_SHORT}": job "deploy" is missing required permissions "contents: write" and "id-token: write" at job level. ` +
342343
`Trusted publishing will not work without them.`,
343344
);
344345
} else {
@@ -357,7 +358,7 @@ async function checkTests(context) {
357358
Object.prototype.hasOwnProperty.call(deployStep.with, 'npm-token')
358359
) {
359360
context.warnings.push(
360-
`[W3019] Workflow "${WORKFLOW_FILE}": job "deploy" step using "ioBroker/testing-action-deploy@v1" ` +
361+
`[W3019] Workflow "${WORKFLOW_FILE_SHORT}": job "deploy" step using "ioBroker/testing-action-deploy@v1" ` +
361362
`has "npm-token" parameter specified. ` +
362363
`Trusted publishing will not work while "npm-token" is set.`,
363364
);

lib/M4000_Repository.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,10 @@ async function checkRepository(context) {
165165
context.checks.push('Meta URL (stable) is OK in latest repository');
166166
}
167167
}
168+
} else if (!context.cfg.isNewAdapter) {
169+
context.warnings.push(
170+
`[W4047] Adapter "${context.adapterName}" is not yet available in the stable repository.`,
171+
);
168172
}
169173
}
170174

@@ -545,3 +549,4 @@ exports.checkRepository = checkRepository;
545549
// [4044] Missing schema definition for JSON5 config files in .vscode/settings.json. Add: {"fileMatch": ${JSON.stringify(json5ConfigFiles)}, "url": "${config.schemaUrls.jsonConfig}"}
546550
// [4045] Incorrect schema URL for JSON5 config files in .vscode/settings.json. Expected: "${config.schemaUrls.jsonConfig}", found: "${json5ConfigSchema.url}"
547551
// [4046] Could not read .vscode/settings.json file: ${error.message}
552+
// [4047] Adapter "${context.adapterName}" is not yet available in the stable repository.

lib/M9000_GitNpmIgnore.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,69 @@
1616

1717
// const common = require('./common.js');
1818

19+
// Source directories whose i18n subdirectories are excluded from npm packaging checks,
20+
// since their contents are typically compiled/moved to build/ or admin/ during the build process.
21+
const SRC_DIRS_TO_IGNORE = ['src', 'src-admin', 'admin-src'];
22+
23+
/**
24+
* Returns a deduplicated list of i18n directory paths found in the given file list.
25+
* Example: '/admin/i18n/en/translations.json' → '/admin/i18n'
26+
* Directories under src, src-admin or admin-src are excluded as those are build sources
27+
* and are not packaged directly.
28+
*
29+
* @param {string[]} filesList list of file paths from the repository (each with a leading slash)
30+
* @returns {string[]} array of unique i18n directory paths with leading slash
31+
*/
32+
function findI18nDirs(filesList) {
33+
const i18nDirs = new Set();
34+
for (const file of filesList) {
35+
const match = file.match(/^(\/(?:[^/]+\/)*i18n)(?:\/|$)/);
36+
if (match) {
37+
const i18nDir = match[1]; // e.g. '/admin/i18n'
38+
// Skip i18n dirs that reside directly inside a source directory
39+
const topLevel = i18nDir.replace(/^\//, '').split('/')[0];
40+
if (!SRC_DIRS_TO_IGNORE.includes(topLevel)) {
41+
i18nDirs.add(i18nDir);
42+
}
43+
}
44+
}
45+
return [...i18nDirs];
46+
}
47+
48+
/**
49+
* Checks whether the given i18n directory is covered by any entry in the package.json "files" array.
50+
*
51+
* @param {string} i18nDir e.g. '/admin/i18n'
52+
* @param {string[]} filesEntries entries from package.json "files"
53+
* @returns {boolean} true if the i18n directory is included
54+
*/
55+
function isI18nDirIncludedInFiles(i18nDir, filesEntries) {
56+
const i18nPath = i18nDir.replace(/^\//, ''); // e.g. 'admin/i18n'
57+
return filesEntries.some(entry => {
58+
const normalized = entry.replace(/^\.\//, '').replace(/\/+$/, '');
59+
// Covered if entry is exactly the i18n dir, a parent dir, or '.' (root)
60+
return normalized === '.' || normalized === i18nPath || i18nPath.startsWith(`${normalized}/`);
61+
});
62+
}
63+
64+
/**
65+
* Checks whether the given i18n directory is excluded by any rule in the .npmignore rule list.
66+
*
67+
* @param {string} i18nDir e.g. '/admin/i18n'
68+
* @param {Array<string|RegExp>} rules parsed .npmignore rules
69+
* @returns {boolean} true if the i18n directory is excluded
70+
*/
71+
function isI18nDirExcluded(i18nDir, rules) {
72+
const i18nPath = i18nDir.replace(/^\//, ''); // e.g. 'admin/i18n'
73+
return rules.some(rule => {
74+
if (typeof rule === 'string') {
75+
const normalized = rule.replace(/^\//, '').replace(/\/+$/, '');
76+
return normalized === i18nPath;
77+
}
78+
return rule.test(i18nPath) || rule.test(`${i18nPath}/`);
79+
});
80+
}
81+
1982
// 900 - ???
2083
async function checkGitIgnore(context) {
2184
console.log('\n[E9000 - E9499] checkGitIgnore');
@@ -138,6 +201,17 @@ async function checkNpmIgnore(context) {
138201
} else {
139202
context.checks.push('package.json "files" already used.');
140203
}
204+
205+
// Check that i18n directories present in the repo are covered by the "files" entries
206+
const i18nDirs = findI18nDirs(context.filesList);
207+
for (const i18nDir of i18nDirs) {
208+
if (!isI18nDirIncludedInFiles(i18nDir, context.packageJson.files)) {
209+
context.errors.push(
210+
`[E9507] i18n directory "${i18nDir.replace(/^\//, '')}" found in repository but is not included in package.json "files". Translations will be missing from the npm package.`,
211+
);
212+
}
213+
}
214+
141215
return context;
142216
}
143217

@@ -192,6 +266,16 @@ async function checkNpmIgnore(context) {
192266
context.errors.push(`[E9010] file ${file} found in repository, but not found in .npmignore`);
193267
}
194268
});
269+
270+
// Check that i18n directories are not excluded by .npmignore
271+
const i18nDirs = findI18nDirs(context.filesList);
272+
for (const i18nDir of i18nDirs) {
273+
if (isI18nDirExcluded(i18nDir, rules)) {
274+
context.errors.push(
275+
`[E9506] i18n directory "${i18nDir.replace(/^\//, '')}" is excluded by .npmignore. Translations will be missing from the npm package.`,
276+
);
277+
}
278+
}
195279
}
196280
}
197281
return context;
@@ -217,3 +301,5 @@ exports.checkNpmIgnore = checkNpmIgnore;
217301
// [9503] .npmignore found - consider using package.json object "files" instead.
218302
// [9504] node_modules not found in `.npmignore`
219303
// [9505] iob_npm.done not found in .npmignore` ### removed ###
304+
// [9506] i18n directory excluded by .npmignore
305+
// [9507] i18n directory not included in package.json "files"

0 commit comments

Comments
 (0)