From fa06536652edf707e961f3e6ff950340b2c41315 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 18:47:17 +0900 Subject: [PATCH 1/8] fix(security-scan): name Git's initial branch for every exact-head checkout Closes #2101. Each actions/checkout step initialises a fresh repository before fetching the exact SHA, and Git 2.28+ prints the "Using 'master' as the name for the initial branch" hint plus the Git 3.0 rename warning on every hosted job. Workflow-level GIT_CONFIG_COUNT/KEY_0/VALUE_0 sets init.defaultBranch=main process-locally -- no global gitconfig write, no stderr suppression -- which the issue's hosted probe verified removes both lines. A dedicated contract test pins the three variables at workflow level and rejects per-job overrides or global writes. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/security-scan.yml | 10 ++++ ...y_scan_checkout_default_branch_contract.py | 49 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 tests/test_security_scan_checkout_default_branch_contract.py diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 500e22b4ab..0f902093e8 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -60,6 +60,16 @@ permissions: actions: read contents: read +# Every `actions/checkout` below initialises a fresh repository before it +# fetches the exact SHA; with no initial-branch setting Git 2.28+ prints the +# "Using 'master' as the name for the initial branch" hint (and the Git 3.0 +# rename warning) on every job. Process-local config -- not a global +# gitconfig write and not stderr suppression -- names it `main`. See #2101. +env: + GIT_CONFIG_COUNT: "1" + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + jobs: changed-scope: name: Detect changed scope diff --git a/tests/test_security_scan_checkout_default_branch_contract.py b/tests/test_security_scan_checkout_default_branch_contract.py new file mode 100644 index 0000000000..ea02d74307 --- /dev/null +++ b/tests/test_security_scan_checkout_default_branch_contract.py @@ -0,0 +1,49 @@ +"""Contract: the central Security Scan names Git's initial branch explicitly. + +Every `actions/checkout` step in `security-scan.yml` initialises a fresh +repository before fetching the exact SHA. Without an initial-branch setting +Git 2.28+ prints `hint: Using 'master' as the name for the initial branch` +(plus the Git 3.0 rename warning) on every hosted job -- observed live on +consumer runs (issue #2101). The repair is process-local Git configuration +through `GIT_CONFIG_COUNT` / `GIT_CONFIG_KEY_0` / `GIT_CONFIG_VALUE_0` at the +workflow level, so it reaches the action's internal `git init` without a +global gitconfig write and without hiding stderr. +""" + +from __future__ import annotations + +from pathlib import Path +import re + + +WORKFLOW = Path(__file__).resolve().parents[1] / ".github/workflows/security-scan.yml" + + +def _workflow_level_env(workflow: str) -> str: + """Return the top-level ``env:`` mapping text (between ``permissions:`` and ``jobs:``).""" + header = workflow.split("\njobs:\n", 1)[0] + match = re.search(r"(?ms)^env:\n((?: .*\n)+)", header) + assert match, "security-scan.yml has no workflow-level env: block" + return match.group(1) + + +def test_workflow_level_git_config_names_the_initial_branch() -> None: + """The three process-local Git config variables must be set exactly.""" + workflow = WORKFLOW.read_text(encoding="utf-8") + env = _workflow_level_env(workflow) + assert 'GIT_CONFIG_COUNT: "1"' in env + assert "GIT_CONFIG_KEY_0: init.defaultBranch" in env + assert "GIT_CONFIG_VALUE_0: main" in env + assert "#2101" in workflow.split("\njobs:\n", 1)[0] + + +def test_no_step_overrides_or_globalises_the_initial_branch_setting() -> None: + """Jobs must neither shadow the variables nor write a global gitconfig.""" + workflow = WORKFLOW.read_text(encoding="utf-8") + body = workflow.split("\njobs:\n", 1)[1] + assert "GIT_CONFIG_COUNT" not in body + assert "GIT_CONFIG_KEY_0" not in body + assert "git config --global" not in body + assert "init.defaultBranch" not in body + # The setting only matters because the exact-head checkouts exist. + assert body.count("uses: actions/checkout@") >= 6 From df7b8564fc6042858cc232371f823cf9d5440bcd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 20:06:21 +0900 Subject: [PATCH 2/8] test(security-scan): expose Git config value override gap --- ...y_scan_checkout_default_branch_contract.py | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/tests/test_security_scan_checkout_default_branch_contract.py b/tests/test_security_scan_checkout_default_branch_contract.py index ea02d74307..c9580069bd 100644 --- a/tests/test_security_scan_checkout_default_branch_contract.py +++ b/tests/test_security_scan_checkout_default_branch_contract.py @@ -15,6 +15,8 @@ from pathlib import Path import re +import pytest + WORKFLOW = Path(__file__).resolve().parents[1] / ".github/workflows/security-scan.yml" @@ -27,6 +29,16 @@ def _workflow_level_env(workflow: str) -> str: return match.group(1) +def _assert_jobs_do_not_override_initial_branch(body: str) -> None: + """Reject job or step configuration that can shadow the workflow Git key.""" + assert "GIT_CONFIG_COUNT" not in body + assert "GIT_CONFIG_KEY_0" not in body + assert "git config --global" not in body + assert "init.defaultBranch" not in body + # The setting only matters because the exact-head checkouts exist. + assert body.count("uses: actions/checkout@") >= 6 + + def test_workflow_level_git_config_names_the_initial_branch() -> None: """The three process-local Git config variables must be set exactly.""" workflow = WORKFLOW.read_text(encoding="utf-8") @@ -41,9 +53,20 @@ def test_no_step_overrides_or_globalises_the_initial_branch_setting() -> None: """Jobs must neither shadow the variables nor write a global gitconfig.""" workflow = WORKFLOW.read_text(encoding="utf-8") body = workflow.split("\njobs:\n", 1)[1] - assert "GIT_CONFIG_COUNT" not in body - assert "GIT_CONFIG_KEY_0" not in body - assert "git config --global" not in body - assert "init.defaultBranch" not in body - # The setting only matters because the exact-head checkouts exist. - assert body.count("uses: actions/checkout@") >= 6 + _assert_jobs_do_not_override_initial_branch(body) + + +def test_git_config_value_override_is_rejected() -> None: + """A job-level value override must not redirect checkout back to another branch name.""" + workflow = WORKFLOW.read_text(encoding="utf-8") + body = workflow.split("\njobs:\n", 1)[1] + hostile_body = ( + " hostile-checkout:\n" + " runs-on: ubuntu-24.04\n" + " env:\n" + " GIT_CONFIG_VALUE_0: master\n" + " steps: []\n" + + body + ) + with pytest.raises(AssertionError): + _assert_jobs_do_not_override_initial_branch(hostile_body) From af7a068772204d9bde0518510d80733bd5a5206d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 20:07:27 +0900 Subject: [PATCH 3/8] fix(security-scan): reject Git config value shadowing --- tests/test_security_scan_checkout_default_branch_contract.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_security_scan_checkout_default_branch_contract.py b/tests/test_security_scan_checkout_default_branch_contract.py index c9580069bd..fc520d2e6d 100644 --- a/tests/test_security_scan_checkout_default_branch_contract.py +++ b/tests/test_security_scan_checkout_default_branch_contract.py @@ -33,6 +33,7 @@ def _assert_jobs_do_not_override_initial_branch(body: str) -> None: """Reject job or step configuration that can shadow the workflow Git key.""" assert "GIT_CONFIG_COUNT" not in body assert "GIT_CONFIG_KEY_0" not in body + assert "GIT_CONFIG_VALUE_0" not in body assert "git config --global" not in body assert "init.defaultBranch" not in body # The setting only matters because the exact-head checkouts exist. From 4e2be9aff8fd4369d3c39d2c1f26ed9c5ff3768a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 21:05:25 +0900 Subject: [PATCH 4/8] test(security-scan): reject scalar env impersonation --- ...y_scan_checkout_default_branch_contract.py | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/tests/test_security_scan_checkout_default_branch_contract.py b/tests/test_security_scan_checkout_default_branch_contract.py index fc520d2e6d..a866a46417 100644 --- a/tests/test_security_scan_checkout_default_branch_contract.py +++ b/tests/test_security_scan_checkout_default_branch_contract.py @@ -29,6 +29,14 @@ def _workflow_level_env(workflow: str) -> str: return match.group(1) +def _assert_workflow_level_git_config(workflow: str) -> None: + """Require the reviewed process-local Git initial-branch configuration.""" + env = _workflow_level_env(workflow) + assert 'GIT_CONFIG_COUNT: "1"' in env + assert "GIT_CONFIG_KEY_0: init.defaultBranch" in env + assert "GIT_CONFIG_VALUE_0: main" in env + + def _assert_jobs_do_not_override_initial_branch(body: str) -> None: """Reject job or step configuration that can shadow the workflow Git key.""" assert "GIT_CONFIG_COUNT" not in body @@ -43,10 +51,7 @@ def _assert_jobs_do_not_override_initial_branch(body: str) -> None: def test_workflow_level_git_config_names_the_initial_branch() -> None: """The three process-local Git config variables must be set exactly.""" workflow = WORKFLOW.read_text(encoding="utf-8") - env = _workflow_level_env(workflow) - assert 'GIT_CONFIG_COUNT: "1"' in env - assert "GIT_CONFIG_KEY_0: init.defaultBranch" in env - assert "GIT_CONFIG_VALUE_0: main" in env + _assert_workflow_level_git_config(workflow) assert "#2101" in workflow.split("\njobs:\n", 1)[0] @@ -71,3 +76,22 @@ def test_git_config_value_override_is_rejected() -> None: ) with pytest.raises(AssertionError): _assert_jobs_do_not_override_initial_branch(hostile_body) + + +def test_block_scalar_cannot_impersonate_workflow_git_config() -> None: + """Indented scalar text must not count as direct workflow ``env`` authority.""" + hostile_workflow = """name: hostile +permissions: + contents: read +env: + DECOY: | + GIT_CONFIG_COUNT: \"1\" + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main +jobs: + scan: + runs-on: ubuntu-24.04 + steps: [] +""" + with pytest.raises(AssertionError): + _assert_workflow_level_git_config(hostile_workflow) From 184d348e7ad65ce4258781c4d4d72deff8c307a4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 21:05:44 +0900 Subject: [PATCH 5/8] ci: verify security-scan initial-branch contract --- ...-security-scan-default-branch-contract.yml | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .github/workflows/verify-security-scan-default-branch-contract.yml diff --git a/.github/workflows/verify-security-scan-default-branch-contract.yml b/.github/workflows/verify-security-scan-default-branch-contract.yml new file mode 100644 index 0000000000..aa3b5ab1a2 --- /dev/null +++ b/.github/workflows/verify-security-scan-default-branch-contract.yml @@ -0,0 +1,49 @@ +name: Verify Security Scan initial-branch contract + +on: + push: + branches: + - fix/security-scan-checkout-default-branch-2101 + +permissions: + contents: read + +env: + GIT_CONFIG_COUNT: "1" + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + +jobs: + verify: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout exact source revision + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.sha }} + persist-credentials: false + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14" + - name: Install exact focused-test dependencies + env: + PIP_DISABLE_PIP_VERSION_CHECK: "1" + PIP_NO_INPUT: "1" + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + cat >"${RUNNER_TEMP}/requirements.txt" <<'EOF' + iniconfig==2.1.0 --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 + packaging==26.2 --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e + pluggy==1.6.0 --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + pygments==2.20.0 --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + pytest==9.1.1 --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c + EOF + python -m pip install --only-binary=:all: --require-hashes -r "${RUNNER_TEMP}/requirements.txt" + - name: Verify exact hostile regression + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + test "$(git rev-parse HEAD)" = "${GITHUB_SHA}" + python -m pytest tests/test_security_scan_checkout_default_branch_contract.py -q + git diff --exit-code From 7d8fe3b250bf8edac654ae5e7e18b826badcbb8e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 21:07:05 +0900 Subject: [PATCH 6/8] test(security-scan): make scalar impersonation regression effective --- tests/test_security_scan_checkout_default_branch_contract.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_security_scan_checkout_default_branch_contract.py b/tests/test_security_scan_checkout_default_branch_contract.py index a866a46417..fd8b6d173b 100644 --- a/tests/test_security_scan_checkout_default_branch_contract.py +++ b/tests/test_security_scan_checkout_default_branch_contract.py @@ -88,6 +88,7 @@ def test_block_scalar_cannot_impersonate_workflow_git_config() -> None: GIT_CONFIG_COUNT: \"1\" GIT_CONFIG_KEY_0: init.defaultBranch GIT_CONFIG_VALUE_0: main + decoy-padding jobs: scan: runs-on: ubuntu-24.04 From 287b0c1c5cf4031ca091f5000c01580ad8766db3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 21:08:39 +0900 Subject: [PATCH 7/8] test(security-scan): require direct workflow env scalars --- ...security_scan_checkout_default_branch_contract.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/test_security_scan_checkout_default_branch_contract.py b/tests/test_security_scan_checkout_default_branch_contract.py index fd8b6d173b..0f07baaa57 100644 --- a/tests/test_security_scan_checkout_default_branch_contract.py +++ b/tests/test_security_scan_checkout_default_branch_contract.py @@ -29,12 +29,18 @@ def _workflow_level_env(workflow: str) -> str: return match.group(1) +def _assert_direct_env_scalar(env: str, key: str, rendered_value: str) -> None: + """Require one exact direct scalar entry in the workflow-level env mapping.""" + pattern = rf"(?m)^ {re.escape(key)}: {re.escape(rendered_value)}$" + assert len(re.findall(pattern, env)) == 1, f"missing or duplicate direct env key: {key}" + + def _assert_workflow_level_git_config(workflow: str) -> None: """Require the reviewed process-local Git initial-branch configuration.""" env = _workflow_level_env(workflow) - assert 'GIT_CONFIG_COUNT: "1"' in env - assert "GIT_CONFIG_KEY_0: init.defaultBranch" in env - assert "GIT_CONFIG_VALUE_0: main" in env + _assert_direct_env_scalar(env, "GIT_CONFIG_COUNT", '"1"') + _assert_direct_env_scalar(env, "GIT_CONFIG_KEY_0", "init.defaultBranch") + _assert_direct_env_scalar(env, "GIT_CONFIG_VALUE_0", "main") def _assert_jobs_do_not_override_initial_branch(body: str) -> None: From 77a4eacb64014650d2818ced739c30e334185ddc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 21:09:30 +0900 Subject: [PATCH 8/8] ci: retire security-scan contract verifier --- ...-security-scan-default-branch-contract.yml | 49 ------------------- 1 file changed, 49 deletions(-) delete mode 100644 .github/workflows/verify-security-scan-default-branch-contract.yml diff --git a/.github/workflows/verify-security-scan-default-branch-contract.yml b/.github/workflows/verify-security-scan-default-branch-contract.yml deleted file mode 100644 index aa3b5ab1a2..0000000000 --- a/.github/workflows/verify-security-scan-default-branch-contract.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Verify Security Scan initial-branch contract - -on: - push: - branches: - - fix/security-scan-checkout-default-branch-2101 - -permissions: - contents: read - -env: - GIT_CONFIG_COUNT: "1" - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: main - -jobs: - verify: - runs-on: ubuntu-24.04 - timeout-minutes: 10 - steps: - - name: Checkout exact source revision - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ github.sha }} - persist-credentials: false - - name: Set up Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: "3.14" - - name: Install exact focused-test dependencies - env: - PIP_DISABLE_PIP_VERSION_CHECK: "1" - PIP_NO_INPUT: "1" - shell: bash --noprofile --norc -e -o pipefail {0} - run: | - cat >"${RUNNER_TEMP}/requirements.txt" <<'EOF' - iniconfig==2.1.0 --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 - packaging==26.2 --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e - pluggy==1.6.0 --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 - pygments==2.20.0 --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 - pytest==9.1.1 --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c - EOF - python -m pip install --only-binary=:all: --require-hashes -r "${RUNNER_TEMP}/requirements.txt" - - name: Verify exact hostile regression - shell: bash --noprofile --norc -e -o pipefail {0} - run: | - test "$(git rev-parse HEAD)" = "${GITHUB_SHA}" - python -m pytest tests/test_security_scan_checkout_default_branch_contract.py -q - git diff --exit-code