docs(skills): source-pack lessons from the Feishu live pass and the N… #773
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [ main, master, develop ] | |
| pull_request: | |
| branches: [ main, master, develop ] | |
| # Cancel previous runs on the same PR/branch | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| CARGO_TERM_COLOR: always | |
| CARGO_INCREMENTAL: 0 | |
| jobs: | |
| docs: | |
| name: Docs | |
| runs-on: ubuntu-latest | |
| env: | |
| RUSTDOCFLAGS: "--cfg docsrs -D warnings" | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install Rust toolchain | |
| # No toolchain argument: rustup installs the version pinned in | |
| # rust-toolchain.toml, keeping CI in lockstep with the Docker build. | |
| run: rustup toolchain install | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Install protoc | |
| run: sudo apt-get install -y protobuf-compiler | |
| - name: Build documentation | |
| run: cargo doc --no-deps -p skardi | |
| test: | |
| name: Test | |
| runs-on: ubuntu-latest | |
| services: | |
| mysql: | |
| image: mysql:8.0 | |
| env: | |
| MYSQL_ROOT_PASSWORD: rootpass | |
| MYSQL_DATABASE: mydb | |
| MYSQL_USER: skardi_user | |
| MYSQL_PASSWORD: skardi_pass | |
| ports: | |
| - 3306:3306 | |
| options: >- | |
| --health-cmd="mysqladmin ping -h 127.0.0.1 -u root -prootpass" | |
| --health-interval=10s | |
| --health-timeout=5s | |
| --health-retries=5 | |
| postgres: | |
| # `pgvector/pgvector:pg16` is the upstream Postgres 16 image with | |
| # the pgvector extension pre-built in. Used so the `docs` table | |
| # can declare an `embedding vector(N)` column and the integration | |
| # tests can round-trip vector cells through the multi-row VALUES | |
| # renderer without a separate extension build step. | |
| image: pgvector/pgvector:pg16 | |
| env: | |
| POSTGRES_DB: mydb | |
| POSTGRES_USER: skardi_user | |
| POSTGRES_PASSWORD: skardi_pass | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd="pg_isready -U skardi_user -d mydb" | |
| --health-interval=10s | |
| --health-timeout=5s | |
| --health-retries=5 | |
| mongo: | |
| image: mongo:7.0 | |
| env: | |
| MONGO_INITDB_ROOT_USERNAME: root | |
| MONGO_INITDB_ROOT_PASSWORD: rootpass | |
| ports: | |
| - 27017:27017 | |
| options: >- | |
| --health-cmd="mongosh --eval 'db.runCommand({ping:1})' --quiet" | |
| --health-interval=10s | |
| --health-timeout=5s | |
| --health-retries=5 | |
| redis: | |
| image: redis:7.4 | |
| ports: | |
| - 6379:6379 | |
| options: >- | |
| --health-cmd="redis-cli ping" | |
| --health-interval=10s | |
| --health-timeout=5s | |
| --health-retries=5 | |
| seekdb: | |
| image: oceanbase/seekdb:latest | |
| env: | |
| OB_TENANT_PASSWORD: skardi_pass | |
| ports: | |
| - 2881:2881 | |
| - 2886:2886 | |
| options: >- | |
| --health-cmd="obclient -h 127.0.0.1 -P 2881 -u root@sys -e 'SELECT 1' || mysql -h 127.0.0.1 -P 2881 -u root@sys -e 'SELECT 1'" | |
| --health-interval=30s | |
| --health-timeout=10s | |
| --health-retries=20 | |
| clickhouse: | |
| image: clickhouse/clickhouse-server:24.8 | |
| env: | |
| CLICKHOUSE_DB: mydb | |
| CLICKHOUSE_USER: skardi_user | |
| CLICKHOUSE_PASSWORD: skardi_pass | |
| ports: | |
| - 8123:8123 | |
| options: >- | |
| --health-cmd="wget --no-verbose --tries=1 --spider http://127.0.0.1:8123/ping || exit 1" | |
| --health-interval=10s | |
| --health-timeout=5s | |
| --health-retries=5 | |
| # DynamoDB Local has no in-image shell tooling for a healthcheck, so | |
| # readiness is polled with the AWS CLI in a dedicated step below. | |
| dynamodb: | |
| image: amazon/dynamodb-local:2.5.2 | |
| ports: | |
| - 8000:8000 | |
| env: | |
| MYSQL_USER: skardi_user | |
| MYSQL_PASSWORD: skardi_pass | |
| PG_USER: skardi_user | |
| PG_PASSWORD: skardi_pass | |
| MONGO_USER: root | |
| MONGO_PASS: rootpass | |
| SEEKDB_USER: "root@sys" | |
| SEEKDB_PASSWORD: "" | |
| INFLUXDB_URL: "http://127.0.0.1:8181" | |
| INFLUXDB_DATABASE: "metrics" | |
| CLICKHOUSE_URL: "http://127.0.0.1:8123" | |
| CLICKHOUSE_DATABASE: "mydb" | |
| CLICKHOUSE_USER: skardi_user | |
| CLICKHOUSE_PASSWORD: skardi_pass | |
| # DynamoDB Local ignores credential values but the AWS SDK requires them | |
| # to be present. The endpoint is supplied via the context/test config. | |
| AWS_ACCESS_KEY_ID: dummy | |
| AWS_SECRET_ACCESS_KEY: dummy | |
| AWS_DEFAULT_REGION: us-east-1 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Install system dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y protobuf-compiler | |
| - name: Install Rust | |
| # Version comes from rust-toolchain.toml. Runners install the pinned | |
| # toolchain with the minimal profile, and a bare `rustup toolchain | |
| # install` ignores CLI --component flags in favor of the toolchain | |
| # file's spec — so components must be added in a second command. | |
| # Kept out of the toolchain file so the Docker build doesn't pull | |
| # them: rustfmt is for `cargo fmt`, llvm-tools-preview for coverage. | |
| run: | | |
| rustup toolchain install | |
| rustup component add rustfmt llvm-tools-preview | |
| - name: Install cargo-llvm-cov | |
| uses: taiki-e/install-action@cargo-llvm-cov | |
| - name: install nextest | |
| uses: taiki-e/install-action@nextest | |
| - name: Free up disk space | |
| # GitHub-hosted runners ship with ~25 GB free on the smaller VM | |
| # pool — not enough for the coverage-instrumented `--all-features` | |
| # link step (LLD crashes with SIGBUS when the build write hits a | |
| # full filesystem). Remove the bulky preinstalled toolchains and | |
| # caches to give the linker headroom. | |
| run: | | |
| sudo rm -rf /usr/share/dotnet | |
| sudo rm -rf /opt/ghc | |
| sudo rm -rf /usr/local/share/boost | |
| sudo rm -rf /usr/local/lib/android | |
| sudo rm -rf /opt/hostedtoolcache/CodeQL | |
| sudo rm -rf /usr/local/share/powershell | |
| sudo rm -rf /usr/local/share/chromium | |
| sudo rm -rf /usr/local/lib/node_modules | |
| sudo rm -rf /opt/microsoft | |
| sudo rm -rf /opt/google | |
| sudo rm -rf "$AGENT_TOOLSDIRECTORY" | |
| sudo docker image prune -af || true | |
| df -h | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| # liteparse-pdfium-sys's build.rs downloads a prebuilt PDFium | |
| # native library to ~/.cache/pdfium-rs (see docs/documents.md) — | |
| # outside the paths rust-cache tracks by default (~/.cargo/*, | |
| # target/). On a cache *hit*, cargo's build-script fingerprinting | |
| # sees the restored target/ output as still valid and skips | |
| # re-running build.rs, so it never re-downloads PDFium; the | |
| # runner then has no libpdfium.so and every documents-feature | |
| # test that touches PDFium panics. Caching this directory too | |
| # keeps the download in sync with the rest of the restored cache. | |
| cache-directories: ~/.cache/pdfium-rs | |
| - name: Check code format | |
| run: cargo fmt --all -- --check | |
| - name: Check the package for errors | |
| run: cargo check --all | |
| - name: Execute rust tests | |
| run: cargo llvm-cov --no-report nextest --all-features | |
| - name: Install integration test dependencies | |
| run: | | |
| wget -qO- https://www.mongodb.org/static/pgp/server-7.0.asc | sudo gpg --dearmor -o /usr/share/keyrings/mongodb-server-7.0.gpg | |
| echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list | |
| sudo apt-get update | |
| sudo apt-get install -y mongodb-mongosh redis-tools | |
| - name: Seed MySQL data | |
| run: | | |
| mysql -h 127.0.0.1 -u skardi_user -pskardi_pass mydb <<'EOF' | |
| CREATE TABLE users ( | |
| id INT PRIMARY KEY AUTO_INCREMENT, | |
| name VARCHAR(100) NOT NULL, | |
| email VARCHAR(100) UNIQUE NOT NULL | |
| ); | |
| CREATE TABLE orders ( | |
| id INT PRIMARY KEY AUTO_INCREMENT, | |
| user_id INT NOT NULL, | |
| product VARCHAR(100) NOT NULL, | |
| amount DECIMAL(10, 2) NOT NULL | |
| ); | |
| CREATE TABLE user_order_stats ( | |
| id INT PRIMARY KEY AUTO_INCREMENT, | |
| user_id INT NOT NULL, | |
| user_name VARCHAR(100) NOT NULL, | |
| user_email VARCHAR(100) NOT NULL, | |
| total_orders INT NOT NULL, | |
| total_spent DECIMAL(10, 2) NOT NULL, | |
| last_order_date VARCHAR(50), | |
| UNIQUE KEY unique_user (user_id) | |
| ); | |
| INSERT INTO users (name, email) VALUES | |
| ('Alice Smith', 'alice@example.com'), | |
| ('Bob Johnson', 'bob@example.com'), | |
| ('Carol Williams', 'carol@example.com'); | |
| INSERT INTO orders (user_id, product, amount) VALUES | |
| (1, 'Laptop', 999.99), | |
| (2, 'Keyboard', 79.99), | |
| (3, 'Monitor', 299.99); | |
| EOF | |
| - name: Seed PostgreSQL data | |
| run: | | |
| PGPASSWORD=skardi_pass psql -h 127.0.0.1 -U skardi_user -d mydb <<'EOF' | |
| CREATE EXTENSION IF NOT EXISTS vector; | |
| CREATE TABLE users ( | |
| id SERIAL PRIMARY KEY, | |
| name VARCHAR(100) NOT NULL, | |
| email VARCHAR(100) UNIQUE NOT NULL | |
| ); | |
| CREATE TABLE orders ( | |
| id SERIAL PRIMARY KEY, | |
| user_id INT NOT NULL, | |
| product VARCHAR(100) NOT NULL, | |
| amount DECIMAL(10, 2) NOT NULL | |
| ); | |
| CREATE TABLE user_order_stats ( | |
| user_id INT PRIMARY KEY, | |
| user_name VARCHAR(100), | |
| user_email VARCHAR(100), | |
| total_orders INT, | |
| total_spent DECIMAL(10, 2), | |
| last_order_date VARCHAR(50) | |
| ); | |
| INSERT INTO users (name, email) VALUES | |
| ('Alice Smith', 'alice@example.com'), | |
| ('Bob Johnson', 'bob@example.com'), | |
| ('Carol Williams', 'carol@example.com'); | |
| INSERT INTO orders (user_id, product, amount) VALUES | |
| (1, 'Laptop', 999.99), | |
| (2, 'Keyboard', 79.99), | |
| (3, 'Monitor', 299.99); | |
| CREATE TABLE articles ( | |
| id SERIAL PRIMARY KEY, | |
| title VARCHAR(200) NOT NULL, | |
| body TEXT NOT NULL, | |
| category VARCHAR(50) NOT NULL | |
| ); | |
| INSERT INTO articles (title, body, category) VALUES | |
| ('Intro to Machine Learning', 'machine learning model training deep neural network supervised algorithms', 'ai'), | |
| ('Natural Language Processing', 'natural language processing text classification sentiment analysis tokenization', 'ai'), | |
| ('Database Query Optimization', 'database query optimization indexing performance tuning relational algebra', 'database'), | |
| ('Deep Learning Advances', 'machine learning classification supervised training model convolutional neural network', 'research'), | |
| ('Neural Network Architectures', 'deep learning neural network convolutional image recognition transformer attention', 'ai'); | |
| -- pgvector-backed `docs` table — exercises the multi-row VALUES | |
| -- renderer's nested-array cell shape (`{"rows": [["a", "b", [v1, | |
| -- v2, v3, v4]], ...]}`) end-to-end. Mirrors the SeekDB `docs` | |
| -- table schema so the same parameter shape works on both. | |
| CREATE TABLE docs ( | |
| id TEXT PRIMARY KEY, | |
| title TEXT NOT NULL, | |
| category TEXT NOT NULL, | |
| embedding vector(4) NOT NULL | |
| ); | |
| EOF | |
| - name: Seed MongoDB data | |
| run: | | |
| mongosh "mongodb://root:rootpass@127.0.0.1:27017/?authSource=admin" <<'EOF' | |
| use mydb | |
| db.createCollection("products", { | |
| validator: { | |
| $jsonSchema: { | |
| bsonType: "object", | |
| required: ["product_id", "name", "price"], | |
| properties: { | |
| product_id: { bsonType: "string" }, | |
| name: { bsonType: "string" }, | |
| category: { bsonType: "string" }, | |
| price: { bsonType: "double" }, | |
| in_stock: { bsonType: "bool" } | |
| } | |
| } | |
| } | |
| }) | |
| db.products.insertMany([ | |
| { _id: "PROD001", product_id: "PROD001", name: "Laptop", category: "Electronics", price: 999.99, in_stock: true }, | |
| { _id: "PROD002", product_id: "PROD002", name: "Keyboard", category: "Electronics", price: 79.99, in_stock: true }, | |
| { _id: "PROD003", product_id: "PROD003", name: "Monitor", category: "Electronics", price: 299.99, in_stock: false }, | |
| { _id: "PROD004", product_id: "PROD004", name: "Mouse", category: "Electronics", price: 29.99, in_stock: true }, | |
| { _id: "PROD005", product_id: "PROD005", name: "Desk Chair", category: "Furniture", price: 199.99, in_stock: true } | |
| ]) | |
| db.createCollection("product_stats", { | |
| validator: { | |
| $jsonSchema: { | |
| bsonType: "object", | |
| required: ["stat_id"], | |
| properties: { | |
| stat_id: { bsonType: "string" }, | |
| category: { bsonType: "string" }, | |
| total_products: { bsonType: "long" }, | |
| total_value: { bsonType: "double" }, | |
| avg_price: { bsonType: "double" } | |
| } | |
| } | |
| } | |
| }) | |
| // ─── Full-text search test collection ───────────────────────── | |
| db.createCollection("dataset_data_texts") | |
| db.dataset_data_texts.createIndex( | |
| { fullTextToken: "text" }, | |
| { default_language: "none" } | |
| ) | |
| db.dataset_data_texts.insertMany([ | |
| { _id: "data001", dataId: "data001", teamId: "team1", datasetId: "ds1", collectionId: "col1", fullTextToken: "machine learning model training deep neural network" }, | |
| { _id: "data002", dataId: "data002", teamId: "team1", datasetId: "ds1", collectionId: "col1", fullTextToken: "natural language processing text classification sentiment" }, | |
| { _id: "data003", dataId: "data003", teamId: "team1", datasetId: "ds1", collectionId: "col2", fullTextToken: "database query optimization indexing performance tuning" }, | |
| { _id: "data004", dataId: "data004", teamId: "team2", datasetId: "ds2", collectionId: "col3", fullTextToken: "machine learning classification supervised training model" }, | |
| { _id: "data005", dataId: "data005", teamId: "team1", datasetId: "ds1", collectionId: "col1", fullTextToken: "deep learning neural network convolutional image recognition" } | |
| ]) | |
| EOF | |
| - name: Seed Redis data | |
| run: | | |
| redis-cli -h 127.0.0.1 <<'EOF' | |
| HSET mydb:products:PROD001 name "Laptop" category "Electronics" price "999.99" in_stock "true" | |
| HSET mydb:products:PROD002 name "Keyboard" category "Electronics" price "79.99" in_stock "true" | |
| HSET mydb:products:PROD003 name "Monitor" category "Electronics" price "299.99" in_stock "false" | |
| HSET mydb:products:PROD004 name "Mouse" category "Electronics" price "29.99" in_stock "true" | |
| HSET mydb:products:PROD005 name "Desk Chair" category "Furniture" price "199.99" in_stock "true" | |
| EOF | |
| - name: Wait for SeekDB to accept connections | |
| run: | | |
| for i in $(seq 1 60); do | |
| if mysql -h 127.0.0.1 -P 2881 -u "root@sys" -e "SELECT 1" >/dev/null 2>&1; then | |
| echo "SeekDB is ready" | |
| break | |
| fi | |
| echo "Waiting for SeekDB... (attempt $i/60)" | |
| sleep 5 | |
| done | |
| - name: Seed SeekDB data | |
| run: | | |
| mysql -h 127.0.0.1 -P 2881 -u "root@sys" <<'EOF' | |
| CREATE DATABASE IF NOT EXISTS mydb; | |
| USE mydb; | |
| CREATE TABLE IF NOT EXISTS users ( | |
| id INT PRIMARY KEY AUTO_INCREMENT, | |
| name VARCHAR(100) NOT NULL, | |
| email VARCHAR(100) UNIQUE NOT NULL | |
| ); | |
| CREATE TABLE IF NOT EXISTS orders ( | |
| id INT PRIMARY KEY AUTO_INCREMENT, | |
| user_id INT NOT NULL, | |
| product VARCHAR(100) NOT NULL, | |
| amount DECIMAL(10, 2) NOT NULL | |
| ); | |
| CREATE TABLE IF NOT EXISTS articles ( | |
| id INT PRIMARY KEY AUTO_INCREMENT, | |
| title VARCHAR(200) NOT NULL, | |
| body TEXT NOT NULL, | |
| category VARCHAR(50) NOT NULL, | |
| FULLTEXT INDEX ft_body (body) WITH PARSER IK | |
| ); | |
| CREATE TABLE IF NOT EXISTS docs ( | |
| id INT PRIMARY KEY AUTO_INCREMENT, | |
| title VARCHAR(200) NOT NULL, | |
| category VARCHAR(50) NOT NULL, | |
| embedding VECTOR(4), | |
| VECTOR INDEX idx_embedding (embedding) WITH (TYPE = HNSW, DISTANCE = L2) | |
| ); | |
| -- Inner-product variant to verify NEGATIVE_INNER_PRODUCT() + HNSW | |
| -- actually work end-to-end; L2/COSINE are exercised via the docs table. | |
| CREATE TABLE IF NOT EXISTS docs_ip ( | |
| id INT PRIMARY KEY AUTO_INCREMENT, | |
| title VARCHAR(200) NOT NULL, | |
| embedding VECTOR(4), | |
| VECTOR INDEX idx_embedding_ip (embedding) WITH (TYPE = HNSW, DISTANCE = INNER_PRODUCT) | |
| ); | |
| INSERT INTO users (name, email) VALUES | |
| ('Alice Smith', 'alice@example.com'), | |
| ('Bob Johnson', 'bob@example.com'), | |
| ('Carol Williams', 'carol@example.com'); | |
| INSERT INTO orders (user_id, product, amount) VALUES | |
| (1, 'Laptop', 999.99), | |
| (2, 'Keyboard', 79.99), | |
| (3, 'Monitor', 299.99); | |
| INSERT INTO articles (title, body, category) VALUES | |
| ('Intro to Machine Learning', 'machine learning model training deep neural network supervised algorithms', 'ai'), | |
| ('Natural Language Processing', 'natural language processing text classification sentiment analysis', 'ai'), | |
| ('Database Query Optimization', 'database query optimization indexing performance tuning relational', 'database'), | |
| ('Deep Learning Advances', 'machine learning classification supervised training model convolutional neural', 'research'), | |
| ('Neural Network Architectures', 'deep learning neural network convolutional image recognition transformer', 'ai'); | |
| INSERT INTO docs (title, category, embedding) VALUES | |
| ('doc-a', 'electronics', '[1.0, 0.0, 0.0, 0.0]'), | |
| ('doc-b', 'electronics', '[0.0, 1.0, 0.0, 0.0]'), | |
| ('doc-c', 'books', '[0.0, 0.0, 1.0, 0.0]'), | |
| ('doc-d', 'electronics', '[1.0, 1.0, 0.0, 0.0]'), | |
| ('doc-e', 'books', '[0.5, 0.5, 0.5, 0.5]'); | |
| INSERT INTO docs_ip (title, embedding) VALUES | |
| ('ip-a', '[1.0, 0.0, 0.0, 0.0]'), | |
| ('ip-b', '[0.0, 1.0, 0.0, 0.0]'), | |
| ('ip-c', '[1.0, 1.0, 0.0, 0.0]'), | |
| ('ip-d', '[0.5, 0.5, 0.5, 0.5]'); | |
| EOF | |
| - name: Start and seed InfluxDB 3 | |
| # InfluxDB 3 Core needs `influxdb3 serve <args>`, which a GitHub | |
| # Actions service container can't express (no command override), so | |
| # run it as a plain container. `--object-store memory` keeps it | |
| # ephemeral and disk-free; `--without-auth` skips token management for | |
| # the test fixture. | |
| run: | | |
| docker run -d --name influxdb3 -p 8181:8181 \ | |
| influxdb:3-core influxdb3 serve \ | |
| --node-id ci-node0 \ | |
| --object-store memory \ | |
| --without-auth | |
| echo "Waiting for InfluxDB 3 to become healthy..." | |
| for i in $(seq 1 60); do | |
| if curl -fsS http://127.0.0.1:8181/health >/dev/null 2>&1; then | |
| echo "InfluxDB 3 is ready" | |
| break | |
| fi | |
| echo "Waiting for InfluxDB 3... (attempt $i/60)" | |
| sleep 2 | |
| done | |
| curl -fsS -XPOST "http://127.0.0.1:8181/api/v3/configure/database" \ | |
| -H "Content-Type: application/json" -d '{"db": "metrics"}' | |
| curl -fsS "http://127.0.0.1:8181/api/v3/write_lp?db=metrics&precision=second" \ | |
| --data-binary @- <<'EOF' | |
| cpu,host=host1,region=us-west usage_user=12.5,usage_system=3.2 1700000000 | |
| cpu,host=host1,region=us-west usage_user=64.1,usage_system=9.8 1700000060 | |
| cpu,host=host2,region=us-west usage_user=41.0,usage_system=6.0 1700000000 | |
| cpu,host=host2,region=us-west usage_user=88.7,usage_system=12.3 1700000060 | |
| cpu,host=host3,region=us-east usage_user=22.4,usage_system=4.1 1700000000 | |
| mem,host=host1,region=us-west used_percent=48.2 1700000000 | |
| mem,host=host2,region=us-west used_percent=73.9 1700000000 | |
| mem,host=host3,region=us-east used_percent=31.5 1700000000 | |
| EOF | |
| - name: Seed ClickHouse data | |
| # The HTTP interface accepts one statement per request, so the seed | |
| # script is split on blank lines and posted statement by statement. | |
| run: | | |
| seed() { | |
| curl -fsS "http://127.0.0.1:8123/?user=skardi_user&password=skardi_pass" \ | |
| --data-binary "$1" > /dev/null | |
| } | |
| seed "CREATE TABLE mydb.users ( | |
| id UInt32, | |
| name String, | |
| email String | |
| ) ENGINE = MergeTree ORDER BY id" | |
| seed "INSERT INTO mydb.users VALUES | |
| (1, 'Alice Smith', 'alice@example.com'), | |
| (2, 'Bob Johnson', 'bob@example.com'), | |
| (3, 'Carol Williams', 'carol@example.com')" | |
| seed "CREATE TABLE mydb.orders ( | |
| id UInt32, | |
| user_id UInt32, | |
| product String, | |
| amount Float64 | |
| ) ENGINE = MergeTree ORDER BY id" | |
| seed "INSERT INTO mydb.orders VALUES | |
| (1, 1, 'Laptop', 999.99), | |
| (2, 2, 'Keyboard', 79.99), | |
| (3, 3, 'Monitor', 299.99)" | |
| seed "CREATE TABLE mydb.products ( | |
| product_id String, | |
| name String, | |
| category Nullable(String), | |
| price Float64, | |
| in_stock Bool | |
| ) ENGINE = MergeTree ORDER BY product_id" | |
| # PROD005 has a NULL category on purpose — exercises NULL handling. | |
| seed "INSERT INTO mydb.products VALUES | |
| ('PROD001', 'Laptop', 'Electronics', 999.99, true), | |
| ('PROD002', 'Keyboard', 'Electronics', 79.99, true), | |
| ('PROD003', 'Monitor', 'Electronics', 299.99, false), | |
| ('PROD004', 'Mouse', 'Electronics', 29.99, true), | |
| ('PROD005', 'Desk Chair', NULL, 199.99, true)" | |
| # Deliberately left empty — exercises schema inference on empty tables. | |
| seed "CREATE TABLE mydb.empty_metrics ( | |
| ts DateTime, | |
| value Float64 | |
| ) ENGINE = MergeTree ORDER BY ts" | |
| - name: Seed DynamoDB data | |
| env: | |
| EP: http://127.0.0.1:8000 | |
| run: | | |
| # Wait for DynamoDB Local to accept connections. | |
| for i in $(seq 1 30); do | |
| if aws dynamodb list-tables --endpoint-url "$EP" >/dev/null 2>&1; then | |
| echo "DynamoDB Local is ready" | |
| break | |
| fi | |
| echo "Waiting for DynamoDB Local... (attempt $i/30)" | |
| sleep 2 | |
| done | |
| aws dynamodb create-table --endpoint-url "$EP" \ | |
| --table-name products \ | |
| --attribute-definitions AttributeName=product_id,AttributeType=S \ | |
| --key-schema AttributeName=product_id,KeyType=HASH \ | |
| --billing-mode PAY_PER_REQUEST | |
| aws dynamodb wait table-exists --endpoint-url "$EP" --table-name products | |
| put() { aws dynamodb put-item --endpoint-url "$EP" --table-name products --item "$1"; } | |
| put '{"product_id":{"S":"PROD001"},"name":{"S":"Laptop"},"category":{"S":"Electronics"},"price":{"N":"999.99"},"in_stock":{"BOOL":true}}' | |
| put '{"product_id":{"S":"PROD002"},"name":{"S":"Keyboard"},"category":{"S":"Electronics"},"price":{"N":"79.99"},"in_stock":{"BOOL":true}}' | |
| put '{"product_id":{"S":"PROD003"},"name":{"S":"Monitor"},"category":{"S":"Electronics"},"price":{"N":"299.99"},"in_stock":{"BOOL":false}}' | |
| put '{"product_id":{"S":"PROD004"},"name":{"S":"Mouse"},"category":{"S":"Electronics"},"price":{"N":"29.99"},"in_stock":{"BOOL":true}}' | |
| # NULL-bearing row: no `category` attribute, exercising NULL handling. | |
| put '{"product_id":{"S":"PROD005"},"name":{"S":"Desk Chair"},"price":{"N":"199.99"},"in_stock":{"BOOL":true}}' | |
| - name: Start skardi-server for CLI e2e smoke tests | |
| run: | | |
| # The CLI's #[ignore] e2e_smoke tests are integration tests whose | |
| # backing service is skardi-server itself; they hit the default | |
| # http://127.0.0.1:8080 (no ctx/pipelines needed). The nextest step | |
| # above already compiled the server binary into the llvm-cov | |
| # target dir, so this starts without rebuilding. | |
| ./target/llvm-cov-target/debug/skardi-server --port 8080 & | |
| for i in $(seq 1 30); do | |
| curl -sf http://127.0.0.1:8080/health >/dev/null && exit 0 | |
| sleep 1 | |
| done | |
| echo "skardi-server failed to become healthy" >&2 | |
| exit 1 | |
| - name: Execute Integration tests | |
| run: cargo llvm-cov --no-report nextest --all-features -- --ignored | |
| # MinIO gives the `documents` connector's object-store path real S3 | |
| # coverage without needing an AWS account or credentials in CI. Started | |
| # with `docker run` rather than a `services:` entry because service | |
| # containers cannot override the image command, and the MinIO image needs | |
| # `server /data`. | |
| - name: Start MinIO for documents S3 tests | |
| run: | | |
| docker run -d --name skardi-minio \ | |
| -p 127.0.0.1:9000:9000 \ | |
| -e MINIO_ROOT_USER=skardiminio \ | |
| -e MINIO_ROOT_PASSWORD=skardiminio123 \ | |
| quay.io/minio/minio server /data | |
| # The image has no shell tooling for a container healthcheck, so poll | |
| # readiness from the runner (same approach as DynamoDB Local above). | |
| for i in $(seq 1 30); do | |
| if curl -fsS http://127.0.0.1:9000/minio/health/live >/dev/null 2>&1; then | |
| echo "minio ready after ${i}s" | |
| break | |
| fi | |
| if [ "$i" = "30" ]; then | |
| echo "minio failed to become ready" | |
| docker logs skardi-minio | |
| exit 1 | |
| fi | |
| sleep 1 | |
| done | |
| AWS_ACCESS_KEY_ID=skardiminio AWS_SECRET_ACCESS_KEY=skardiminio123 \ | |
| aws --endpoint-url http://127.0.0.1:9000 s3 mb s3://skardi-ci-documents | |
| # These are `#[ignore]`d and skip themselves unless DOCUMENTS_S3_LIVE=1, so | |
| # the step above's credentials are what actually arms them. Set at step | |
| # scope, not job scope: the job-level AWS_* values are the `dummy` | |
| # placeholders DynamoDB Local expects, and MinIO rejects a root password | |
| # shorter than 8 characters. | |
| - name: Execute documents S3 live tests against MinIO | |
| env: | |
| DOCUMENTS_S3_LIVE: "1" | |
| DOCUMENTS_S3_BUCKET: skardi-ci-documents | |
| # `AmazonS3Builder::from_env()` honours both of these, so the real S3 | |
| # HTTP client path is exercised, not a stub. | |
| AWS_ENDPOINT: http://127.0.0.1:9000 | |
| AWS_ALLOW_HTTP: "true" | |
| AWS_REGION: us-east-1 | |
| AWS_ACCESS_KEY_ID: skardiminio | |
| AWS_SECRET_ACCESS_KEY: skardiminio123 | |
| # `--no-tests=fail` is explicit rather than relying on the current | |
| # default: if these tests are ever renamed out of the filter's reach, the | |
| # step must fail loudly instead of reporting success having run nothing. | |
| run: | | |
| cargo llvm-cov --no-report nextest --all-features \ | |
| -E 'test(/live_s3_/)' --no-tests=fail -- --ignored | |
| - name: Stop MinIO | |
| if: always() | |
| run: docker rm -f skardi-minio || true | |
| - name: Generate coverage report (lcov) | |
| run: cargo llvm-cov report --lcov --output-path lcov.info | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v5 | |
| with: | |
| files: lcov.info | |
| fail_ci_if_error: false | |
| token: ${{ secrets.CODECOV_TOKEN }} |