From 39530f51b3307d685b5e15331a6c773cfb0a047b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 06:44:13 +0900 Subject: [PATCH 1/3] ci(security): use central dependency review Signed-off-by: Seongho Bae --- .github/workflows/dependency-review.yml | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index f33b5a35..2ffc8706 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -2,6 +2,11 @@ name: dependency-review on: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] + +concurrency: + group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number }} + cancel-in-progress: true permissions: contents: read @@ -9,15 +14,8 @@ permissions: jobs: dependency-review: - name: dependency-review - runs-on: ubuntu-latest - env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - - - name: Dependency review - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 - with: - allow-ghsas: "GHSA-69w3-r845-3855" + if: ${{ github.event.action != 'closed' && github.event.pull_request.draft == false }} + uses: ContextualWisdomLab/.github/.github/workflows/dependency-review.yml@f43dcb884be5a0efc61611b5c8cb83c4c7735995 + with: + fail_on_severity: low + allow_ghsas: "GHSA-69w3-r845-3855" From 230f29ddcde1dd18a0e17e40796e0de69889245e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 06:54:59 +0900 Subject: [PATCH 2/3] test(ci): support reusable workflow jobs Signed-off-by: Seongho Bae --- tests/test_workflow_runtime_env.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_workflow_runtime_env.py b/tests/test_workflow_runtime_env.py index d73660c2..05dc7de7 100644 --- a/tests/test_workflow_runtime_env.py +++ b/tests/test_workflow_runtime_env.py @@ -1,3 +1,4 @@ +import re from pathlib import Path import yaml @@ -15,6 +16,12 @@ def test_each_workflow_job_forces_javascript_actions_to_node24(): for job_name, job_data in data["jobs"].items(): if workflow_path.name in {"scorecards.yml", "gh-pages.yml"}: continue + if "uses" in job_data: + assert re.search(r"@[0-9a-f]{40}$", job_data["uses"]), ( + workflow_path, + job_name, + ) + continue assert job_data["env"]["FORCE_JAVASCRIPT_ACTIONS_TO_NODE24"] is True, ( workflow_path, job_name, From df3943671e4b80eefe095da6997dba2877f0649d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 07:56:07 +0900 Subject: [PATCH 3/3] ci: bound pull request workflow concurrency --- .github/workflows/build-ci-image.yml | 8 ++++- .github/workflows/clusterfuzzlite.yml | 6 ++++ .github/workflows/codeql.yml | 1 - .github/workflows/container-image.yml | 6 ++++ .github/workflows/quality-gate.yml | 6 ++++ .github/workflows/scorecards.yml | 1 - .github/workflows/tests.yml | 8 ++++- tests/test_workflow_security.py | 44 +++++++++++++++++++++++++++ 8 files changed, 76 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-ci-image.yml b/.github/workflows/build-ci-image.yml index bb6cc954..253e4043 100644 --- a/.github/workflows/build-ci-image.yml +++ b/.github/workflows/build-ci-image.yml @@ -7,6 +7,7 @@ on: - 'Dockerfile.test' - '.github/workflows/build-ci-image.yml' pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] paths: - 'Dockerfile.test' - '.github/workflows/build-ci-image.yml' @@ -16,8 +17,13 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: build-and-push: + if: github.event_name != 'pull_request' || (github.event.action != 'closed' && github.event.pull_request.draft == false) runs-on: ubuntu-latest permissions: contents: read @@ -42,7 +48,7 @@ jobs: - name: Lowercase repository owner id: lowercase_owner run: | - echo "owner=${GITHUB_REPOSITORY_OWNER,,}" >> $GITHUB_OUTPUT + echo "owner=${GITHUB_REPOSITORY_OWNER,,}" >> "$GITHUB_OUTPUT" - name: Extract metadata for Docker id: meta diff --git a/.github/workflows/clusterfuzzlite.yml b/.github/workflows/clusterfuzzlite.yml index b70a6263..08e7c1c7 100644 --- a/.github/workflows/clusterfuzzlite.yml +++ b/.github/workflows/clusterfuzzlite.yml @@ -2,13 +2,19 @@ name: clusterfuzzlite on: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] workflow_dispatch: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: fuzz: + if: github.event_name != 'pull_request' || (github.event.action != 'closed' && github.event.pull_request.draft == false) runs-on: ubuntu-latest env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 176559fb..a484ca7b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -3,7 +3,6 @@ name: codeql on: push: branches: [main, develop] - pull_request: schedule: - cron: '43 5 * * 1' diff --git a/.github/workflows/container-image.yml b/.github/workflows/container-image.yml index 4b027fba..37b23cab 100644 --- a/.github/workflows/container-image.yml +++ b/.github/workflows/container-image.yml @@ -2,6 +2,7 @@ name: container-image on: pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] push: tags: - 'v*' @@ -15,8 +16,13 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: image: + if: github.event_name != 'pull_request' || (github.event.action != 'closed' && github.event.pull_request.draft == false) runs-on: ubuntu-latest permissions: contents: read diff --git a/.github/workflows/quality-gate.yml b/.github/workflows/quality-gate.yml index bf3a55a9..32d9a54b 100644 --- a/.github/workflows/quality-gate.yml +++ b/.github/workflows/quality-gate.yml @@ -4,12 +4,18 @@ on: push: branches: [main, develop] pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: quality-gate: + if: github.event_name != 'pull_request' || (github.event.action != 'closed' && github.event.pull_request.draft == false) name: quality-gate runs-on: ubuntu-latest env: diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index c183c23b..914418d7 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -3,7 +3,6 @@ name: scorecards on: push: branches: [develop] - pull_request: schedule: - cron: '31 5 * * 1' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 88574c0f..ae2ee91a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,13 +4,19 @@ on: push: branches: [main, develop] pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] permissions: contents: read packages: read +concurrency: + group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: pytest: + if: github.event_name != 'pull_request' || (github.event.action != 'closed' && github.event.pull_request.draft == false) runs-on: ubuntu-latest env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true @@ -35,4 +41,4 @@ jobs: env: PYTHONWARNINGS: error PYTHONPATH: src - run: uv run pytest --cov=src/newsdom_api --cov-branch --cov-report=term-missing --cov-fail-under=100 \ No newline at end of file + run: uv run pytest --cov=src/newsdom_api --cov-branch --cov-report=term-missing --cov-fail-under=100 diff --git a/tests/test_workflow_security.py b/tests/test_workflow_security.py index db8785d9..ad09543c 100644 --- a/tests/test_workflow_security.py +++ b/tests/test_workflow_security.py @@ -91,6 +91,50 @@ def test_ci_workflows_run_for_all_pull_requests(): ) +def test_pull_request_workflows_cancel_only_superseded_ready_heads(): + for workflow_path in _iter_workflow_paths(): + workflow = yaml.safe_load(workflow_path.read_text(encoding="utf-8")) + triggers = workflow.get("on", workflow.get(True)) + if "pull_request" not in triggers: + continue + + pull_request = triggers["pull_request"] or {} + assert { + "opened", + "synchronize", + "reopened", + "ready_for_review", + "converted_to_draft", + "closed", + } <= set(pull_request["types"]), workflow_path + concurrency = workflow["concurrency"] + assert "${{ github.workflow }}-${{ github.repository }}-" in concurrency[ + "group" + ], workflow_path + assert concurrency["cancel-in-progress"] in { + True, + "${{ github.event_name == 'pull_request' }}", + }, workflow_path + + for job in workflow["jobs"].values(): + condition = str(job.get("if", "")) + if "github.event_name == 'workflow_dispatch'" in condition: + continue + assert "github.event.action != 'closed'" in condition, workflow_path + assert ( + "github.event.pull_request.draft == false" in condition + ), workflow_path + + +def test_central_security_owners_replace_local_pr_triggers(): + for workflow_name in ("codeql.yml", "scorecards.yml"): + workflow = yaml.safe_load( + Path(".github/workflows", workflow_name).read_text(encoding="utf-8") + ) + triggers = workflow.get("on", workflow.get(True)) + assert "pull_request" not in triggers, workflow_name + + def test_docs_workflow_uses_least_privilege_pages_permissions(): text = Path(".github/workflows/gh-pages.yml").read_text(encoding="utf-8") assert "contents: write" not in text