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
15 changes: 11 additions & 4 deletions .github/workflows/build-static.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ name: build-static
on:
workflow_call: {}

permissions:
contents: read

jobs:
build:
name: static ${{ matrix.arch }}
Expand All @@ -22,25 +25,29 @@ jobs:
arch: aarch64
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

# ~200MB static ECL toolchain, rebuilt only when the version or the
# build script changes. runner.arch is in the key because runner.os
# is "Linux" on both architectures.
- name: Cache static ECL toolchain
uses: actions/cache@v6
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: .cache/ecl-static
key: ecl-static-26.5.5-${{ runner.arch }}-${{ hashFiles('scripts/build-static.sh') }}

- name: Build static binary in Alpine
run: |
mkdir -p .cache/ecl-static
# alpine:3.22 pinned to its manifest-list digest (covers amd64+arm64);
# dependabot cannot see inside run: blocks, so bump it by hand with
# `docker manifest inspect alpine:3.22` (or the registry API) on upgrades.
docker run --rm \
-v "$GITHUB_WORKSPACE:/src" \
-v "$GITHUB_WORKSPACE/.cache/ecl-static:/cache" \
-w /src \
alpine:3.22 sh scripts/build-static.sh
alpine:3.22@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce \
sh scripts/build-static.sh
# the container runs as root; later steps and the cache post-step
# run as the unprivileged runner user
sudo chown -R "$(id -u):$(id -g)" dist bin .cache/ecl-static
Expand All @@ -53,7 +60,7 @@ jobs:
"./dist/clicklisp-linux-${{ matrix.arch }}" version
echo '(select (1))' | "./dist/clicklisp-linux-${{ matrix.arch }}" compile

- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: clicklisp-linux-${{ matrix.arch }}
path: dist/clicklisp-linux-${{ matrix.arch }}
Expand Down
11 changes: 8 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ on:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
# Unit tests + a full binary build with the distro ECL, on both
# architectures.
Expand All @@ -16,7 +19,7 @@ jobs:
runner: [ubuntu-24.04, ubuntu-24.04-arm]
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Install ECL
run: |
sudo apt-get update
Expand All @@ -40,6 +43,7 @@ jobs:
runs-on: ${{ matrix.runner }}
env:
ECL_VERSION: "26.5.5"
ECL_SHA256: "a01a5bcda8c5b73e59dda3494fd13e5fec5db6aa1dad782c3cc3bb57f1633435"
# object-size, shift-base and pointer-overflow fire by design in
# ECL's core (tagged fixnums, stack-allocated frames) -- ~120 known
# reports. Everything else in UBSan plus all of ASan stays on.
Expand All @@ -54,14 +58,14 @@ jobs:
ASAN_OPTIONS: "detect_leaks=0:detect_stack_use_after_return=0:allow_user_segv_handler=1"
UBSAN_OPTIONS: "print_stacktrace=1"
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgmp-dev
- name: Cache sanitized ECL
id: cache-ecl
uses: actions/cache@v6
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: ~/ecl-asan
# bump the -vN suffix when SAN_CFLAGS or configure flags change
Expand All @@ -70,6 +74,7 @@ jobs:
if: steps.cache-ecl.outputs.cache-hit != 'true'
run: |
curl -fsSL "https://ecl.common-lisp.dev/static/files/release/ecl-${ECL_VERSION}.tgz" -o ecl.tgz
echo "${ECL_SHA256} ecl.tgz" | sha256sum -c -
mkdir ecl-src
tar xzf ecl.tgz -C ecl-src --strip-components=1
cd ecl-src
Expand Down
27 changes: 16 additions & 11 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,9 @@ name: pages
on:
push:
branches: [main]
paths: ["site/**", "logo.png", ".github/workflows/pages.yml"]
paths: ["site/**", "logo.png", "examples/**", "src/**", "clicklisp.asd", "Makefile", "build/build.lisp", "version.sexp", "scripts/gen-site-data.sh", ".github/workflows/pages.yml"]
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

# One deploy at a time; let an in-flight production deploy finish.
concurrency:
group: pages
Expand All @@ -19,12 +14,19 @@ concurrency:
jobs:
build:
runs-on: ubuntu-24.04
permissions:
contents: read
pages: write # configure-pages (enablement: true) reads/creates the Pages site via the API
steps:
- uses: actions/checkout@v7
- uses: actions/configure-pages@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6
with:
enablement: true
- uses: actions/setup-node@v7
- name: Install ECL
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends ecl
- name: Build clicklisp and generate demo data
run: make all site-data
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
cache: npm
Expand All @@ -35,16 +37,19 @@ jobs:
cp ../logo.png public/logo.png # keep the site logo in sync with the repo's
npm ci
npm run build
- uses: actions/upload-pages-artifact@v5
- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
with:
path: site/dist

deploy:
needs: build
runs-on: ubuntu-24.04
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v5
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5
13 changes: 7 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ on:
push:
tags: ["v*"]

permissions:
contents: write

jobs:
check-version:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Tag must match version.sexp
run: |
tag="${GITHUB_REF_NAME#v}"
Expand All @@ -28,9 +27,11 @@ jobs:
release:
needs: build
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
path: dist
merge-multiple: true
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ dist/
.claude/CLAUDE.md
site/node_modules/
site/dist/
site/src/generated/
*.tsbuildinfo
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ ECL ?= ecl
BIN := bin/clicklisp
SOURCES := clicklisp.asd version.sexp $(wildcard src/*.lisp)

.PHONY: all test smoke clean
.PHONY: all test smoke site-data clean

all: $(BIN)

Expand All @@ -23,7 +23,13 @@ smoke: $(BIN)
$(BIN) rules sql --all --load examples/analytics/uk-price-paid.lisp
$(BIN) rules sql --all --load examples/analytics/repo-health.lisp
$(BIN) rules sql --all --load examples/analytics/hackernews.lisp
$(BIN) rules sql --all --load examples/analytics/github-events.lisp
$(BIN) rules sql --all --load examples/analytics/repo-health.lisp --load examples/analytics/repo-health-playground.lisp
$(BIN) rules json --all --load examples/rules.lisp | python3 -m json.tool > /dev/null
@echo smoke OK

site-data: $(BIN)
scripts/gen-site-data.sh

clean:
rm -rf bin
40 changes: 37 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ parameters; `CLICKLISP_FORMAT` and `CLICKLISP_PLAY` override the output
format and the endpoint. One statement per invocation (the ClickHouse
HTTP interface is single-statement).

The same queries run as [live demos](https://cl.clickhouse.com/#demos)
in the browser: the site's deploy workflow builds the binary, compiles
the example libraries with `rules json`, and each demo page sends the
compiled SQL to the playground with typed `param_` bindings — no server
of ours in the path.

## The query language

Operators and clause names are matched by symbol *name*, so forms can be
Expand Down Expand Up @@ -168,8 +174,15 @@ Rules are validated (compiled) at load time and rendered on demand:
clicklisp rules --load rules.lisp # list
clicklisp rules sql --load rules.lisp --all # emit SQL for all
clicklisp rules sql --load rules.lisp ssh-bruteforce
clicklisp rules json --load rules.lisp --all # machine-readable JSON
```

`rules json` dumps each rule as JSON — `name`, `description`, `severity`,
`tags`, the extracted `{name:Type}` `params`, compact `sql`, multiline
`sql_pretty`, and the reconstructed `form` — for anything downstream that
wants rules as data; it is what feeds the
[live demos](https://cl.clickhouse.com/#demos) at build time.

See [examples/rules.lisp](examples/rules.lisp).

Exit codes: `0` success, `1` bad input (a query that does not compile),
Expand All @@ -179,7 +192,7 @@ Exit codes: `0` success, `1` bad input (a query that does not compile),

`defquery` is `defrule` minus the detection metadata: named queries for
any domain, served by the same `rules` commands.
[examples/analytics/](examples/analytics/) has three libraries built on
[examples/analytics/](examples/analytics/) has four libraries built on
the playground's example datasets:

- [uk-price-paid.lisp](examples/analytics/uk-price-paid.lisp) — UK
Expand All @@ -189,11 +202,21 @@ the playground's example datasets:
- [repo-health.lisp](examples/analytics/repo-health.lisp) — engineering
analytics (code churn, bus factor, comment-to-code ratio) over
`clickhouse git-import` tables; the whole library is one macro
parameterized by table names, so it runs against *your* repo locally
or the playground's pre-imported ClickHouse and Grafana mirrors.
parameterized by table names, so it runs against *your* repo locally,
and [repo-health-playground.lisp](examples/analytics/repo-health-playground.lisp)
re-invokes it against the playground's pre-imported ClickHouse mirror
(load both files; the second registration wins).
- [hackernews.lisp](examples/analytics/hackernews.lisp) — text search,
array lambdas, CTE composition, and a query that calls back into a
clicklisp executable UDF.
- [github-events.lisp](examples/analytics/github-events.lisp) — a
`defrule` threat pack over 11 billion rows of public GitHub events
(`github.events`): branch-reset storms, mass ref deletion, tag
retargeting, star storms, bulk collaborator adds, commit-hour
anomalies, each tagged with MITRE ATT&CK technique IDs. Two rules
default to historical windows because the dataset's loader stopped
populating `push_size` and tag `CreateEvent`s around October 2025 —
see the file header.

Rule files are full Common Lisp, and queries are data — so a macro can
generate a family of queries with no copy-paste and no templating
Expand Down Expand Up @@ -365,6 +388,15 @@ compiling Lisp at `CREATE FUNCTION` time.
- **release** — pushing a tag `vX.Y.Z` (which must match `version.sexp`)
builds both static binaries and publishes them with SHA256SUMS to a
GitHub release.
- **pages** — builds the binary with the distro ECL, generates the demo
data (`make site-data`), builds the site, and deploys it to
[cl.clickhouse.com](https://cl.clickhouse.com) — so the demos are
compiled by the same commit they document.

Supply-chain posture: every workflow action is pinned to a commit SHA
(dependabot keeps the pins fresh), workflows run with least-privilege
`permissions`, the ECL source tarball is SHA256-verified before it is
built, and the Alpine build image is pinned by digest.

## Layout

Expand All @@ -377,6 +409,8 @@ src/main.lisp CLI entry point
build/build.lisp ECL AOT build (c:build-program; works on static ECL)
scripts/build-static.sh static musl build, run inside alpine:3.22
scripts/play.sh pipe SQL from stdin to the public ClickHouse playground
scripts/gen-site-data.sh compile the example libraries to JSON for the site demos
examples/ detection rules, analytics libraries, UDFs, server config
site/ cl.clickhouse.com — docs plus live demos of the libraries
tests/ zero-dependency harness + suite
```
1 change: 1 addition & 0 deletions clicklisp.asd
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
:serial t
:components ((:file "tests/harness")
(:file "tests/compiler-tests")
(:file "tests/rules-json-tests")
(:file "tests/udf-tests"))
:perform (test-op (o c)
(unless (symbol-call '#:clicklisp/test '#:run-tests)
Expand Down
Loading