From 626bb39bffb35667be48f42e54cb8d1683a24943 Mon Sep 17 00:00:00 2001 From: Yesol Heo | Creative Developer <86992002+Yesol-Pilot@users.noreply.github.com> Date: Thu, 27 Aug 2026 23:00:21 +0900 Subject: [PATCH 1/4] fix(governance): make settings and secret validation fail accurately --- .../repository-governance-reusable.yml | 217 +++++++++++++++--- 1 file changed, 186 insertions(+), 31 deletions(-) diff --git a/.github/workflows/repository-governance-reusable.yml b/.github/workflows/repository-governance-reusable.yml index 31afe49..5db4de5 100644 --- a/.github/workflows/repository-governance-reusable.yml +++ b/.github/workflows/repository-governance-reusable.yml @@ -2,6 +2,12 @@ name: NeoGenesis Repository Governance on: workflow_call: + inputs: + self_test_only: + description: Run validator invariants without evaluating the caller repository. + required: false + type: boolean + default: false permissions: contents: read @@ -29,6 +35,7 @@ jobs: env: REPOSITORY: ${{ github.repository }} GH_TOKEN: ${{ github.token }} + SELF_TEST_ONLY: ${{ inputs.self_test_only }} run: | set -euo pipefail python3 - <<'PY' @@ -41,11 +48,138 @@ jobs: import subprocess import sys import urllib.request + from collections.abc import Mapping root = pathlib.Path('.') repository = os.environ.get('REPOSITORY', '') token = os.environ.get('GH_TOKEN', '') + self_test_only = os.environ.get('SELF_TEST_ONLY', '').lower() == 'true' errors: list[str] = [] + notes: list[str] = [] + + # Prefix boundaries are mandatory. Without them, ordinary identifiers + # such as task-execution, risk-register, desk-state and mask-value are + # misclassified merely because they contain the substring "sk-". + secret_patterns: tuple[tuple[str, re.Pattern[str]], ...] = ( + ( + 'GitHub classic token', + re.compile(r'(? list[str]: + return [label for label, pattern in secret_patterns if pattern.search(line)] + + def setting_finding(settings: Mapping[str, object], key: str, expected: bool) -> str | None: + if key not in settings or type(settings.get(key)) is not bool: + return f'repository setting {key} is UNVERIFIED (field unavailable)' + actual = settings[key] + if actual is not expected: + return f'repository setting {key} must be {expected}, got {actual}' + return None + + def run_validator_self_tests() -> None: + negative_lines = ( + 'task-execution-listener.ts', + 'risk-classification', + 'desktop-task-service', + 'mask-sensitive-value', + 'desk-state', + 'ask-followup-question', + 'the sk-prefix is discussed without a credential', + ) + for line in negative_lines: + labels = matching_secret_labels(line) + if labels: + raise AssertionError(f'false-positive secret match for {line!r}: {labels}') + + positive_lines = { + 'GitHub classic token': 'ghp_' + ('A1' * 18), + 'GitHub fine-grained token': 'github_pat_' + ('A1_' * 14), + 'OpenAI project or service key': 'sk-' + 'proj-' + ('A1_' * 10), + 'OpenAI legacy key': 'sk-' + ('A1' * 16), + 'Anthropic key': 'sk-' + 'ant-' + ('A1_' * 10), + 'xAI key': 'xai-' + ('A1_' * 10), + 'AWS access key': 'AKIA' + ('A1' * 8), + 'Google API key': 'AIza' + ('A1_' * 11) + 'A1', + 'Slack token': 'xoxb-' + ('A1-' * 8), + 'Stripe live secret': 'sk_' + 'live_' + ('A1' * 8), + 'Hugging Face token': 'hf_' + ('A1' * 15), + 'Private key header': '-----BEGIN PRIVATE KEY-----', + } + for expected_label, line in positive_lines.items(): + labels = matching_secret_labels(line) + if expected_label not in labels: + raise AssertionError( + f'secret detector missed synthetic {expected_label}: observed={labels}' + ) + + if setting_finding({}, 'allow_squash_merge', True) != ( + 'repository setting allow_squash_merge is UNVERIFIED (field unavailable)' + ): + raise AssertionError('missing repository setting did not remain UNVERIFIED') + if setting_finding({'allow_squash_merge': None}, 'allow_squash_merge', True) != ( + 'repository setting allow_squash_merge is UNVERIFIED (field unavailable)' + ): + raise AssertionError('null repository setting did not remain UNVERIFIED') + if setting_finding({'allow_squash_merge': False}, 'allow_squash_merge', True) != ( + 'repository setting allow_squash_merge must be True, got False' + ): + raise AssertionError('confirmed repository setting mismatch was not rejected') + if setting_finding({'allow_squash_merge': True}, 'allow_squash_merge', True) is not None: + raise AssertionError('matching repository setting was rejected') + + run_validator_self_tests() + if self_test_only: + print('repository-governance validator self-test PASS') + raise SystemExit(0) allowlist_path = root / '.github' / 'repository-governance-allowlist.json' allowlist: dict[str, object] = {} @@ -74,9 +208,9 @@ jobs: return errors.append(message) + settings: dict[str, object] = {} if not repository or not token: - record('repository identity or GitHub token is unavailable') - settings = {} + record('repository settings are UNVERIFIED: repository identity or GitHub token unavailable') else: request = urllib.request.Request( f'https://api.github.com/repos/{repository}', @@ -89,10 +223,13 @@ jobs: ) try: with urllib.request.urlopen(request, timeout=20) as response: - settings = json.load(response) + value = json.load(response) + if isinstance(value, dict): + settings = value + else: + record('repository settings are UNVERIFIED: API response is not an object') except Exception as exc: - settings = {} - record(f'cannot read repository settings: {type(exc).__name__}') + record(f'repository settings are UNVERIFIED: {type(exc).__name__}') contract = root / 'REPOSITORY_GOVERNANCE.md' if not contract.is_file(): @@ -116,17 +253,23 @@ jobs: 'allow_auto_merge': True, } for key, expected in required_settings.items(): - actual = settings.get(key) - if actual is not expected: - record(f'repository setting {key} must be {expected}, got {actual}') + finding = setting_finding(settings, key, expected) + if finding: + record(finding) + + default_branch_value = settings.get('default_branch') + if not isinstance(default_branch_value, str) or not default_branch_value: + record('repository default branch is UNVERIFIED (field unavailable)') + default_branch = '' + else: + default_branch = default_branch_value - default_branch = str(settings.get('default_branch') or '') approved_exceptions = { 'NeoGenesisAI/neomux-desktop': 'einstein/main', } task_prefixes = ('codex/', 'rebuild/', 'feature/', 'fix/', 'chore/', 'governance/') expected_exception = approved_exceptions.get(repository) - if expected_exception: + if expected_exception and default_branch: if default_branch != expected_exception: record( f'documented default-branch exception drifted: expected {expected_exception}, got {default_branch}' @@ -151,6 +294,7 @@ jobs: 'node_modules', '__pycache__', '.venv', 'venv', '.next', 'coverage', 'Library', 'Temp', 'obj', '.gradle' } + generated_paths: set[str] = set() for raw in tracked_paths: path = pathlib.PurePosixPath(raw) @@ -160,19 +304,13 @@ jobs: record(f'prohibited environment file is tracked: {raw}', raw) if name in forbidden_names or name.endswith(forbidden_suffixes): record(f'prohibited credential or signing file is tracked: {raw}', raw) - if parts & generated_parts: - record(f'generated or dependency directory is tracked: {raw}', raw) - - secret_patterns = [ - ('GitHub classic token', re.compile(r'gh[pousr]_[A-Za-z0-9]{36,}')), - ('GitHub fine-grained token', re.compile(r'github_pat_[A-Za-z0-9_]{40,}')), - ('OpenAI secret key', re.compile(r'sk-(?:proj-)?[A-Za-z0-9_-]{20,}')), - ('AWS access key', re.compile(r'AKIA[0-9A-Z]{16}')), - ('Google API key', re.compile(r'AIza[0-9A-Za-z_-]{35}')), - ('Slack token', re.compile(r'xox[baprs]-[0-9A-Za-z-]{20,}')), - ('Stripe live secret', re.compile(r'sk_live_[0-9A-Za-z]{16,}')), - ('Private key header', re.compile(r'-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----')), - ] + matching_generated = sorted(parts & generated_parts) + if matching_generated: + generated_paths.add(f'{matching_generated[0]} in {raw}') + + for generated_path in sorted(generated_paths): + record(f'generated or dependency directory is tracked: {generated_path}') + text_extensions = { '.txt', '.md', '.json', '.jsonc', '.yaml', '.yml', '.toml', '.ini', '.env', '.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py', '.rb', @@ -180,11 +318,11 @@ jobs: '.bash', '.zsh', '.ps1', '.xml', '.html', '.css', '.scss', '.sql', '.gradle', '.properties', '.conf', '.config' } - ignored_markers = ('REDACTED', 'EXAMPLE', 'DUMMY', 'PLACEHOLDER') for raw in tracked_paths: path = root / raw - if raw in allowed_paths: + pure_path = pathlib.PurePosixPath(raw) + if raw in allowed_paths or set(pure_path.parts) & generated_parts: continue if not path.is_file() or path.stat().st_size > 2_000_000: continue @@ -197,17 +335,34 @@ jobs: except UnicodeDecodeError: continue for line_number, line in enumerate(content.splitlines(), start=1): - if any(marker in line.upper() for marker in ignored_markers): - continue - for label, pattern in secret_patterns: - if pattern.search(line): - record(f'{label} pattern detected at {raw}:{line_number}', raw) + for label in matching_secret_labels(line): + record(f'{label} pattern detected at {raw}:{line_number}', raw) unique_errors = sorted(set(errors)) + summary_path = os.environ.get('GITHUB_STEP_SUMMARY') + summary_lines = [ + '## Repository governance result', + '', + f'- Repository: `{repository or "UNAVAILABLE"}`', + f'- Findings: `{len(unique_errors)}`', + '- Validator self-tests: `PASS`', + ] + if unique_errors: + summary_lines.extend(['', '### Fail-closed findings', '']) + summary_lines.extend(f'- `{item}`' for item in unique_errors) + if notes: + summary_lines.extend(['', '### Notes', '']) + summary_lines.extend(f'- {item}' for item in notes) + if summary_path: + pathlib.Path(summary_path).write_text('\n'.join(summary_lines) + '\n', encoding='utf-8') + if unique_errors: for item in unique_errors: print(f'::error::{item}') - print(f'repository-governance failed with {len(unique_errors)} unique finding(s)', file=sys.stderr) + print( + f'repository-governance failed with {len(unique_errors)} unique finding(s)', + file=sys.stderr, + ) raise SystemExit(1) print('repository-governance PASS') From c8b55554a5c7190d4ac43177baebd15151d3b9cc Mon Sep 17 00:00:00 2001 From: Yesol Heo | Creative Developer <86992002+Yesol-Pilot@users.noreply.github.com> Date: Thu, 27 Aug 2026 23:00:45 +0900 Subject: [PATCH 2/4] ci(governance): execute reusable validator invariants on pull requests --- ...repository-governance-validator-selftest.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/repository-governance-validator-selftest.yml diff --git a/.github/workflows/repository-governance-validator-selftest.yml b/.github/workflows/repository-governance-validator-selftest.yml new file mode 100644 index 0000000..fd9b8e9 --- /dev/null +++ b/.github/workflows/repository-governance-validator-selftest.yml @@ -0,0 +1,17 @@ +name: Repository Governance Validator Self-test + +on: + pull_request: + paths: + - ".github/workflows/repository-governance-reusable.yml" + - ".github/workflows/repository-governance-validator-selftest.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + validator-self-test: + uses: ./.github/workflows/repository-governance-reusable.yml + with: + self_test_only: true From 32c3a5bf523efa4d0f727667ee0f4608a93aa7eb Mon Sep 17 00:00:00 2001 From: Yesol Heo | Creative Developer <86992002+Yesol-Pilot@users.noreply.github.com> Date: Thu, 27 Aug 2026 23:05:20 +0900 Subject: [PATCH 3/4] fix(governance): read merge policy through GraphQL and avoid self-match --- .../repository-governance-reusable.yml | 277 +++++++++++------- 1 file changed, 165 insertions(+), 112 deletions(-) diff --git a/.github/workflows/repository-governance-reusable.yml b/.github/workflows/repository-governance-reusable.yml index 5db4de5..bd9e3ce 100644 --- a/.github/workflows/repository-governance-reusable.yml +++ b/.github/workflows/repository-governance-reusable.yml @@ -55,69 +55,53 @@ jobs: token = os.environ.get('GH_TOKEN', '') self_test_only = os.environ.get('SELF_TEST_ONLY', '').lower() == 'true' errors: list[str] = [] - notes: list[str] = [] - # Prefix boundaries are mandatory. Without them, ordinary identifiers - # such as task-execution, risk-register, desk-state and mask-value are - # misclassified merely because they contain the substring "sk-". secret_patterns: tuple[tuple[str, re.Pattern[str]], ...] = ( - ( - 'GitHub classic token', - re.compile(r'(? list[str]: return [label for label, pattern in secret_patterns if pattern.search(line)] - def setting_finding(settings: Mapping[str, object], key: str, expected: bool) -> str | None: + def setting_finding( + settings: Mapping[str, object], key: str, expected: bool + ) -> str | None: if key not in settings or type(settings.get(key)) is not bool: return f'repository setting {key} is UNVERIFIED (field unavailable)' actual = settings[key] @@ -126,7 +110,7 @@ jobs: return None def run_validator_self_tests() -> None: - negative_lines = ( + negatives = ( 'task-execution-listener.ts', 'risk-classification', 'desktop-task-service', @@ -135,12 +119,12 @@ jobs: 'ask-followup-question', 'the sk-prefix is discussed without a credential', ) - for line in negative_lines: + for line in negatives: labels = matching_secret_labels(line) if labels: - raise AssertionError(f'false-positive secret match for {line!r}: {labels}') + raise AssertionError(f'false-positive match for {line!r}: {labels}') - positive_lines = { + positives = { 'GitHub classic token': 'ghp_' + ('A1' * 18), 'GitHub fine-grained token': 'github_pat_' + ('A1_' * 14), 'OpenAI project or service key': 'sk-' + 'proj-' + ('A1_' * 10), @@ -152,29 +136,33 @@ jobs: 'Slack token': 'xoxb-' + ('A1-' * 8), 'Stripe live secret': 'sk_' + 'live_' + ('A1' * 8), 'Hugging Face token': 'hf_' + ('A1' * 15), - 'Private key header': '-----BEGIN PRIVATE KEY-----', + 'Private key header': '-----BEGIN ' + 'PRIVATE' + ' KEY-----', } - for expected_label, line in positive_lines.items(): - labels = matching_secret_labels(line) - if expected_label not in labels: + for expected, line in positives.items(): + observed = matching_secret_labels(line) + if expected not in observed: raise AssertionError( - f'secret detector missed synthetic {expected_label}: observed={labels}' + f'secret detector missed synthetic {expected}: {observed}' ) - if setting_finding({}, 'allow_squash_merge', True) != ( - 'repository setting allow_squash_merge is UNVERIFIED (field unavailable)' - ): - raise AssertionError('missing repository setting did not remain UNVERIFIED') - if setting_finding({'allow_squash_merge': None}, 'allow_squash_merge', True) != ( - 'repository setting allow_squash_merge is UNVERIFIED (field unavailable)' - ): - raise AssertionError('null repository setting did not remain UNVERIFIED') - if setting_finding({'allow_squash_merge': False}, 'allow_squash_merge', True) != ( - 'repository setting allow_squash_merge must be True, got False' - ): - raise AssertionError('confirmed repository setting mismatch was not rejected') - if setting_finding({'allow_squash_merge': True}, 'allow_squash_merge', True) is not None: - raise AssertionError('matching repository setting was rejected') + expected_unverified = ( + 'repository setting allow_squash_merge is UNVERIFIED ' + '(field unavailable)' + ) + if setting_finding({}, 'allow_squash_merge', True) != expected_unverified: + raise AssertionError('missing setting did not remain UNVERIFIED') + if setting_finding( + {'allow_squash_merge': None}, 'allow_squash_merge', True + ) != expected_unverified: + raise AssertionError('null setting did not remain UNVERIFIED') + if setting_finding( + {'allow_squash_merge': False}, 'allow_squash_merge', True + ) != 'repository setting allow_squash_merge must be True, got False': + raise AssertionError('confirmed mismatch was not rejected') + if setting_finding( + {'allow_squash_merge': True}, 'allow_squash_merge', True + ) is not None: + raise AssertionError('matching setting was rejected') run_validator_self_tests() if self_test_only: @@ -185,12 +173,12 @@ jobs: allowlist: dict[str, object] = {} if allowlist_path.is_file(): try: - allowlist = json.loads(allowlist_path.read_text(encoding='utf-8')) - if not isinstance(allowlist, dict): + value = json.loads(allowlist_path.read_text(encoding='utf-8')) + if not isinstance(value, dict): raise ValueError('root must be an object') + allowlist = value except Exception as exc: errors.append(f'invalid governance allowlist: {type(exc).__name__}') - allowlist = {} allowed_paths = { str(value) for value in allowlist.get('allowedPaths', []) @@ -209,27 +197,73 @@ jobs: errors.append(message) settings: dict[str, object] = {} - if not repository or not token: - record('repository settings are UNVERIFIED: repository identity or GitHub token unavailable') + if not repository or not token or '/' not in repository: + record( + 'repository settings are UNVERIFIED: ' + 'repository identity or GitHub token unavailable' + ) else: + owner, name = repository.split('/', 1) + query = ''' + query RepositoryGovernance($owner: String!, $name: String!) { + repository(owner: $owner, name: $name) { + defaultBranchRef { name } + squashMergeAllowed + mergeCommitAllowed + rebaseMergeAllowed + deleteBranchOnMerge + allowUpdateBranch + autoMergeAllowed + } + } + ''' request = urllib.request.Request( - f'https://api.github.com/repos/{repository}', + 'https://api.github.com/graphql', + data=json.dumps({ + 'query': query, + 'variables': {'owner': owner, 'name': name}, + }).encode('utf-8'), headers={ 'Accept': 'application/vnd.github+json', 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json', 'X-GitHub-Api-Version': '2022-11-28', 'User-Agent': 'neogenesis-repository-governance', }, + method='POST', ) try: with urllib.request.urlopen(request, timeout=20) as response: - value = json.load(response) - if isinstance(value, dict): - settings = value - else: - record('repository settings are UNVERIFIED: API response is not an object') + payload = json.load(response) + graph_errors = payload.get('errors') if isinstance(payload, dict) else None + repo_data = ( + payload.get('data', {}).get('repository') + if isinstance(payload, dict) + and isinstance(payload.get('data'), dict) + else None + ) + if graph_errors or not isinstance(repo_data, dict): + record('repository settings are UNVERIFIED: GraphQL read failed') + else: + default_ref = repo_data.get('defaultBranchRef') + settings = { + 'allow_squash_merge': repo_data.get('squashMergeAllowed'), + 'allow_merge_commit': repo_data.get('mergeCommitAllowed'), + 'allow_rebase_merge': repo_data.get('rebaseMergeAllowed'), + 'delete_branch_on_merge': repo_data.get('deleteBranchOnMerge'), + 'allow_update_branch': repo_data.get('allowUpdateBranch'), + 'allow_auto_merge': repo_data.get('autoMergeAllowed'), + 'default_branch': ( + default_ref.get('name') + if isinstance(default_ref, dict) + else None + ), + } except Exception as exc: - record(f'repository settings are UNVERIFIED: {type(exc).__name__}') + record( + f'repository settings are UNVERIFIED: GraphQL ' + f'{type(exc).__name__}' + ) contract = root / 'REPOSITORY_GOVERNANCE.md' if not contract.is_file(): @@ -237,12 +271,21 @@ jobs: else: text = contract.read_text(encoding='utf-8', errors='replace') if 'ng-repo-governance/1.0.0' not in text: - record('repository contract does not identify policy ng-repo-governance/1.0.0') + record( + 'repository contract does not identify policy ' + 'ng-repo-governance/1.0.0' + ) normalized = text.replace('`', '').lower() - if 'unknown' not in normalized or 'must never be reported as pass' not in normalized: + if ( + 'unknown' not in normalized + or 'must never be reported as pass' not in normalized + ): record('repository contract does not define UNKNOWN as non-PASS') if 'presence of this file alone' not in normalized: - record('repository contract does not prohibit documentation-only compliance') + record( + 'repository contract does not prohibit ' + 'documentation-only compliance' + ) required_settings = { 'allow_squash_merge': True, @@ -257,28 +300,34 @@ jobs: if finding: record(finding) - default_branch_value = settings.get('default_branch') - if not isinstance(default_branch_value, str) or not default_branch_value: + default_value = settings.get('default_branch') + if not isinstance(default_value, str) or not default_value: record('repository default branch is UNVERIFIED (field unavailable)') default_branch = '' else: - default_branch = default_branch_value + default_branch = default_value approved_exceptions = { 'NeoGenesisAI/neomux-desktop': 'einstein/main', } - task_prefixes = ('codex/', 'rebuild/', 'feature/', 'fix/', 'chore/', 'governance/') + task_prefixes = ( + 'codex/', 'rebuild/', 'feature/', 'fix/', 'chore/', 'governance/' + ) expected_exception = approved_exceptions.get(repository) if expected_exception and default_branch: if default_branch != expected_exception: record( - f'documented default-branch exception drifted: expected {expected_exception}, got {default_branch}' + 'documented default-branch exception drifted: ' + f'expected {expected_exception}, got {default_branch}' ) elif default_branch.startswith(task_prefixes): record(f'task branch is configured as repository default: {default_branch}') tracked = subprocess.check_output(['git', 'ls-files', '-z']).split(b'\0') - tracked_paths = [p.decode('utf-8', errors='surrogateescape') for p in tracked if p] + tracked_paths = [ + value.decode('utf-8', errors='surrogateescape') + for value in tracked if value + ] allowed_env_names = { '.env.example', '.env.sample', '.env.template', '.env.local.example' @@ -300,13 +349,15 @@ jobs: path = pathlib.PurePosixPath(raw) name = path.name parts = set(path.parts) - if name == '.env' or (name.startswith('.env.') and name not in allowed_env_names): + if name == '.env' or ( + name.startswith('.env.') and name not in allowed_env_names + ): record(f'prohibited environment file is tracked: {raw}', raw) if name in forbidden_names or name.endswith(forbidden_suffixes): record(f'prohibited credential or signing file is tracked: {raw}', raw) - matching_generated = sorted(parts & generated_parts) - if matching_generated: - generated_paths.add(f'{matching_generated[0]} in {raw}') + generated = sorted(parts & generated_parts) + if generated: + generated_paths.add(f'{generated[0]} in {raw}') for generated_path in sorted(generated_paths): record(f'generated or dependency directory is tracked: {generated_path}') @@ -318,6 +369,7 @@ jobs: '.bash', '.zsh', '.ps1', '.xml', '.html', '.css', '.scss', '.sql', '.gradle', '.properties', '.conf', '.config' } + special_text_names = {'Dockerfile', 'Makefile', 'Gemfile', 'Podfile'} for raw in tracked_paths: path = root / raw @@ -326,9 +378,10 @@ jobs: continue if not path.is_file() or path.stat().st_size > 2_000_000: continue - if path.suffix.lower() not in text_extensions and path.name not in { - 'Dockerfile', 'Makefile', 'Gemfile', 'Podfile' - }: + if ( + path.suffix.lower() not in text_extensions + and path.name not in special_text_names + ): continue try: content = path.read_text(encoding='utf-8') @@ -350,17 +403,17 @@ jobs: if unique_errors: summary_lines.extend(['', '### Fail-closed findings', '']) summary_lines.extend(f'- `{item}`' for item in unique_errors) - if notes: - summary_lines.extend(['', '### Notes', '']) - summary_lines.extend(f'- {item}' for item in notes) if summary_path: - pathlib.Path(summary_path).write_text('\n'.join(summary_lines) + '\n', encoding='utf-8') + pathlib.Path(summary_path).write_text( + '\n'.join(summary_lines) + '\n', encoding='utf-8' + ) if unique_errors: for item in unique_errors: print(f'::error::{item}') print( - f'repository-governance failed with {len(unique_errors)} unique finding(s)', + f'repository-governance failed with ' + f'{len(unique_errors)} unique finding(s)', file=sys.stderr, ) raise SystemExit(1) From b9be4aa9939adde6d648e601fc5ae2eda07af33d Mon Sep 17 00:00:00 2001 From: Yesol Heo | Creative Developer <86992002+Yesol-Pilot@users.noreply.github.com> Date: Thu, 27 Aug 2026 23:13:08 +0900 Subject: [PATCH 4/4] ci(governance): cancel superseded validator self-tests --- .../workflows/repository-governance-validator-selftest.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/repository-governance-validator-selftest.yml b/.github/workflows/repository-governance-validator-selftest.yml index fd9b8e9..269f695 100644 --- a/.github/workflows/repository-governance-validator-selftest.yml +++ b/.github/workflows/repository-governance-validator-selftest.yml @@ -10,6 +10,10 @@ on: permissions: contents: read +concurrency: + group: repository-governance-validator-selftest-${{ github.ref }} + cancel-in-progress: true + jobs: validator-self-test: uses: ./.github/workflows/repository-governance-reusable.yml