From c8d348b088b160420b1a510ea5df15507964a40b Mon Sep 17 00:00:00 2001 From: Carl <74130028+Carl723000@users.noreply.github.com> Date: Sat, 17 Oct 2026 14:54:11 +0800 Subject: [PATCH 1/2] ci: schedule bounded weekly Dependabot update proposals Monitor development, build-tool and action dependencies in three weekly Shanghai-time scopes, with two ordinary PRs per scope and minor/patch groups. Leave majors and sensitive API/accessibility upgrades individual without ignoring security fixes. Keep merge and new-release publication manual. Add portable policy/mutation checks and bilingual contributor guidance; do not change package versions, runtime code, locks or release workflows. Co-authored-by: OpenAI Codex <215057067+openai-codex[bot]@users.noreply.github.com> --- .github/dependabot.yml | 56 ++++++++++ .github/scripts/dependabotPolicy.test.mjs | 128 ++++++++++++++++++++++ CONTRIBUTING.md | 45 ++++++++ 3 files changed, 229 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/scripts/dependabotPolicy.test.mjs diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..3f823c9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,56 @@ +# Weekly proposals only. Existing security alerts/updates stay independent. +# No ignore rules, automatic merge, registry credentials or release automation. +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + timezone: "Asia/Shanghai" + open-pull-requests-limit: 2 + labels: ["dependencies"] + commit-message: + prefix: "build" + include: "scope" + groups: + dev-tooling: + applies-to: "version-updates" + patterns: ["*"] + exclude-patterns: ["@axe-core/playwright", "@types/vscode"] + update-types: ["minor", "patch"] + - package-ecosystem: "npm" + directory: "/.github/build-tools" + schedule: + interval: "weekly" + day: "monday" + time: "09:30" + timezone: "Asia/Shanghai" + open-pull-requests-limit: 2 + labels: ["dependencies"] + commit-message: + prefix: "build" + include: "scope" + groups: + build-tooling: + applies-to: "version-updates" + patterns: ["*"] + update-types: ["minor", "patch"] + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "10:00" + timezone: "Asia/Shanghai" + open-pull-requests-limit: 2 + labels: ["dependencies"] + commit-message: + prefix: "build" + include: "scope" + groups: + github-actions: + applies-to: "version-updates" + patterns: ["*"] + update-types: ["minor", "patch"] diff --git a/.github/scripts/dependabotPolicy.test.mjs b/.github/scripts/dependabotPolicy.test.mjs new file mode 100644 index 0000000..c396117 --- /dev/null +++ b/.github/scripts/dependabotPolicy.test.mjs @@ -0,0 +1,128 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { existsSync, readFileSync, readdirSync } from 'node:fs'; + +const root = new URL('../../', import.meta.url); +const read = (file) => readFileSync(new URL(file, root), 'utf8'); +const active = (source) => source.replaceAll('\r\n', '\n').split('\n').filter((line) => !/^\s*#/.test(line)).join('\n'); + +// This is a fail-closed guard for this file's deliberately small, block-style +// YAML contract, not a general YAML parser. Real YAML syntax is also checked +// during configuration review without adding a project parser dependency. +function entries(source) { + source = active(source); + assert.match(source, /^version: 2\nupdates:\n/); + assert.doesNotMatch(source, /\t|[&*][A-Za-z_]|<<:/, 'no YAML aliases/merges or tabs'); + assert.doesNotMatch(source, /^\s*(?:ignore|allow|target-branch|registries|insecure-external-code-execution|exclude-paths|reviewers|cooldown):/m, + 'do not restrict vulnerable-package coverage or silently change update routing'); + const matches = [...source.matchAll(/^ - package-ecosystem: "([^"]+)"\n([\s\S]*?)(?=^ - package-ecosystem:|(?![\s\S]))/gm)]; + assert.equal(matches.length, 3, 'exactly three update scopes'); + assert.equal((source.match(/^ - /gm) ?? []).length, 3); + return matches.map(([, ecosystem, body]) => ({ ecosystem, body })); +} + +function scalar(body, key, indentation) { + const matches = [...body.matchAll(new RegExp(`^ {${indentation}}${key}: (.+)$`, 'gm'))]; + assert.equal(matches.length, 1, `one ${key} at indentation ${indentation}`); + return JSON.parse(matches[0][1]); +} + +function validate(source) { + const updates = entries(source); + const expected = [ + { ecosystem: 'npm', directory: '/', time: '09:00', group: 'dev-tooling', sensitive: ['@axe-core/playwright', '@types/vscode'] }, + { ecosystem: 'npm', directory: '/.github/build-tools', time: '09:30', group: 'build-tooling', sensitive: [] }, + { ecosystem: 'github-actions', directory: '/', time: '10:00', group: 'github-actions', sensitive: [] }, + ]; + return updates.map(({ ecosystem, body }, index) => { + const want = expected[index]; + assert.equal(ecosystem, want.ecosystem); + assert.equal(scalar(body, 'directory', 4), want.directory); + assert.equal(scalar(body, 'open-pull-requests-limit', 4), 2); + assert.deepEqual(scalar(body, 'labels', 4), ['dependencies']); + assert.match(body, /^ schedule:\n interval: "weekly"\n day: "monday"\n time: "[^"\n]+"\n timezone: "Asia\/Shanghai"$/m); + assert.equal(scalar(body, 'interval', 6), 'weekly'); + assert.equal(scalar(body, 'day', 6), 'monday'); + assert.equal(scalar(body, 'timezone', 6), 'Asia/Shanghai'); + assert.equal(scalar(body, 'time', 6), want.time); + assert.match(body, new RegExp(`^ groups:\\n ${want.group}:\\n`, 'm')); + assert.equal(scalar(body, 'applies-to', 8), 'version-updates'); + assert.deepEqual(scalar(body, 'patterns', 8), ['*']); + assert.deepEqual(scalar(body, 'update-types', 8), ['minor', 'patch']); + const exclusions = want.sensitive.length ? scalar(body, 'exclude-patterns', 8) : []; + assert.deepEqual(exclusions, want.sensitive); + if (!want.sensitive.length) assert.doesNotMatch(body, /exclude-patterns:/); + assert.match(body, /^ commit-message:\n prefix: "build"\n include: "scope"$/m); + // A second mapping could otherwise silently replace the reviewed scope. + const topKeys = [...body.matchAll(/^ {4}([\w-]+):/gm)].map((m) => m[1]); + assert.deepEqual(topKeys, ['directory', 'schedule', 'open-pull-requests-limit', 'labels', 'commit-message', 'groups']); + assert.equal((body.match(/^ {6}[\w-]+:\s*$/gm) ?? []).length, 1, 'one version-only group'); + return { ecosystem, directory: want.directory, exclusions }; + }); +} + +test('Dependabot covers both npm locks and workflow actions on bounded weekly schedules', () => { + const source = read('.github/dependabot.yml'); + const scopes = validate(source); + assert.deepEqual(validate(source.replaceAll('\n', '\r\n')), scopes, 'checkout line endings do not change policy'); + for (const { ecosystem, directory } of scopes) { + if (ecosystem === 'npm') { + const prefix = directory === '/' ? '' : `${directory.slice(1)}/`; + assert.ok(existsSync(new URL(`${prefix}package-lock.json`, root))); + assert.ok(existsSync(new URL(`${prefix}package.json`, root))); + } + } +}); + +test('ordinary groups leave major and sensitive upgrades individual without suppressing security fixes', () => { + const source = read('.github/dependabot.yml'); + const scopes = validate(source); + assert.doesNotMatch(active(source), /applies-to: "security-updates"|skip-changelog/); + const rootScope = scopes[0]; + const grouped = (name, bump) => ['minor', 'patch'].includes(bump) && !rootScope.exclusions.includes(name); + assert.equal(grouped('@playwright/test', 'minor'), true); + assert.equal(grouped('typescript', 'patch'), true); + assert.equal(grouped('typescript', 'major'), false); + assert.equal(grouped('@types/node', 'major'), false); + assert.equal(grouped('@axe-core/playwright', 'minor'), false); + assert.equal(grouped('@types/vscode', 'minor'), false); + assert.equal(grouped('@axe-core/playwright', 'patch'), false); +}); + +test('unsafe schedule, routing, grouping and limit mutations fail the configuration guard', () => { + const source = read('.github/dependabot.yml'); + for (const [before, after] of [ + ['interval: "weekly"', 'interval: "daily"'], + ['day: "monday"', 'day: "friday"'], + ['timezone: "Asia/Shanghai"', 'timezone: "UTC"'], + ['open-pull-requests-limit: 2', 'open-pull-requests-limit: 20'], + ['directory: "/.github/build-tools"', 'directory: "/tools"'], + ['applies-to: "version-updates"', 'applies-to: "security-updates"'], + ['update-types: ["minor", "patch"]', 'update-types: ["major", "minor", "patch"]'], + ['exclude-patterns: ["@axe-core/playwright", "@types/vscode"]', 'exclude-patterns: []'], + ['labels: ["dependencies"]', 'labels: ["dependencies", "skip-changelog"]'], + [' groups:\n', ' ignore: []\n groups:\n'], + [' groups:\n', ' target-branch: "preview"\n groups:\n'], + [' groups:\n', ' allow: []\n groups:\n'], + ]) { + assert.ok(source.includes(before), `missing mutation anchor: ${before}`); + assert.throws(() => validate(source.replace(before, after)), { code: 'ERR_ASSERTION' }, after); + } +}); + +test('dependency PRs retain CI, manual integration and explicit release publication boundaries', () => { + const ci = active(read('.github/workflows/test.yml')); + assert.match(ci, /^ pull_request:\s*$/m); + assert.doesNotMatch(ci, /paths-ignore:|dependabot\[bot\]/); + const workflows = readdirSync(new URL('.github/workflows/', root)).filter((file) => /\.ya?ml$/.test(file)); + assert.ok(workflows.length > 0); + for (const file of workflows) { + const source = active(read(`.github/workflows/${file}`)); + assert.doesNotMatch(source, /gh\s+pr\s+merge|enablePullRequestAutoMerge|mergePullRequest/, + `${file}: no bot-driven merge workflow`); + } + const publish = active(read('.github/workflows/publish.yml')); + assert.match(publish, /^ release:\n types: \[published\]$/m); + assert.match(publish, /^ workflow_dispatch:/m); + assert.doesNotMatch(publish, /^ (?:push|pull_request|pull_request_target|schedule):/m); +}); diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a610701..f742819 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,6 +74,51 @@ A separate window or worktree is insufficient. Compare selected normal-state metadata/hashes before and after; unexplained changes stay unknown. Do not use a default-profile `code --install-extension` or modify the daily installation. +### Dependabot update proposals + +`.github/dependabot.yml` becomes active only after it reaches the default branch. +It checks development dependencies, separate build tools and GitHub Actions each +Monday at 09:00, 09:30 and 10:00 respectively in `Asia/Shanghai`. These are requested +check times, not guaranteed job start times; first enablement may also prompt an +initial check. Validate the first real job and generated PR after enablement; +local checks alone do not establish scheduling or real bot execution. + +Each scope permits at most **two open ordinary version-update PRs** (up to six +across the three scopes, not two globally). Minor/patch proposals are grouped by +scope. Major updates and root `@axe-core/playwright` / `@types/vscode` updates stay +individual for focused review. Excluding a package from a group does not ignore +it: sensitive upgrades can still be proposed. No ignore/allow filter or non-default +target branch is added, and groups apply only to version updates, leaving existing +security alerts and security-update PRs independent of the weekly schedule and +ordinary-PR limit. Available safe fixes must still be assessed for compatibility. + +Bot proposals are not approvals. Preserve paired Playwright package/image pins, +reviewed Action SHAs, both locks and their policy tests, the supported Node line, +and the VS Code API floor. An upgrade-only PR may initially fail intentional pin +guards until its compatibility evidence and related pins/tests are reviewed. +Use the existing CODEOWNERS/CI process and maintainer-authorized Squash and merge; +this configuration adds **no automatic merge or new-release publication**. +Existing delivery recovery for already-published releases is unchanged. +The `dependencies` label does not hide +release notes: add `skip-changelog` only when the reviewed change is internal-only. +The configuration alone does not upgrade packages or change runtime code or +release triggers. Removing it stops ordinary version updates; security alerts +and security updates remain separate repository security settings. + +中文审阅:配置进入默认分支后才启用。开发依赖、独立构建工具和 GitHub Actions 分别在 +每周一北京时间 09:00、09:30、10:00 请求检查;实际启动可能排队,初次启用也可能先检查一次。 +每类最多两个未关闭的普通升级 PR,三类合计最多六个,不是全仓库最多两个。小版本/补丁 +按用途分组;大版本及根目录 Axe、VS Code 类型升级单独提案,仍可能出现,并非忽略。 +不设置忽略/允许筛选或其他目标分支,分组仅用于普通版本升级;已有安全警报和安全修复 +不受每周排期及普通 PR 限额限制。机器人只提建议,不自动批准,不新增自动合并或新版本 +发布;既有已发布版本的交付恢复保持不变。 +维护者须核对 Playwright 包/镜像、Action SHA、锁文件、策略测试、Node 支持周期及 API +兼容门槛;只改版本可能先触发固定版本守卫,不能为变绿盲目放宽测试。沿用 CODEOWNERS +和 CI,明确授权后 Squash 合并;内部维护经审阅后再标记 `skip-changelog`,不默认隐藏 +所有依赖或安全变更。配置本身不升级包或修改产品代码、发布触发器;实际排期及机器人 +PR 行为须启用后核验首个作业,本地校验不能证明已运行定时机器人。可删除配置停用普通 +升级;安全警报/安全修复开关仍在仓库安全设置中单独管理。 + ## Pull requests - Keep each PR focused on one logical change. From 52aa0aac236360776fce8948007d7a6e57cb101f Mon Sep 17 00:00:00 2001 From: Carl <74130028+Carl723000@users.noreply.github.com> Date: Sat, 17 Oct 2026 15:45:45 +0800 Subject: [PATCH 2/2] ci: move weekly Dependabot proposals to Sunday Retain the Shanghai timezone, staggered times, grouping and manual review boundaries. Update bilingual guidance and reject regression to Monday. Co-authored-by: OpenAI Codex <215057067+openai-codex[bot]@users.noreply.github.com> --- .github/dependabot.yml | 6 +++--- .github/scripts/dependabotPolicy.test.mjs | 6 +++--- CONTRIBUTING.md | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3f823c9..82ccc75 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,7 +6,7 @@ updates: directory: "/" schedule: interval: "weekly" - day: "monday" + day: "sunday" time: "09:00" timezone: "Asia/Shanghai" open-pull-requests-limit: 2 @@ -24,7 +24,7 @@ updates: directory: "/.github/build-tools" schedule: interval: "weekly" - day: "monday" + day: "sunday" time: "09:30" timezone: "Asia/Shanghai" open-pull-requests-limit: 2 @@ -41,7 +41,7 @@ updates: directory: "/" schedule: interval: "weekly" - day: "monday" + day: "sunday" time: "10:00" timezone: "Asia/Shanghai" open-pull-requests-limit: 2 diff --git a/.github/scripts/dependabotPolicy.test.mjs b/.github/scripts/dependabotPolicy.test.mjs index c396117..a014c04 100644 --- a/.github/scripts/dependabotPolicy.test.mjs +++ b/.github/scripts/dependabotPolicy.test.mjs @@ -40,9 +40,9 @@ function validate(source) { assert.equal(scalar(body, 'directory', 4), want.directory); assert.equal(scalar(body, 'open-pull-requests-limit', 4), 2); assert.deepEqual(scalar(body, 'labels', 4), ['dependencies']); - assert.match(body, /^ schedule:\n interval: "weekly"\n day: "monday"\n time: "[^"\n]+"\n timezone: "Asia\/Shanghai"$/m); + assert.match(body, /^ schedule:\n interval: "weekly"\n day: "sunday"\n time: "[^"\n]+"\n timezone: "Asia\/Shanghai"$/m); assert.equal(scalar(body, 'interval', 6), 'weekly'); - assert.equal(scalar(body, 'day', 6), 'monday'); + assert.equal(scalar(body, 'day', 6), 'sunday'); assert.equal(scalar(body, 'timezone', 6), 'Asia/Shanghai'); assert.equal(scalar(body, 'time', 6), want.time); assert.match(body, new RegExp(`^ groups:\\n ${want.group}:\\n`, 'm')); @@ -93,7 +93,7 @@ test('unsafe schedule, routing, grouping and limit mutations fail the configurat const source = read('.github/dependabot.yml'); for (const [before, after] of [ ['interval: "weekly"', 'interval: "daily"'], - ['day: "monday"', 'day: "friday"'], + ['day: "sunday"', 'day: "monday"'], ['timezone: "Asia/Shanghai"', 'timezone: "UTC"'], ['open-pull-requests-limit: 2', 'open-pull-requests-limit: 20'], ['directory: "/.github/build-tools"', 'directory: "/tools"'], diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f742819..d5274a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,7 +78,7 @@ a default-profile `code --install-extension` or modify the daily installation. `.github/dependabot.yml` becomes active only after it reaches the default branch. It checks development dependencies, separate build tools and GitHub Actions each -Monday at 09:00, 09:30 and 10:00 respectively in `Asia/Shanghai`. These are requested +Sunday at 09:00, 09:30 and 10:00 respectively in `Asia/Shanghai`. These are requested check times, not guaranteed job start times; first enablement may also prompt an initial check. Validate the first real job and generated PR after enablement; local checks alone do not establish scheduling or real bot execution. @@ -106,7 +106,7 @@ release triggers. Removing it stops ordinary version updates; security alerts and security updates remain separate repository security settings. 中文审阅:配置进入默认分支后才启用。开发依赖、独立构建工具和 GitHub Actions 分别在 -每周一北京时间 09:00、09:30、10:00 请求检查;实际启动可能排队,初次启用也可能先检查一次。 +每周日北京时间 09:00、09:30、10:00 请求检查;实际启动可能排队,初次启用也可能先检查一次。 每类最多两个未关闭的普通升级 PR,三类合计最多六个,不是全仓库最多两个。小版本/补丁 按用途分组;大版本及根目录 Axe、VS Code 类型升级单独提案,仍可能出现,并非忽略。 不设置忽略/允许筛选或其他目标分支,分组仅用于普通版本升级;已有安全警报和安全修复