Skip to content

Commit cbdc5cd

Browse files
committed
ci(sql): lint migrations and publish SARIF results
1 parent 9911b62 commit cbdc5cd

4 files changed

Lines changed: 208 additions & 0 deletions

File tree

.github/scripts/sqruff_to_sarif.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""Convert sqruff JSON lint output to SARIF 2.1.0 format."""
2+
3+
import hashlib
4+
import json
5+
import os
6+
from pathlib import Path
7+
from urllib.parse import quote
8+
9+
SARIF_SCHEMA = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json"
10+
SQRUFF_VERSION = "0.38.0"
11+
SEVERITY_MAP = {"Warning": "warning", "Error": "error", "Note": "note"}
12+
13+
14+
def uri(path: str) -> str:
15+
return quote(path, safe="/")
16+
17+
18+
def to_region(r: dict) -> dict:
19+
return {
20+
"startLine": max(r["start"]["line"], 1),
21+
"startColumn": max(r["start"]["character"], 1),
22+
"endLine": max(r["end"]["line"], 1),
23+
"endColumn": max(r["end"]["character"], 1),
24+
}
25+
26+
27+
def fingerprint(rule_id: str, file_uri: str, line: int, message: str) -> str:
28+
return hashlib.sha256(f"{rule_id}:{file_uri}:{line}:{message}".encode()).hexdigest()
29+
30+
31+
def build_rule(rule_id: str) -> dict:
32+
return {
33+
"id": rule_id,
34+
"shortDescription": {"text": f"sqruff rule {rule_id}"},
35+
"helpUri": f"https://docs.sqruff.com/rules/{rule_id.lower()}",
36+
"properties": {"tags": ["sql", "sqruff"]},
37+
}
38+
39+
40+
def build_result(file_path: str, diag: dict) -> dict:
41+
rule_id = diag.get("code") or "unknown"
42+
file_uri = uri(file_path)
43+
line = diag["range"]["start"]["line"]
44+
message = diag["message"]
45+
46+
return {
47+
"ruleId": rule_id,
48+
"level": SEVERITY_MAP.get(diag.get("severity", "Warning"), "warning"),
49+
"message": {"text": message},
50+
"partialFingerprints": {"sqruffFingerprint/v1": fingerprint(rule_id, file_uri, line, message)},
51+
"locations": [{
52+
"physicalLocation": {
53+
"artifactLocation": {"uri": file_uri, "uriBaseId": "%SRCROOT%"},
54+
"region": to_region(diag["range"]),
55+
}
56+
}],
57+
}
58+
59+
60+
def build_sarif(data: dict) -> dict:
61+
pairs = [(fp, diag) for fp, diags in data.items() for diag in diags]
62+
rules = sorted({diag.get("code") for _, diag in pairs if diag.get("code")})
63+
64+
return {
65+
"$schema": SARIF_SCHEMA,
66+
"version": "2.1.0",
67+
"runs": [{
68+
"tool": {
69+
"driver": {
70+
"name": "sqruff",
71+
"informationUri": "https://github.com/quarylabs/sqruff",
72+
"version": SQRUFF_VERSION,
73+
"rules": [build_rule(r) for r in rules],
74+
}
75+
},
76+
"results": [build_result(fp, diag) for fp, diag in pairs],
77+
"artifacts": [
78+
{"location": {"uri": uri(fp), "uriBaseId": "%SRCROOT%"}}
79+
for fp in data
80+
],
81+
}],
82+
}
83+
84+
85+
def main() -> None:
86+
tmp = Path(os.environ["RUNNER_TEMP"])
87+
input_path = tmp / "sqruff-raw.json"
88+
output_path = tmp / "sqruff.sarif"
89+
90+
data = json.loads(input_path.read_text())
91+
output_path.write_text(json.dumps(build_sarif(data), indent=2))
92+
93+
total = sum(len(v) for v in data.values())
94+
print(f"✅ {total} violation(s) across {len(data)} file(s) → {output_path}")
95+
if not total:
96+
print("🎉 No violations — clean SARIF will auto-close stale alerts")
97+
98+
99+
if __name__ == "__main__":
100+
main()
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
---
2+
name: Linter & Formatter
3+
4+
on:
5+
pull_request:
6+
push:
7+
branches:
8+
- main
9+
10+
concurrency:
11+
group: ${{ github.workflow }}-${{ github.event.number || github.ref_name }}
12+
cancel-in-progress: true
13+
14+
jobs:
15+
prettier:
16+
name: Prettier Formatter
17+
runs-on: ubuntu-24.04
18+
permissions:
19+
contents: read
20+
actions: read
21+
security-events: write
22+
steps:
23+
- uses: actions/checkout@v5
24+
- uses: actions/setup-node@v6
25+
with:
26+
node-version: 25.9.0
27+
- name: Init local project
28+
run: npm init -y
29+
- name: Install deps
30+
run: |
31+
npm install --save-dev prettier@3.8.3 @prettier/plugin-xml@3.4.2 prettier-plugin-java@2.8.1
32+
- name: Run Prettier
33+
run: |
34+
npx prettier \
35+
--config .prettierrc.yaml \
36+
--write .
37+
- id: verify_format
38+
name: Verify no changes after formatting
39+
run: |
40+
changed_files="$(git diff --name-only)"
41+
42+
if [[ -z "$changed_files" ]]; then
43+
echo "Code is properly formatted."
44+
exit 0
45+
fi
46+
47+
cat <<EOF
48+
Code is not formatted properly. Please run Prettier locally and commit the changes.
49+
50+
Changed files:
51+
$changed_files
52+
EOF
53+
54+
printf 'changed_files<<EOF\n%s\nEOF\n' "$changed_files" >> "$GITHUB_OUTPUT"
55+
exit 1
56+
- if: failure()
57+
name: Upload formatter results
58+
uses: actions/upload-artifact@v7
59+
with:
60+
name: prettier-results
61+
include-hidden-files: true
62+
path: ${{ steps.verify_format.outputs.changed_files }}
63+
retention-days: 1
64+
65+
yaml-linter:
66+
name: YAML Linter
67+
runs-on: ubuntu-latest
68+
permissions:
69+
contents: read
70+
actions: read
71+
security-events: write
72+
steps:
73+
- uses: actions/checkout@v5
74+
- run: pip install yamllint==1.38.0
75+
- run: yamllint -c .yamllint.yaml --format github --strict .
76+
77+
sqruff:
78+
name: Lint with sqruff
79+
runs-on: ubuntu-24.04
80+
steps:
81+
- uses: actions/checkout@v5
82+
- run: pip install sqruff==0.38.0
83+
- name: Run sqruff lint
84+
run: sqruff lint --format json . > ${{ runner.temp }}/sqruff-raw.json
85+
- name: Convert sqruff JSON to SARIF
86+
if: always()
87+
run: python .github/scripts/sqruff_to_sarif.py
88+
- if: always()
89+
name: Upload SARIF
90+
uses: github/codeql-action/upload-sarif@v4
91+
with:
92+
sarif_file: ${{ runner.temp }}/sqruff.sarif
93+
category: sqruff

.sqruff

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[sqruff]
2+
dialect = postgres
3+
exclude_rules = LT05
4+
rules = all
5+
6+
[sqruff:indentation]
7+
indent_unit = space
8+
tab_space_size = 4

.squawk.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
pg_version = "18.0"
2+
3+
excluded_rules = [
4+
"require-timeout-settings",
5+
]
6+
7+
assume_in_transaction = true

0 commit comments

Comments
 (0)