Skip to content

feat(ci): add CodeQL reusable workflow and Workflow Sanity check#26

Merged
lml2468 merged 2 commits into
mainfrom
feat/automation-p1-codeql-sanity
May 19, 2026
Merged

feat(ci): add CodeQL reusable workflow and Workflow Sanity check#26
lml2468 merged 2 commits into
mainfrom
feat/automation-p1-codeql-sanity

Conversation

@lml2468
Copy link
Copy Markdown
Contributor

@lml2468 lml2468 commented May 19, 2026

Summary

P1 batch — adds two new reusable workflows to the shared .github repository.


reusable-codeql.yml

Reusable CodeQL security analysis workflow, consumed by all Mininglamp-OSS repos.

Inputs:

Input Type Required Description
language string go or javascript-typescript
build-command string Optional custom build; autobuild used when empty

Permissions: security-events: write, contents: read, actions: read (all others omitted)

Queries: security-extended — covers all CWE-coded security vulnerabilities

Action pins: github/codeql-action@7c1e4cf0 (v4) for all three steps (init/autobuild/analyze)

Trigger pattern in consuming repos:

on:
  push:
    branches: [main]
  pull_request:
    types: [opened, synchronize, reopened, ready_for_review]
  schedule:
    - cron: "0 6 * * *"  # daily full scan
  workflow_dispatch:

workflow-sanity.yml

Validates workflow file quality on every PR or push that touches .github/workflows/** or .github/actions/**.

Jobs:

  1. no-tabs — Python3 scan for tab characters across all *.yml / *.yaml files
  2. actionlint — Static analysis via actionlint v1.7.12 (SHA-256 verified, extracted to RUNNER_TEMP)

actionlint install security:

  • Downloads binary to $RUNNER_TEMP
  • Verifies sha256sum -c before extraction
  • Adds $RUNNER_TEMP to $GITHUB_PATH (no sudo needed)

Must merge before business repos add codeql.yml callers

Depends on nothing; is depended on by the per-repo codeql.yml PRs coming next.

…batch)

reusable-codeql.yml:
- workflow_call with language (go | javascript-typescript) and optional build-command inputs
- Minimal permissions: security-events:write + contents:read + actions:read
- github/codeql-action pinned to SHA (v4: 7c1e4cf0...)
- queries: security-extended for comprehensive coverage
- autobuild or custom build depending on input

workflow-sanity.yml:
- Triggers on PR / push to main when .github/workflows/** or .github/actions/** change
- no-tabs job: Python3 scan across *.yml and *.yaml
- actionlint job: v1.7.12, SHA-256 verified, extracted to RUNNER_TEMP + GITHUB_PATH
- actionlint runs without path args (auto-discovers all workflow files)
yujiawei
yujiawei previously approved these changes May 19, 2026
Copy link
Copy Markdown

@yujiawei yujiawei left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — PR #26 (.github)

Reviewed bf5c39d1673699116682f2db5fe9609a3788a881 end-to-end: both workflow files, all action SHA pins, and the actionlint integrity check. This is a clean, security-conscious PR. No P0/P1 blockers. A handful of P2 hardening / cosmetic suggestions below, none of which need to block merge.

Verification

Check Result Evidence
actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 resolves to a real v4 tag maps to v4.3.1 (also tagged v4) on actions/checkout
github/codeql-action@7c1e4cf0b20d7c1872b26569c00ba908797a59bf matches v4 tag exact match for the v4 ref on github/codeql-action
actionlint v1.7.12 linux/amd64 SHA-256 matches upstream 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 matches the official actionlint_1.7.12_checksums.txt
Permissions are deny-by-default with per-job least-privilege grants reusable-codeql.yml:15 (permissions: {}) + :19-22 job-scoped grant; workflow-sanity.yml:14-15
Checkout does not persist GITHUB_TOKEN into the workspace persist-credentials: false on every checkout
Reusable workflow’s category parameter is correctly partitioned per language reusable-codeql.yml:52

Strengths worth keeping

  1. All third-party actions are pinned by full 40-char SHA with a trailing # v4 comment for readability. This is the gold standard and the right call for a centrally consumed reusable workflow.
  2. actionlint install is hardened end-to-end: downloads to $RUNNER_TEMP, verifies SHA-256 before extracting, no sudo, no install into a shared path. Adding $RUNNER_TEMP to $GITHUB_PATH keeps the runner clean.
  3. CodeQL uses security-extended, which is the right choice for an org-wide baseline — covers all CWE-coded security queries without the noise of security-and-quality.
  4. permissions: {} at workflow level in reusable-codeql.yml with explicit per-job grants is exactly the pattern GitHub recommends for reusable workflows.
  5. fetch-depth: 2 on the CodeQL checkout is correct for PR-incremental analysis (deeper history is only needed when computing diffs across many commits).
  6. Concurrency group with cancel-in-progress: true on workflow-sanity.yml will prevent stale-PR pile-up — good for a check that fires on every workflow edit.
  7. Trigger model for workflow-sanity.yml uses pull_request (not pull_request_target), so workflow file used at runtime is from the base ref, not PR head. No script-injection vector via PR-modified YAML.

P2 — Suggestions (non-blocking)

1. Defense-in-depth: pass inputs.build-command via env:, not direct interpolation

.github/workflows/reusable-codeql.yml:47-49:

- name: Custom build
  if: inputs.build-command != ''
  run: ${{ inputs.build-command }}

Today the caller is trusted (org-internal repos invoking workflow_call), so this is not exploitable in the current threat model. However, the rest of this PR follows GitHub's script-injection hardening guide religiously, and this one step breaks that pattern. The recommended form is:

- name: Custom build
  if: inputs.build-command != ''
  env:
    BUILD_COMMAND: ${{ inputs.build-command }}
  run: $BUILD_COMMAND

Benefit: avoids YAML-vs-shell quote-escaping bugs (e.g. a build command containing a single quote will currently fail to parse), and means the value passes through the runner's env rather than being template-substituted into the script body — which is GitHub's universally recommended pattern even when the source is trusted. Pure defense-in-depth; not a blocker.

2. no-tabs success message counts the wrong files

.github/workflows/workflow-sanity.yml:56:

print(f"No tabs found in {len(list(pathlib.Path('.github').rglob('*.yml')))} workflow file(s). ✓")

The scan itself (lines 40-49) inspects .github/workflows/** + .github/actions/** for both *.yml AND *.yaml, but the success message counts only *.yml under all of .github/ — which would (a) include unrelated files like dependabot.yml, issue templates, CODEOWNERS-adjacent YAML, etc., and (b) miss any *.yaml files the scanner actually checked. Cosmetic-only — the actual check is correct — but the reported count is misleading.

Suggested fix:

scanned_paths = [
    p
    for root in [pathlib.Path(".github/workflows"), pathlib.Path(".github/actions")]
    if root.exists()
    for p in root.rglob("*")
    if p.suffix in {".yml", ".yaml"}
]
print(f"No tabs found in {len(scanned_paths)} workflow file(s). ✓")

3. Concurrency group prefix duplicates github.workflow

.github/workflows/workflow-sanity.yml:17-19:

group: workflow-sanity-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}

${{ github.workflow }} already expands to "Workflow Sanity", so the literal workflow-sanity- prefix duplicates it. Either drop the prefix or drop the ${{ github.workflow }} expression. Functional today, just noisy in the run logs.

4. Document caller responsibilities (concurrency + trigger surface)

The reusable workflow does not set concurrency (correctly — that's the caller's call), and does not constrain its trigger surface (also correct). It would be helpful to leave a short comment block at the top of reusable-codeql.yml noting:

  • Callers should set their own concurrency group (otherwise multiple PR pushes will queue CodeQL runs, which are slow).
  • Callers should NOT invoke this from pull_request_target against fork PRs without further hardening, because the security-events: write permission combined with PR head checkout would be a meaningful escalation.

This is a docs/maintainability item, not a code defect.

5. Consider adding an explicit language whitelist (nice-to-have)

Today a caller that typos language: golang will fail inside codeql-action/init with a CodeQL error — clear, but mid-job. A two-line if: inputs.language != 'go' && inputs.language != 'javascript-typescript' precondition step would fail faster with a project-specific message. Maintenance burden vs. signal trade-off; reasonable either way.

Notes for the human reviewer (needs-human-review)

This PR was flagged security_sensitive and rightly so — it adds the security-scanning pipeline used by every downstream repo. Items worth a human eyeball:

  • Confirm the action pin SHAs are the ones you actually intend to consume for the next 6+ months. CodeQL action SHA 7c1e4cf0... is the current v4 ref tip; if you want SHA stability across CodeQL action updates, consider whether you're OK auto-rolling forward when v4 advances (you've pinned the SHA, so you won't — that's fine, just be explicit about it in your update cadence).
  • security-events: write is granted on the reusable workflow. When you wire callers in business repos, confirm those callers never expose this on pull_request_target from forks (would let a fork upload arbitrary CodeQL findings to the org's Security tab).
  • The build-command interpolation (P2 #1 above) is the only "would I want a human to look at this twice" finding. Worth a sentence-long acknowledgement either way.

Verdict

Approve. The two suggestions worth acting on before the next batch land are:

  1. Switch build-command to the env-var pattern (5 lines, defense-in-depth).
  2. Fix the misleading no-tabs success-count expression (purely cosmetic, but it'll annoy whoever debugs a future false-positive).

The other items are nits and can be addressed at leisure. Nothing here blocks the downstream codeql.yml caller PRs.

Jerry-Xin
Jerry-Xin previously approved these changes May 19, 2026
Copy link
Copy Markdown

@Jerry-Xin Jerry-Xin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds relevant shared CI workflows for CodeQL and workflow linting; no blocking issues found.

💬 Non-blocking

  • 🔵 Suggestion: .github/workflows/reusable-codeql.yml:47 runs inputs.build-command directly. That is acceptable for a trusted reusable workflow input, but callers should pass only static maintainer-controlled commands, not PR-derived values.
  • 🔵 Suggestion: .github/workflows/reusable-codeql.yml:26 uses a 30-minute timeout. Large Go or JS/TS repositories may exceed this on first CodeQL runs; consider whether 45-60 minutes is safer for org-wide reuse.
  • 🔵 Suggestion: .github/workflows/workflow-sanity.yml:55 reports only .yml count and excludes .yaml, while the scan checks both. This is cosmetic but can make logs misleading.
  • 🔵 Suggestion: .github/workflows/workflow-sanity.yml:80-83 actionlint scans workflows only, not standalone composite action metadata under .github/actions/**. The trigger includes actions, so consider adding separate validation if this repo will host local actions.

✅ Highlights

  • Relevance gate passed: reusable workflows and workflow hygiene checks are in scope for the shared Mininglamp-OSS/.github repository.
  • Good least-privilege posture with top-level permissions: {} and job-scoped permissions.
  • Action pins and checksum verification are strong choices for CI supply-chain hardening.
  • I verified the actionlint tarball checksum locally and ran the matching actionlint version against the repo; it reported no workflow issues.

Copy link
Copy Markdown
Contributor Author

@lml2468 lml2468 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Adds two new workflow files: reusable-codeql.yml (org-wide CodeQL analysis) and workflow-sanity.yml (tab detection + actionlint on workflow changes).

Findings

No new blocking issues beyond what prior reviewers already covered. Verified at head bf5c39d1:

  • Security posture: Top-level permissions: {} with scoped job-level grants, SHA-pinned actions with comment tags, persist-credentials: false on all checkouts, actionlint SHA-256 verification before extraction. Textbook hardening.
  • build-command interpolation (L47): Prior reviews noted this should use env-var pattern for defense-in-depth. I concur — not exploitable today (callers are org-internal workflow_call), but breaks the otherwise-consistent hardening pattern in this PR.
  • Tab-count mismatch (L55): Success message counts *.yml under all of .github/ but the scanner checks *.yml + *.yaml under workflows/ + actions/ only. Cosmetic discrepancy, already noted.
  • CI: add-to-project failure is pre-existing (missing PROJECT_TOKEN). Both new sanity jobs (No tabs, actionlint) pass green.

No findings to add beyond yujiawei and Jerry-Xin's thorough reviews.

Verdict

APPROVED — well-structured security-scanning baseline for downstream repos. (Submitted as COMMENTED due to GitHub self-approval limitation.)

Without on.workflow_call:, external repos cannot call this workflow via
'uses:' — GitHub requires reusable workflows to explicitly declare the
workflow_call trigger.

Keeps existing pull_request / push / workflow_dispatch triggers so the
.github repo's own CI still runs sanity checks on its own workflow files.
@lml2468 lml2468 dismissed stale reviews from Jerry-Xin and yujiawei via 88248be May 19, 2026 11:27
Copy link
Copy Markdown

@yujiawei yujiawei left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — PR #26 (.github)

Summary

Two new reusable workflows added to the org-wide .github repo: reusable-codeql.yml (CodeQL static analysis, called per-repo) and workflow-sanity.yml (no-tabs check + actionlint, runs on workflow file changes and is also exposed as workflow_call).

Supply-chain hygiene is excellent across the diff:

  • actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 ↔ verified to match upstream v4 tag.
  • github/codeql-action/{init,autobuild,analyze}@7c1e4cf0b20d7c1872b26569c00ba908797a59bf ↔ verified to match upstream v4 tag.
  • actionlint_1.7.12_linux_amd64.tar.gz SHA256 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 ↔ matches the official rhysd/actionlint v1.7.12 checksums.txt.
  • permissions: {} at workflow level on reusable-codeql.yml, narrowed to actions:read / contents:read / security-events:write only on the analyze job — minimal scope, deny-by-default.
  • persist-credentials: false on every checkout.
  • runs-on: ubuntu-24.04 (pinned, not ubuntu-latest).
  • timeout-minutes: 30 on the CodeQL job.
  • actionlint installed into $RUNNER_TEMP, no sudo, SHA-verified before extraction.

Verdict: APPROVED. No P0/P1 blockers. A few P2 hardening / cosmetic notes below; none gate merge.


P2 — Hardening / Nits

1. inputs.build-command interpolated directly into run: (reusable-codeql.yml:46)

- name: Custom build
  if: inputs.build-command != ''
  run: ${{ inputs.build-command }}

The build-command string is templated straight into the shell script. For a workflow_call-only entrypoint this is bounded — only sibling org repos can call the reusable workflow, and the caller controls the input — so it's not exploitable by an external PR contributor. Still, the standard hardening pattern is to route untrusted-ish values through env: and reference "$BUILD_CMD" from the script, so a stray quote / newline / backtick in a caller's input cannot break out of the surrounding script. Optional change:

- name: Custom build
  if: inputs.build-command != ''
  env:
    BUILD_CMD: ${{ inputs.build-command }}
  run: bash -c "$BUILD_CMD"

Not a blocker — flagging because this workflow is the security backbone for the whole org, and the rest of the file is otherwise textbook-hardened.

2. fetch-depth: 2 on the CodeQL checkout (reusable-codeql.yml:32)

CodeQL typically uses default fetch-depth: 1 (or 0 for full history when needed for diff-based queries). 2 is unusual and not explained — was this copied from a doc example, or is it intentional for a specific analysis mode? If there's no concrete reason, dropping the override (default 1) reduces clone time on large repos. Non-blocking.

3. no-tabs step success message is inconsistent with what was scanned (workflow-sanity.yml:53)

print(f"No tabs found in {len(list(pathlib.Path('.github').rglob('*.yml')))} workflow file(s). ✓")

The scan only walks .github/workflows/ and .github/actions/ and checks both *.yml and *.yaml. The success message instead counts .github/**/*.yml (so it includes dependabot.yml, issue templates, etc.) and misses *.yaml. Cosmetic — affects only the log line, not correctness — but the number printed will misrepresent how many files were actually checked. Cleanest fix is to tally inside the loop:

scanned = 0
for root in [...]:
    ...
    for path in sorted(itertools.chain(root.rglob("*.yml"), root.rglob("*.yaml"))):
        scanned += 1
        if b"\t" in path.read_bytes():
            bad.append(str(path))
...
print(f"No tabs found in {scanned} workflow file(s). ✓")

4. concurrency.group has a redundant component (workflow-sanity.yml:24)

group: workflow-sanity-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}

Workflow name is already Workflow Sanity, and the static workflow-sanity- prefix makes ${{ github.workflow }} redundant within this file. Harmless, but if you later rename the workflow the group key will drift in a confusing way. Either drop the prefix or drop ${{ github.workflow }}.

5. workflow-sanity.yml double-purposes as both reusable and self-CI

The file declares workflow_call: plus pull_request: / push: / workflow_dispatch: in the same on: block. That's valid — GitHub honors workflow_call only when invoked via uses: from another workflow, and the other triggers fire only inside .github itself — but it's worth a comment in the file explaining the dual role, since callers reading it may be surprised by the unrelated trigger list.

6. $RUNNER_TEMP prepended to GITHUB_PATH (workflow-sanity.yml:81)

The install step does:

echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"

This adds the entire temp directory to PATH for subsequent steps. In this two-step job it's fine — only one binary lands there and the next step exits. If this workflow grows, prefer extracting actionlint to a known subdirectory (e.g. $RUNNER_TEMP/actionlint-bin/) and adding only that to PATH so an unrelated future temp file can never be picked up as a command name. Non-blocking.


Security-sensitive checklist (PR was tagged needs-human-review)

Check Status Notes
All uses: pinned by full commit SHA All 4 action refs are 40-char SHAs with # v4 comments
Pinned SHAs verified against upstream tags Both actions/checkout@v4 and github/codeql-action@v4 confirmed via GitHub API
Downloaded binary integrity verified actionlint SHA256 matches upstream checksums.txt
permissions: minimized Workflow-level {} + job-level minimum on CodeQL; contents: read on sanity
persist-credentials: false on checkout Both workflows
No pull_request_target / forked-PR write paths Only pull_request (and push/workflow_dispatch)
Shell-injection vectors from inputs ⚠️ P2 build-command flows directly into run: — bounded (workflow_call only) but worth the env-var hardening (see P2-1)
Runner pinned (no ubuntu-latest) Both use ubuntu-24.04
No secret exposure in logs No secrets: referenced; CodeQL inherits GITHUB_TOKEN only

Human-eyes recommendation: spot-check the actionlint SHA256 once more before merge (I verified it against rhysd/actionlint v1.7.12 checksums.txt via the official release page, hash matches). Beyond that, no concerns require a human override.

@lml2468
Copy link
Copy Markdown
Contributor Author

lml2468 commented May 19, 2026

Fix pushed (88248be)

Blocking fixed: added workflow_call: trigger

workflow-sanity.yml was missing on: workflow_call:, which is required for any workflow to be callable via uses:. Added the trigger while preserving existing pull_request, push, and workflow_dispatch triggers — the .github repo's own CI still runs normally.

Copy link
Copy Markdown

@Jerry-Xin Jerry-Xin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR is relevant to this shared .github repository and is ready to merge; I found no blocking issues.

💬 Non-blocking

  • 🟡 Warning: .github/workflows/workflow-sanity.yml:58 reports only *.yml files in the success message, while the scan checks both *.yml and *.yaml. This can make logs misleading; consider counting both extensions.
  • 🔵 Suggestion: .github/workflows/reusable-codeql.yml:6 documents accepted languages, but does not enforce them. That is acceptable because CodeQL will fail on invalid input, but documenting expected caller examples or using a matrix in consuming repos would reduce misuse.

✅ Highlights

  • Least-privilege permissions are used in both workflows.
  • Actions are pinned to full commit SHAs.
  • workflow-sanity.yml verifies the actionlint archive hash before extraction.
  • Local validation: tab scan passed, and actionlint v1.7.12 completed without findings.

@lml2468 lml2468 merged commit ca4b01b into main May 19, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants