diff --git a/.github/workflows/scheduled-security-scan.yml b/.github/workflows/scheduled-security-scan.yml index 8ecb5185..6b19cf25 100644 --- a/.github/workflows/scheduled-security-scan.yml +++ b/.github/workflows/scheduled-security-scan.yml @@ -90,13 +90,13 @@ jobs: with: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + uses: github/codeql-action/init@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - name: Perform CodeQL Analysis continue-on-error: true - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + uses: github/codeql-action/analyze@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4 with: category: "/language:${{ matrix.language }}-scheduled" diff --git a/scripts/ci/pr_review_merge_scheduler.py b/scripts/ci/pr_review_merge_scheduler.py index 75e18c86..dd0c0a43 100644 --- a/scripts/ci/pr_review_merge_scheduler.py +++ b/scripts/ci/pr_review_merge_scheduler.py @@ -12,6 +12,7 @@ import subprocess import sys import time +import unicodedata from collections.abc import Sequence from dataclasses import dataclass from datetime import datetime, timezone @@ -126,7 +127,7 @@ RUNNING_CHECK_STATES = {"PENDING", "EXPECTED", "QUEUED", "IN_PROGRESS", "WAITING", "REQUESTED"} FAILED_CHECK_CONCLUSIONS = {"FAILURE", "ERROR", "CANCELLED", "TIMED_OUT", "STARTUP_FAILURE"} ACTION_REQUIRED_CONCLUSIONS = {"ACTION_REQUIRED"} -GIT_REF_RE = re.compile(r"^(?!-)[A-Za-z0-9._/-]+$") +GIT_REF_ASCII_SAFE_CHARS = frozenset("._/-") GIT_SHA_RE = re.compile(r"^[0-9a-fA-F]{40}$") GITHUB_REPOSITORY_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") REVIEW_BODY_HEAD_SHA_RE = re.compile(r"Head SHA:\s*`([0-9a-fA-F]{40})`") @@ -529,12 +530,24 @@ def split_repo(repo: str) -> tuple[str, str]: def validate_git_ref(ref: str) -> str: - """Return a conservative Git ref name for gh workflow dispatch fields.""" + """Return a conservative Unicode-capable Git ref for structured argv/JSON fields.""" + has_unsafe_character = isinstance(ref, str) and any( + ( + character.isascii() + and not (character.isalnum() or character in GIT_REF_ASCII_SAFE_CHARS) + ) + or ( + not character.isascii() + and unicodedata.category(character)[0] in {"C", "Z"} + ) + for character in ref + ) if ( not isinstance(ref, str) or not ref - or not GIT_REF_RE.fullmatch(ref) + or has_unsafe_character or ref == "HEAD" + or ref.startswith("-") or ref.startswith("/") or ref.endswith(("/", ".")) or "@{" in ref @@ -542,7 +555,10 @@ def validate_git_ref(ref: str) -> str: or "//" in ref ): raise ValueError(f"invalid git ref: {ref!r}") - if any(part == "." or part.startswith(".") for part in ref.split("/")): + if any( + part == "." or part.startswith(".") or part.endswith(".lock") + for part in ref.split("/") + ): raise ValueError(f"invalid git ref: {ref!r}") return ref diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index 3e421e90..f7aa640d 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -162,7 +162,12 @@ def test_run_split_repo_and_graphql(monkeypatch): with pytest.raises(ValueError): sched.split_repo("/repo") - assert sched.validate_git_ref("feature/safe.branch-1") == "feature/safe.branch-1" + for valid_ref in ( + "feature/safe.branch-1", + "🎨-palette-ux-improvement-13325911538352561627", + "기능/달력-개선", + ): + assert sched.validate_git_ref(valid_ref) == valid_ref for bad_ref in ( "", "-bad", @@ -172,8 +177,11 @@ def test_run_split_repo_and_graphql(monkeypatch): "feature/./main", "feature//main", "feature/main.", + "feature/main.lock", "feature/@{upstream}", "feat;echo pwned", + "feature/\u00a0hidden", + "feature/\u200bhidden", ): with pytest.raises(ValueError): sched.validate_git_ref(bad_ref) @@ -1891,6 +1899,7 @@ def test_missing_evidence_dispatch_uses_central_required_workflow_repository(mon calls = [] head_sha = "a" * 40 base_sha = "b" * 40 + head_ref = "🎨-palette-ux-improvement-13325911538352561627" def fake_run_with_env(args, *, stdin=None, env=None): calls.append((args, stdin, None if env is None else env.get("GH_TOKEN"))) @@ -1905,7 +1914,12 @@ def fake_run_with_env(args, *, stdin=None, env=None): monkeypatch.setenv("SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY", "ContextualWisdomLab/.github") monkeypatch.setenv("SCHEDULER_REQUIRED_WORKFLOW_REF", "main") - pr = make_pr(baseRefName="develop", baseRefOid=base_sha, headRefOid=head_sha) + pr = make_pr( + baseRefName="develop", + baseRefOid=base_sha, + headRefName=head_ref, + headRefOid=head_sha, + ) sched.dispatch_strix_evidence("owner/repo", "Strix Security Scan", pr, dry_run=False) sched.dispatch_opencode_review("owner/repo", "OpenCode Review", pr, dry_run=False) @@ -1950,7 +1964,7 @@ def fake_run_with_env(args, *, stdin=None, env=None): "pr_number": 1, "pr_base_ref": "develop", "pr_base_sha": base_sha, - "pr_head_ref": "feature", + "pr_head_ref": head_ref, "pr_head_sha": head_sha, }, }