Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,27 @@ jobs:
release-please:
runs-on: ubuntu-24.04
steps:
# The organization's delivery App opens and updates the release pull request, so GitHub
# emits its pull request events and the organization auto-merge workflow treats it as its own.
# Without the App's variable, Release Please keeps github.token.
- name: Create delivery token
id: delivery-token
if: vars.CHIO_DELIVERY_APP_ID != ''
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.CHIO_DELIVERY_APP_ID }}
private-key: ${{ secrets.CHIO_DELIVERY_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: ${{ github.event.repository.name }}
permission-actions: write
permission-contents: write
permission-issues: write
permission-pull-requests: write

- uses: googleapis/release-please-action@v4
id: release
with:
token: ${{ steps.delivery-token.outputs.token || github.token }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json

Expand All @@ -37,9 +55,9 @@ jobs:
gh workflow run publish.yml --repo "${GITHUB_REPOSITORY}" --ref main \
-f "tag=${RELEASE_TAG}"

# A pull request this workflow opens starts no pull_request runs, so the release pull
# request's required checks are dispatched on its branch: CI replays every recorded case
# before the release, alongside the distribution build.
# The release pull request's required checks are dispatched on its branch, since a pull request
# github.token opens starts no pull_request runs: CI replays every recorded case before the
# release, alongside the distribution build.
- name: Check the release pull request and build its distributions
if: steps.release.outputs.prs_created == 'true'
env:
Expand Down
16 changes: 15 additions & 1 deletion scripts/clutter_guard/_helpers/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
CLASSES_NOT_COUNTED_IN_RECORD_DATA,
CONTENT_PATTERNS,
ENVIRONMENT_VARIABLE_PROSE_PATTERN,
PULL_REQUEST_TRIGGER_PATTERN,
RECORD_DATA_FIELD_PATTERN,
SECRET_PART_PAIRS,
SECRET_PART_SEARCH_PATTERN,
Expand All @@ -23,6 +24,7 @@
SNIPPET_LIMIT,
VARIABLE_NAME_CHARACTERS,
VARIABLE_NAME_PATTERN,
WORKFLOW_SECRET_NAMES,
)
from scripts.clutter_guard.models import Finding

Expand All @@ -43,7 +45,11 @@ def text_findings(*, path: str, text: str) -> list[Finding]:
spans: list[tuple[str, int, int]] = []
for clutter_class, pattern in CONTENT_PATTERNS:
spans.extend(_pattern_spans(text=text, clutter_class=clutter_class, pattern=pattern))
spans.extend((SECRET_VARIABLE_NAME, *span) for span in _secret_variables(text))
spans.extend(
(SECRET_VARIABLE_NAME, *span)
for span in _secret_variables(text)
if not _workflow_secret(path=path, name=text[span[0] : span[1]], text=text)
)
found: dict[tuple[int, str], Finding] = {}
line: int = 1
counted_to: int = 0
Expand Down Expand Up @@ -125,6 +131,14 @@ def _is_secret_variable(*, text: str, start: int, end: int) -> bool:
)


def _workflow_secret(*, path: str, name: str, text: str) -> bool:
"""Whether `name` is a secret only `path` may read, and `path` has no pull request trigger."""
return (
WORKFLOW_SECRET_NAMES.get(name) == path
and re.search(PULL_REQUEST_TRIGGER_PATTERN, text) is None
)


def _snippet(*, text: str, start: int, end: int) -> str:
line_start: int = text.rfind("\n", 0, start) + 1
line_end: int = text.find("\n", end)
Expand Down
7 changes: 7 additions & 0 deletions scripts/clutter_guard/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@
AFTER_ASSIGNMENT_PATTERN: str = r"^(?:=|\s*[?:+]=|:\s*[\"']?\$)"
ENVIRONMENT_VARIABLE_PROSE_PATTERN: str = r"(?i)\benv(?:ironment)?[ _-]?var(?:iable)?s?\b"
ALLOWED_SECRET_NAMES: frozenset[str] = frozenset({"GITHUB_TOKEN", "GH_TOKEN"})
WORKFLOW_SECRET_NAMES: dict[str, str] = {
"CHIO_DELIVERY_PRIVATE_KEY": ".github/workflows/release-please.yml",
}
PULL_REQUEST_TRIGGER_PATTERN: str = (
r"(?m)^(?:on:\s*(?:\[[^\]\n]*\bpull_request|pull_request)"
r"|\s+(?:-\s+)?pull_request(?:_target)?\s*(?::|$))"
)

BINARY_PROBE_BYTES: int = 8192
SNIPPET_CONTEXT: int = 30
Expand Down
43 changes: 43 additions & 0 deletions tests/unit/scripts/clutter_guard/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
_HOME: str = "/" + "home/alice/"
_SECRET_NAME: str = "SERVICE_" + "TOKEN"
_GITHUB_SECRET_NAME: str = "GITHUB_" + "TOKEN"
_DELIVERY_SECRET_NAME: str = "CHIO_DELIVERY_" + "PRIVATE_KEY"
_REPOSITORY_GITIGNORE: Path = Path(__file__).resolve().parents[4] / ".gitignore"
_IGNORED_CLUTTER: tuple[str, ...] = (
"AGENTS.md",
Expand Down Expand Up @@ -229,6 +230,48 @@ def add_github_token(repository: Path) -> None:
)


def _delivery_workflow(triggers: str) -> str:
"""A workflow with `triggers` that reads the delivery App key, mentioning pull_request runs."""
return (
f"on:\n{triggers}\n"
"# A pull request it opens starts no pull_request runs.\n"
"jobs:\n release:\n steps:\n - with:\n"
f" private-key: ${{{{ secrets.{_DELIVERY_SECRET_NAME} }}}}\n"
)


def add_release_workflow_delivery_key(repository: Path) -> None:
"""Adds the delivery App key to the release workflow, on push and dispatch only."""
_write(
repository,
".github/workflows/release-please.yml",
_delivery_workflow(" push:\n workflow_dispatch:"),
)


def add_delivery_key_in_another_workflow(repository: Path) -> None:
"""Adds the delivery App key to a workflow other than the release workflow."""
_write(repository, ".github/workflows/ci.yml", _delivery_workflow(" push:"))


def add_delivery_key_with_pull_request_trigger(repository: Path) -> None:
"""Adds the delivery App key to the release workflow, which also runs on pull requests."""
_write(
repository,
".github/workflows/release-please.yml",
_delivery_workflow(" push:\n pull_request:"),
)


def add_delivery_key_with_pull_request_target_trigger(repository: Path) -> None:
"""Adds the delivery App key to the release workflow, which also runs on pull_request_target."""
_write(
repository,
".github/workflows/release-please.yml",
_delivery_workflow(" push:\n pull_request_target:\n types: [opened]"),
)


def add_sql_system_function(repository: Path) -> None:
"""Adds SQL calling a Snowflake system function whose name has a secret-ish part."""
_write(repository, "docs/sql.md", f"SELECT SYSTEM${_SECRET_NAME}();\n")
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/scripts/clutter_guard/test_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
add_case_with_digest_alias,
add_case_with_digest_alias_and_decision,
add_decision,
add_delivery_key_in_another_workflow,
add_delivery_key_with_pull_request_target_trigger,
add_delivery_key_with_pull_request_trigger,
add_github_token,
add_hex_and_recorded_setup,
add_home_path,
Expand All @@ -24,6 +27,7 @@
add_placeholder_paths,
add_product_named_source,
add_release_changelog,
add_release_workflow_delivery_key,
add_review_record,
add_reviewer_word,
add_run_record,
Expand Down Expand Up @@ -203,6 +207,30 @@
expected_return_code=0,
expected_fragment="passed, 0 findings",
),
GuardTestCase(
description="the release workflow reading the delivery App key passes",
change=add_release_workflow_delivery_key,
expected_return_code=0,
expected_fragment="passed, 0 findings",
),
GuardTestCase(
description="the delivery App key in any other workflow is rejected",
change=add_delivery_key_in_another_workflow,
expected_return_code=1,
expected_fragment=".github/workflows/ci.yml:8: secret-variable-name:",
),
GuardTestCase(
description="the delivery App key in a release workflow with a pull_request trigger is rejected",
change=add_delivery_key_with_pull_request_trigger,
expected_return_code=1,
expected_fragment=".github/workflows/release-please.yml:9: secret-variable-name:",
),
GuardTestCase(
description="the delivery App key in a release workflow on pull_request_target is rejected",
change=add_delivery_key_with_pull_request_target_trigger,
expected_return_code=1,
expected_fragment=".github/workflows/release-please.yml:10: secret-variable-name:",
),
GuardTestCase(
description="a SQL system function with a secret-ish part passes",
change=add_sql_system_function,
Expand Down
Loading