diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index bfa08167..9ad36b4b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -102,8 +102,8 @@ body: attributes: label: 日志(可选) description: | - 可粘贴相关日志。 - Paste relevant logs, or attach screenshots / recordings. + 可粘贴相关日志或附上截图。**不要粘贴真实输入内容、用户词库、API Key 或任何凭据。** + Paste relevant logs. Do not include real typed text, user dictionary contents, API keys or credentials. render: shell validations: required: false @@ -123,3 +123,5 @@ body: required: true - label: 我已尽量提供可复现信息。I provided enough detail to reproduce the issue. required: true + - label: 这不是安全漏洞。安全问题请按 SECURITY.md 私下上报。This is not a security vulnerability; those go through SECURITY.md. + required: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1caa156..346617e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,10 @@ jobs: run: python3 -m unittest discover -s tests -p 'test_prepare_env.py' - name: Test the automatic release CI gate run: python3 -m unittest discover -s tests -p 'test_release_pr_ci.py' + - name: Test the engine-bump auto-merge guards + run: | + python3 -m pip install --quiet pyyaml + python3 -m unittest discover -s tests -p 'test_engine_bump_triage.py' - name: Read the reviewed lock id: lock run: | diff --git a/.github/workflows/engine-bump-triage.yml b/.github/workflows/engine-bump-triage.yml index b5f93cdc..49000a41 100644 --- a/.github/workflows/engine-bump-triage.yml +++ b/.github/workflows/engine-bump-triage.yml @@ -130,10 +130,14 @@ jobs: fi - name: Enable auto-merge - if: steps.classify.outputs.verdict == 'inert' + # The lock guard belongs here. It used to be appended to the HEAD environment value instead, + # where it was not a condition at all: auto-merge ran whether or not the contract lock + # verified, and the comment below printed the commit followed by the literal text + # "&& steps.lock.outputs.locked == 'yes'". + if: steps.classify.outputs.verdict == 'inert' && steps.lock.outputs.locked == 'yes' env: BASE: ${{ steps.range.outputs.base }} - HEAD: ${{ steps.range.outputs.head }} && steps.lock.outputs.locked == 'yes' + HEAD: ${{ steps.range.outputs.head }} run: | set -euo pipefail gh pr merge --auto --squash "$PR_URL" diff --git a/tests/test_engine_bump_triage.py b/tests/test_engine_bump_triage.py new file mode 100644 index 00000000..38a70862 --- /dev/null +++ b/tests/test_engine_bump_triage.py @@ -0,0 +1,48 @@ +"""Guard the conditions on the engine-bump auto-merge step. + +This exists because of a specific failure: the contract-lock guard was written into the step's HEAD +environment value rather than its `if:`, so it was not a condition at all. Auto-merge ran whether or +not the lock verified, and the comment the step posts printed the commit followed by the literal +text `&& steps.lock.outputs.locked == 'yes'`. Nothing failed, so nothing surfaced it. +""" +from pathlib import Path +import unittest + +import yaml + +WORKFLOW = Path(__file__).resolve().parents[1] / '.github/workflows/engine-bump-triage.yml' + + +def steps(): + # `on:` parses as the boolean True in YAML 1.1, which is why the job lookup goes through 'jobs'. + workflow = yaml.safe_load(WORKFLOW.read_text(encoding='utf-8')) + return [step for job in workflow['jobs'].values() for step in job.get('steps', [])] + + +def step_named(name): + matches = [step for step in steps() if step.get('name') == name] + if len(matches) != 1: + raise AssertionError(f'expected exactly one step named {name!r}, found {len(matches)}') + return matches[0] + + +class AutoMergeGuards(unittest.TestCase): + def test_auto_merge_requires_both_the_verdict_and_the_lock(self): + condition = step_named('Enable auto-merge')['if'] + self.assertIn("steps.classify.outputs.verdict == 'inert'", condition) + self.assertIn("steps.lock.outputs.locked == 'yes'", condition) + + def test_no_step_condition_leaked_into_an_environment_value(self): + for step in steps(): + for key, value in (step.get('env') or {}).items(): + self.assertNotIn('steps.', str(value).split('}}')[-1], + f'{step.get("name")}: env {key} carries a condition fragment') + + def test_the_comment_prints_only_the_commit_range(self): + env = step_named('Enable auto-merge')['env'] + for key in ('BASE', 'HEAD'): + self.assertRegex(env[key], r'^\$\{\{ steps\.range\.outputs\.\w+ \}\}$') + + +if __name__ == '__main__': + unittest.main()