Files
teng-lin--notebooklm-py/tests/unit/test_check_workflow_secret_gates.py
wehub-resource-sync 09e9f3545f
Test / Code Quality (push) Has been cancelled
Test / Test (macos-latest, Python 3.10) (push) Has been cancelled
Test / Test (macos-latest, Python 3.11) (push) Has been cancelled
Test / Test (macos-latest, Python 3.12) (push) Has been cancelled
Test / Test (macos-latest, Python 3.13) (push) Has been cancelled
Test / Test (macos-latest, Python 3.14) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.10) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.11) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.12) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.13) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.14) (push) Has been cancelled
Test / Test (windows-latest, Python 3.10) (push) Has been cancelled
Test / Test (windows-latest, Python 3.11) (push) Has been cancelled
Test / Test (windows-latest, Python 3.12) (push) Has been cancelled
Test / Test (windows-latest, Python 3.13) (push) Has been cancelled
Test / Test (windows-latest, Python 3.14) (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
dependency-audit / pip-audit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:30:13 +08:00

811 lines
24 KiB
Python

"""Tests for ``scripts/check_workflow_secret_gates.py``.
Verifies the static check correctly accepts gated jobs and rejects
ungated jobs across the four gate shapes used in this repo:
* ``environment:`` at job level (literal name).
* ``environment:`` at job level (conditional expression).
* job-level ``if:`` guard pinning ``sender.login`` to a maintainer.
* step-level ``if:`` guard referencing ``is_standard``.
The script is imported via spec-loading (matching the sibling
``test_check_coverage_thresholds.py`` convention) because ``scripts/`` is
not a package in this repo.
"""
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
from textwrap import dedent
import pytest
pytestmark = pytest.mark.repo_lint
REPO_ROOT = Path(__file__).resolve().parents[2]
SCRIPT_PATH = REPO_ROOT / "scripts" / "check_workflow_secret_gates.py"
def _load_module():
spec = importlib.util.spec_from_file_location("check_workflow_secret_gates", SCRIPT_PATH)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
@pytest.fixture()
def script():
return _load_module()
def _write_workflow(dir_path: Path, name: str, body: str) -> Path:
p = dir_path / name
p.write_text(dedent(body).lstrip("\n"))
return p
def _run(script, tmp_path, monkeypatch, capsys) -> tuple[int, str, str]:
monkeypatch.setattr(
sys,
"argv",
["check_workflow_secret_gates.py", "--workflow-dir", str(tmp_path)],
)
rc = script.main()
captured = capsys.readouterr()
return rc, captured.out, captured.err
def test_ungated_secret_fails(tmp_path, monkeypatch, capsys, script):
_write_workflow(
tmp_path,
"bad.yml",
"""
name: bad
on:
workflow_dispatch:
jobs:
oops:
runs-on: ubuntu-latest
steps:
- name: leak
env:
X: ${{ secrets.MY_SECRET }}
run: echo "$X"
""",
)
rc, _out, err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 1
assert "secrets.MY_SECRET" in err
assert "'oops'" in err
def test_environment_literal_passes(tmp_path, monkeypatch, capsys, script):
_write_workflow(
tmp_path,
"ok_env.yml",
"""
name: ok-env
on:
workflow_dispatch:
jobs:
fine:
runs-on: ubuntu-latest
environment: protected-readonly
steps:
- name: use
env:
X: ${{ secrets.MY_SECRET }}
run: echo "$X"
""",
)
rc, _out, _err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 0
def test_environment_expression_with_empty_fallback_fails(tmp_path, monkeypatch, capsys, script):
"""The conditional shape that broke #1009 must now fail the checker.
Historically rpc-health.yml / nightly.yml used:
environment: ${{ github.event_name == 'workflow_dispatch'
&& 'protected-readonly' || '' }}
The empty-string fallback means "no environment association" on
scheduled runs, which silently broke once the consumed secret was
migrated env-only. The fix lifts the conditional and binds the env
unconditionally; this test pins that regression at the checker layer
so the conditional shape can never come back.
"""
_write_workflow(
tmp_path,
"bad_env_expr.yml",
"""
name: bad-env-expr
on:
workflow_dispatch:
schedule:
- cron: '0 7 * * *'
jobs:
fine:
runs-on: ubuntu-latest
environment: ${{ github.event_name == 'workflow_dispatch' && 'protected-readonly' || '' }}
steps:
- name: use
env:
X: ${{ secrets.MY_SECRET }}
run: echo "$X"
""",
)
rc, _out, _err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 1
def test_environment_unconditional_passes(tmp_path, monkeypatch, capsys, script):
"""The corrected unconditional binding must satisfy the gate."""
_write_workflow(
tmp_path,
"ok_env_uncond.yml",
"""
name: ok-env-uncond
on:
workflow_dispatch:
schedule:
- cron: '0 7 * * *'
jobs:
fine:
runs-on: ubuntu-latest
environment: protected-readonly
steps:
- name: use
env:
X: ${{ secrets.MY_SECRET }}
run: echo "$X"
""",
)
rc, _out, _err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 0
def test_step_if_with_is_standard_passes(tmp_path, monkeypatch, capsys, script):
_write_workflow(
tmp_path,
"ok_step_if.yml",
"""
name: ok-step-if
on:
workflow_dispatch:
jobs:
resolve:
runs-on: ubuntu-latest
outputs:
is_standard: ${{ steps.r.outputs.is_standard }}
steps:
- id: r
run: echo "is_standard=true" >> "$GITHUB_OUTPUT"
fine:
needs: resolve
runs-on: ubuntu-latest
steps:
- name: use
if: needs.resolve.outputs.is_standard == 'true'
env:
X: ${{ secrets.MY_SECRET }}
run: echo "$X"
""",
)
rc, _out, _err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 0
def test_step_if_block_scalar_with_is_standard_passes(tmp_path, monkeypatch, capsys, script):
# The ``if: |`` block-scalar form must be parsed too. nightly.yml's
# Retry step combines is_standard with steps.e2e.outcome via ``|``.
_write_workflow(
tmp_path,
"ok_block_if.yml",
"""
name: ok-block-if
on:
workflow_dispatch:
jobs:
fine:
runs-on: ubuntu-latest
steps:
- name: use
if: |
needs.r.outputs.is_standard == 'true'
&& always()
env:
X: ${{ secrets.MY_SECRET }}
run: echo "$X"
""",
)
rc, _out, _err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 0
def test_job_if_with_sender_login_passes(tmp_path, monkeypatch, capsys, script):
# claude.yml convention: pin sender.login to the maintainer at job
# level. The step inside doesn't need its own gate because the job
# already won't run for any other actor.
_write_workflow(
tmp_path,
"ok_job_if.yml",
"""
name: ok-job-if
on:
issue_comment:
types: [created]
jobs:
claude:
if: github.event.sender.login == 'teng-lin'
runs-on: ubuntu-latest
steps:
- name: use
env:
X: ${{ secrets.CLAUDE_TOKEN }}
run: echo "$X"
""",
)
rc, _out, _err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 0
def test_job_if_block_scalar_with_sender_login_passes(tmp_path, monkeypatch, capsys, script):
# claude.yml uses the block-scalar form for its multi-line guard;
# the job-level parser must accept that shape.
_write_workflow(
tmp_path,
"ok_job_block_if.yml",
"""
name: ok-job-block-if
on:
issue_comment:
types: [created]
jobs:
claude:
if: |
github.event.sender.login == 'teng-lin' && (
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude'))
)
runs-on: ubuntu-latest
steps:
- name: use
env:
X: ${{ secrets.CLAUDE_TOKEN }}
run: echo "$X"
""",
)
rc, _out, _err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 0
def test_github_token_is_benign(tmp_path, monkeypatch, capsys, script):
# GITHUB_TOKEN is auto-provisioned and constrained by `permissions:`,
# so it does not need an environment gate.
_write_workflow(
tmp_path,
"ok_github_token.yml",
"""
name: ok-github-token
on:
workflow_dispatch:
jobs:
benign:
runs-on: ubuntu-latest
steps:
- name: use
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh issue list
""",
)
rc, _out, _err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 0
def test_empty_string_environment_does_not_count(tmp_path, monkeypatch, capsys, script):
# Literal empty string is not a real environment association — must
# still fail. The expression form with an empty-string fallback also
# fails (see ``test_environment_expression_with_empty_fallback_fails``
# above); this test pins the bare ``environment: ''`` shape.
_write_workflow(
tmp_path,
"bad_empty_env.yml",
"""
name: bad-empty-env
on:
workflow_dispatch:
jobs:
oops:
runs-on: ubuntu-latest
environment: ""
steps:
- name: use
env:
X: ${{ secrets.MY_SECRET }}
run: echo "$X"
""",
)
rc, _out, err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 1
assert "secrets.MY_SECRET" in err
def test_secret_reference_inside_comment_ignored(tmp_path, monkeypatch, capsys, script):
# Example/documentation references inside `#` comments must not
# trigger the gate. The verify-package.yml comment block in this PR
# demonstrates the pattern.
_write_workflow(
tmp_path,
"ok_comment.yml",
"""
name: ok-comment
on:
workflow_dispatch:
jobs:
fine:
# This is an example: ${{ secrets.DOCS_ONLY }} is not real.
runs-on: ubuntu-latest
steps:
# Note: secrets.ALSO_DOCS would normally need a gate.
- name: only echo
run: echo "no secret here"
""",
)
rc, _out, _err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 0
def test_missing_directory_returns_2(tmp_path, monkeypatch, capsys, script):
bogus = tmp_path / "does-not-exist"
monkeypatch.setattr(
sys,
"argv",
["check_workflow_secret_gates.py", "--workflow-dir", str(bogus)],
)
rc = script.main()
captured = capsys.readouterr()
assert rc == 2
assert "Not a directory" in captured.err
def test_secret_before_environment_in_same_job_passes(tmp_path, monkeypatch, capsys, script):
# Key-order false-positive guard. ``environment:`` may appear AFTER a
# ``${{ secrets.X }}`` reference in the same job (e.g. inside a
# ``strategy: matrix`` block placed before the environment line by
# an author following a convention from a different repo). The
# checker must evaluate the gate against the JOB-FINAL state, not
# the prefix-at-line state.
_write_workflow(
tmp_path,
"ok_secret_first.yml",
"""
name: ok-secret-first
on:
workflow_dispatch:
jobs:
fine:
runs-on: ubuntu-latest
strategy:
matrix:
tag: ["${{ secrets.MY_SECRET }}"]
environment: protected-readonly
steps:
- name: use
env:
X: ${{ secrets.MY_SECRET }}
run: echo "$X"
""",
)
rc, _out, _err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 0
def test_secret_in_step_before_step_if_passes(tmp_path, monkeypatch, capsys, script):
# Step-scope key-order guard. ``if:`` may appear AFTER the ``env:``
# block in the same step. Checker must defer step-gate evaluation
# to end-of-step rather than judge against the prefix state.
_write_workflow(
tmp_path,
"ok_step_env_first.yml",
"""
name: ok-step-env-first
on:
workflow_dispatch:
jobs:
resolve:
runs-on: ubuntu-latest
outputs:
is_standard: ${{ steps.r.outputs.is_standard }}
steps:
- id: r
run: echo "is_standard=true" >> "$GITHUB_OUTPUT"
fine:
needs: resolve
runs-on: ubuntu-latest
steps:
- name: use
env:
X: ${{ secrets.MY_SECRET }}
if: needs.resolve.outputs.is_standard == 'true'
run: echo "$X"
""",
)
rc, _out, _err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 0
def test_trailing_comment_secret_reference_ignored(tmp_path, monkeypatch, capsys, script):
# Trailing ``# ...`` comments on otherwise non-comment lines must
# not produce false positives. Authors may write notes like
# ``run: echo hi # don't use ${{ secrets.X }} here``.
_write_workflow(
tmp_path,
"ok_trailing_comment.yml",
"""
name: ok-trailing-comment
on:
workflow_dispatch:
jobs:
fine:
runs-on: ubuntu-latest
steps:
- name: noop with trailing comment
run: echo hi # don't use ${{ secrets.MY_SECRET }} here
""",
)
rc, _out, _err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 0
def test_is_standard_inside_block_scalar_comment_does_not_satisfy(
tmp_path, monkeypatch, capsys, script
):
# Defence against an `if: |` block-scalar that ONLY contains the
# ``is_standard`` token inside a YAML comment. Such a guard does
# NOT actually gate the run (comments are stripped before
# expression evaluation), so the checker must reject it.
_write_workflow(
tmp_path,
"bad_comment_decoy.yml",
"""
name: bad-comment-decoy
on:
workflow_dispatch:
jobs:
oops:
runs-on: ubuntu-latest
steps:
- name: leak
if: |
always() # is_standard
env:
X: ${{ secrets.MY_SECRET }}
run: echo "$X"
""",
)
rc, _out, err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 1
assert "secrets.MY_SECRET" in err
def test_jobs_present_but_no_jobs_parsed_surfaces_violation(tmp_path, monkeypatch, capsys, script):
# Robustness against silent-pass. If the YAML contains a ``jobs:``
# header AND ``secrets.*`` references but the parser identifies no
# jobs (e.g. unexpected indentation), the checker must surface that
# rather than report "OK". Use a 3-space indent (legal YAML, but
# the parser is pinned to 2-space) to trigger the miss.
p = tmp_path / "weird_indent.yml"
p.write_text(
"name: weird\n"
"on: workflow_dispatch:\n"
"jobs:\n"
" weird:\n"
" runs-on: ubuntu-latest\n"
" steps:\n"
" - run: echo ${{ secrets.X }}\n"
)
rc, _out, err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 1
assert "parser identified no jobs" in err
def test_negation_guard_at_step_does_not_pass(tmp_path, monkeypatch, capsys, script):
# An ``if: needs.r.outputs.is_standard != 'true'`` guard is the
# inverse of the trusted condition — it explicitly RUNS on non-
# standard branches, which is the opposite of what we want. The
# checker must reject it (C-CODEX-3 fix).
_write_workflow(
tmp_path,
"bad_negation_step.yml",
"""
name: bad-negation-step
on:
workflow_dispatch:
jobs:
oops:
runs-on: ubuntu-latest
steps:
- name: leak
if: needs.r.outputs.is_standard != 'true'
env:
X: ${{ secrets.MY_SECRET }}
run: echo "$X"
""",
)
rc, _out, err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 1
assert "secrets.MY_SECRET" in err
def test_negation_actor_guard_at_job_does_not_pass(tmp_path, monkeypatch, capsys, script):
# ``if: github.actor != 'dependabot[bot]'`` substring-matches
# ``github.actor`` but is NOT a trusted pin — it accepts every
# actor except dependabot. Reject.
_write_workflow(
tmp_path,
"bad_actor_negation.yml",
"""
name: bad-actor-negation
on:
workflow_dispatch:
jobs:
oops:
if: github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
- name: leak
env:
X: ${{ secrets.MY_SECRET }}
run: echo "$X"
""",
)
rc, _out, err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 1
assert "secrets.MY_SECRET" in err
def test_bare_actor_reference_does_not_pass(tmp_path, monkeypatch, capsys, script):
# ``if: github.actor`` (truthy check — runs whenever actor is set,
# i.e. always) is not a positive comparison and must be rejected.
_write_workflow(
tmp_path,
"bad_bare_actor.yml",
"""
name: bad-bare-actor
on:
workflow_dispatch:
jobs:
oops:
if: github.actor
runs-on: ubuntu-latest
steps:
- name: leak
env:
X: ${{ secrets.MY_SECRET }}
run: echo "$X"
""",
)
rc, _out, err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 1
assert "secrets.MY_SECRET" in err
def test_bracket_notation_secret_detected(tmp_path, monkeypatch, capsys, script):
# ``${{ secrets['MY_SECRET'] }}`` is a legal alternative to dot
# notation. The checker must detect it (I-CODEX-1 fix).
_write_workflow(
tmp_path,
"bad_bracket.yml",
"""
name: bad-bracket
on:
workflow_dispatch:
jobs:
oops:
runs-on: ubuntu-latest
steps:
- name: leak
env:
X: ${{ secrets['MY_SECRET'] }}
run: echo "$X"
""",
)
rc, _out, err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 1
assert "MY_SECRET" in err
def test_dynamic_bracket_indexing_detected(tmp_path, monkeypatch, capsys, script):
# Dynamic indexing ``${{ secrets[matrix.name] }}`` can't be
# statically resolved but must still trip the gate.
_write_workflow(
tmp_path,
"bad_dynamic.yml",
"""
name: bad-dynamic
on:
workflow_dispatch:
jobs:
oops:
runs-on: ubuntu-latest
steps:
- name: leak
env:
X: ${{ secrets[matrix.name] }}
run: echo "$X"
""",
)
rc, _out, err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 1
assert "secrets.<dynamic>" in err
def test_secrets_inherit_requires_gate(tmp_path, monkeypatch, capsys, script):
# ``secrets: inherit`` on a reusable-workflow call forwards ALL
# caller secrets. Treat as a non-bypassable secret consumer.
_write_workflow(
tmp_path,
"bad_inherit.yml",
"""
name: bad-inherit
on:
workflow_dispatch:
jobs:
call:
uses: ./.github/workflows/reusable.yml
secrets: inherit
""",
)
rc, _out, err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 1
assert "<inherit>" in err
def test_secrets_inherit_with_environment_passes(tmp_path, monkeypatch, capsys, script):
# …but ``secrets: inherit`` paired with an approved environment is
# acceptable (still gated by maintainer approval at the caller).
_write_workflow(
tmp_path,
"ok_inherit_env.yml",
"""
name: ok-inherit-env
on:
workflow_dispatch:
jobs:
call:
uses: ./.github/workflows/reusable.yml
secrets: inherit
environment: protected-readonly
""",
)
rc, _out, _err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 0
def test_typoed_environment_name_does_not_pass(tmp_path, monkeypatch, capsys, script):
# ``environment: protectd-readonly`` is a typo. GitHub would silently
# auto-create that environment with no protection rules at runtime,
# bypassing maintainer approval. The static checker must reject
# unapproved environment names (C-CODEX-2 fix).
_write_workflow(
tmp_path,
"bad_typo_env.yml",
"""
name: bad-typo-env
on:
workflow_dispatch:
jobs:
oops:
runs-on: ubuntu-latest
environment: protectd-readonly
steps:
- name: leak
env:
X: ${{ secrets.MY_SECRET }}
run: echo "$X"
""",
)
rc, _out, err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 1
assert "secrets.MY_SECRET" in err
def test_environment_expression_with_unapproved_literal_does_not_pass(
tmp_path, monkeypatch, capsys, script
):
# Conditional environment that resolves to a non-approved name on
# one branch and empty on the other provides NO gate; reject.
_write_workflow(
tmp_path,
"bad_expr_env.yml",
"""
name: bad-expr-env
on:
workflow_dispatch:
jobs:
oops:
runs-on: ubuntu-latest
environment: ${{ github.event_name == 'workflow_dispatch' && 'staging' || '' }}
steps:
- name: leak
env:
X: ${{ secrets.MY_SECRET }}
run: echo "$X"
""",
)
rc, _out, err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 1
assert "secrets.MY_SECRET" in err
def test_github_actor_positive_pin_passes(tmp_path, monkeypatch, capsys, script):
# ``github.actor == '<name>'`` is the alternate spelling of the
# sender.login pin; both are recognised as trusted job-level guards.
_write_workflow(
tmp_path,
"ok_github_actor.yml",
"""
name: ok-github-actor
on:
workflow_dispatch:
jobs:
fine:
if: github.actor == 'teng-lin'
runs-on: ubuntu-latest
steps:
- name: use
env:
X: ${{ secrets.MY_SECRET }}
run: echo "$X"
""",
)
rc, _out, _err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 0
def test_inline_secret_on_step_header_line_detected(tmp_path, monkeypatch, capsys, script):
# Secret appearing on the same line as the step key (e.g.
# ``- run: echo ${{ secrets.X }}``) must still be detected. An
# earlier version of the checker ``continue``d immediately after
# the step header match and missed this.
_write_workflow(
tmp_path,
"bad_inline_step.yml",
"""
name: bad-inline-step
on:
workflow_dispatch:
jobs:
oops:
runs-on: ubuntu-latest
steps:
- run: echo ${{ secrets.MY_SECRET }}
""",
)
rc, _out, err = _run(script, tmp_path, monkeypatch, capsys)
assert rc == 1
assert "secrets.MY_SECRET" in err
def test_real_repo_workflows_pass(monkeypatch, capsys, script):
"""The real ``.github/workflows`` directory must pass.
This is the load-bearing acceptance check: any new workflow file
landing without a gate will fail this assertion via the CI quality
job. Pinning the dependency here means a refactor of the checker
itself can't silently weaken the real-world coverage.
"""
workflows_dir = REPO_ROOT / ".github" / "workflows"
monkeypatch.setattr(
sys,
"argv",
["check_workflow_secret_gates.py", "--workflow-dir", str(workflows_dir)],
)
rc = script.main()
assert rc == 0, capsys.readouterr().err