feat(ci): add CodeQL reusable workflow and Workflow Sanity check#26
Conversation
…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
left a comment
There was a problem hiding this comment.
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
- All third-party actions are pinned by full 40-char SHA with a trailing
# v4comment for readability. This is the gold standard and the right call for a centrally consumed reusable workflow. - actionlint install is hardened end-to-end: downloads to
$RUNNER_TEMP, verifies SHA-256 before extracting, nosudo, no install into a shared path. Adding$RUNNER_TEMPto$GITHUB_PATHkeeps the runner clean. - CodeQL uses
security-extended, which is the right choice for an org-wide baseline — covers all CWE-coded security queries without the noise ofsecurity-and-quality. permissions: {}at workflow level inreusable-codeql.ymlwith explicit per-job grants is exactly the pattern GitHub recommends for reusable workflows.fetch-depth: 2on the CodeQL checkout is correct for PR-incremental analysis (deeper history is only needed when computing diffs across many commits).- Concurrency group with
cancel-in-progress: trueonworkflow-sanity.ymlwill prevent stale-PR pile-up — good for a check that fires on every workflow edit. - Trigger model for
workflow-sanity.ymlusespull_request(notpull_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_COMMANDBenefit: 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
concurrencygroup (otherwise multiple PR pushes will queue CodeQL runs, which are slow). - Callers should NOT invoke this from
pull_request_targetagainst fork PRs without further hardening, because thesecurity-events: writepermission 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 currentv4ref tip; if you want SHA stability across CodeQL action updates, consider whether you're OK auto-rolling forward whenv4advances (you've pinned the SHA, so you won't — that's fine, just be explicit about it in your update cadence). security-events: writeis granted on the reusable workflow. When you wire callers in business repos, confirm those callers never expose this onpull_request_targetfrom forks (would let a fork upload arbitrary CodeQL findings to the org's Security tab).- The
build-commandinterpolation (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:
- Switch
build-commandto the env-var pattern (5 lines, defense-in-depth). - Fix the misleading
no-tabssuccess-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
left a comment
There was a problem hiding this comment.
Adds relevant shared CI workflows for CodeQL and workflow linting; no blocking issues found.
💬 Non-blocking
- 🔵 Suggestion:
.github/workflows/reusable-codeql.yml:47runsinputs.build-commanddirectly. 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:26uses 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:55reports only.ymlcount and excludes.yaml, while the scan checks both. This is cosmetic but can make logs misleading. - 🔵 Suggestion:
.github/workflows/workflow-sanity.yml:80-83actionlint 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/.githubrepository. - 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.
lml2468
left a comment
There was a problem hiding this comment.
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: falseon all checkouts, actionlint SHA-256 verification before extraction. Textbook hardening. build-commandinterpolation (L47): Prior reviews noted this should use env-var pattern for defense-in-depth. I concur — not exploitable today (callers are org-internalworkflow_call), but breaks the otherwise-consistent hardening pattern in this PR.- Tab-count mismatch (L55): Success message counts
*.ymlunder all of.github/but the scanner checks*.yml+*.yamlunderworkflows/+actions/only. Cosmetic discrepancy, already noted. - CI:
add-to-projectfailure is pre-existing (missingPROJECT_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.
yujiawei
left a comment
There was a problem hiding this comment.
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 upstreamv4tag.github/codeql-action/{init,autobuild,analyze}@7c1e4cf0b20d7c1872b26569c00ba908797a59bf↔ verified to match upstreamv4tag.actionlint_1.7.12_linux_amd64.tar.gzSHA2568aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8↔ matches the officialrhysd/actionlintv1.7.12checksums.txt.permissions: {}at workflow level onreusable-codeql.yml, narrowed toactions:read / contents:read / security-events:writeonly on theanalyzejob — minimal scope, deny-by-default.persist-credentials: falseon every checkout.runs-on: ubuntu-24.04(pinned, notubuntu-latest).timeout-minutes: 30on the CodeQL job.- actionlint installed into
$RUNNER_TEMP, nosudo, 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 | 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.
Fix pushed (88248be)Blocking fixed: added
|
Jerry-Xin
left a comment
There was a problem hiding this comment.
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:58reports only*.ymlfiles in the success message, while the scan checks both*.ymland*.yaml. This can make logs misleading; consider counting both extensions. - 🔵 Suggestion:
.github/workflows/reusable-codeql.yml:6documents 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.ymlverifies the actionlint archive hash before extraction.- Local validation: tab scan passed, and
actionlint v1.7.12completed without findings.
Summary
P1 batch — adds two new reusable workflows to the shared
.githubrepository.reusable-codeql.ymlReusable CodeQL security analysis workflow, consumed by all Mininglamp-OSS repos.
Inputs:
languagegoorjavascript-typescriptbuild-commandPermissions:
security-events: write,contents: read,actions: read(all others omitted)Queries:
security-extended— covers all CWE-coded security vulnerabilitiesAction pins:
github/codeql-action@7c1e4cf0(v4) for all three steps (init/autobuild/analyze)Trigger pattern in consuming repos:
workflow-sanity.ymlValidates workflow file quality on every PR or push that touches
.github/workflows/**or.github/actions/**.Jobs:
no-tabs— Python3 scan for tab characters across all*.yml/*.yamlfilesactionlint— Static analysis via actionlint v1.7.12 (SHA-256 verified, extracted toRUNNER_TEMP)actionlint install security:
$RUNNER_TEMPsha256sum -cbefore extraction$RUNNER_TEMPto$GITHUB_PATH(nosudoneeded)Must merge before business repos add
codeql.ymlcallersDepends on nothing; is depended on by the per-repo
codeql.ymlPRs coming next.