+1 (726) 227-2971

CI for LookML: Data Tests, LAMS, and Spectacles in a Git Workflow

LookML is code, and code without tests and automated checks breaks in production. Looker gives you three complementary layers: LookML data tests that assert facts about your data through the model, LAMS (Look At Me Sideways), a linter for LookML style and structural rules, and Spectacles, which validates that every explore actually compiles to SQL your warehouse accepts. This tutorial wires all three into a GitHub Actions workflow and finishes with advanced deploy mode so production only changes when CI is green.

1. LookML data tests

Data tests live in LookML and run against your warehouse through the model. Each test defines a query (an explore_source, like a native derived table) and an assertion over its result.

# tests/orders.test.lkml
test: orders_have_positive_revenue {
  explore_source: orders {
    column: total_revenue { field: orders.total_revenue }
    filters: [orders.created_date: "30 days"]
  }
  assert: revenue_is_positive {
    expression: ${orders.total_revenue} > 0 ;;
  }
}

test: order_ids_are_unique {
  explore_source: orders {
    column: count { field: orders.count }
    column: distinct_ids { field: orders.distinct_order_ids }
  }
  assert: count_matches_distinct {
    expression: ${orders.count} = ${orders.distinct_order_ids} ;;
  }
}

Include the test file in the model (include: "/tests/*.test.lkml"). In the IDE, Run LookML Tests executes them; from the API, run_lookml_test does the same, which is what CI will call. Write tests for the invariants dashboards silently depend on: primary keys are unique (fan-out detection), revenue reconciles to a control total, the most recent partition is not empty after the nightly load.

2. Linting with LAMS

LAMS is Looker's open-source LookML linter. It enforces the rules in Looker's style guide (every view has a primary key, join relationships are declared, no sql_table_name without a schema) and lets you add project-specific rules in a manifest.lkml block.

Install and run it locally against a checked-out project:

npm install -g @looker/look-at-me-sideways
lams --reporting=no --source='**/*.lkml'

Turn on the rules that matter to you in manifest.lkml:

# manifest.lkml
#LAMS
#rule: K1{}      # every view has exactly one primary key
#rule: K3{}      # primary key is named pk1_...
#rule: E2{}      # joins declare relationship and sql_on
#rule: F1{}      # fields only reference their own view or joined views
#rule: T2{}      # derived tables with persistence declare a datagroup

LAMS writes a markdown report and exits non-zero on errors, which is exactly what a CI step wants. It is also a good way to get an inherited project under control: run it once, fix the primary-key and join warnings, and the worst fan-out bugs usually disappear with them.

3. SQL validation with Spectacles

The LookML validator checks syntax and references; it does not know whether ${TABLE}.amount exists in your warehouse or whether a dialect-specific expression is valid. Spectacles does: for every explore, it generates queries through the Looker API and runs them against the warehouse on your branch, reporting fields whose SQL fails. It also runs your LookML data tests and content validation.

pip install spectacles
spectacles sql \
  --base-url "$LOOKERSDK_BASE_URL" \
  --client-id "$LOOKERSDK_CLIENT_ID" \
  --client-secret "$LOOKERSDK_CLIENT_SECRET" \
  --project ecommerce \
  --branch "$GITHUB_HEAD_REF" \
  --explores "ecommerce/*"

spectacles assert --project ecommerce --branch "$GITHUB_HEAD_REF"   # LookML data tests
spectacles content --project ecommerce --branch "$GITHUB_HEAD_REF"  # saved content still valid

Spectacles needs a service user on the instance with develop permission on the project (so it can check out the branch) and access_data. Use a dedicated user and vault the key. On BigQuery the SQL validation runs with LIMIT 0 style queries by default so it costs almost nothing.

4. Putting it in GitHub Actions

A workflow that runs on every pull request to the production branch:

# .github/workflows/lookml-ci.yml
name: LookML CI
on:
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: "20" }
      - run: npm install -g @looker/look-at-me-sideways
      - run: lams --reporting=no --source='**/*.lkml'

  validate:
    runs-on: ubuntu-latest
    needs: lint
    env:
      LOOKERSDK_BASE_URL: ${{ secrets.LOOKERSDK_BASE_URL }}
      LOOKERSDK_CLIENT_ID: ${{ secrets.LOOKERSDK_CLIENT_ID }}
      LOOKERSDK_CLIENT_SECRET: ${{ secrets.LOOKERSDK_CLIENT_SECRET }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.12" }
      - run: pip install spectacles
      - name: SQL validation
        run: spectacles sql --project ecommerce --branch "$GITHUB_HEAD_REF" --explores "ecommerce/*"
      - name: LookML data tests
        run: spectacles assert --project ecommerce --branch "$GITHUB_HEAD_REF"
      - name: Content validation
        run: spectacles content --project ecommerce --branch "$GITHUB_HEAD_REF"

Spectacles reads the LOOKERSDK_* variables, so the flags from section 3 can be omitted in CI. Mark the validate job as a required status check in the repository's branch protection and nobody can merge a broken explore.

5. Deploying only what passed: advanced deploy mode

By default, Looker's production model is whatever is on the production branch's HEAD, updated whenever someone clicks Deploy or a webhook fires. Advanced deploy mode (Project settings > Git production branch) decouples the two: production is pinned to a specific commit and only moves when you tell it to, via the IDE or a webhook call with the commit or branch ref.

The deploy webhook is a plain POST with a secret header:

curl -X POST "https://<instance>/webhooks/projects/ecommerce/deploy/branch/main" \
  -H "X-Looker-Deploy-Secret: $LOOKER_DEPLOY_SECRET"

Add a deploy job to the workflow that runs only on push to main after the same validations pass, and production deploys itself exactly when CI is green:

  deploy:
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    needs: validate
    steps:
      - run: |
          curl -fsS -X POST "${{ secrets.LOOKERSDK_BASE_URL }}/webhooks/projects/ecommerce/deploy/branch/main" \
            -H "X-Looker-Deploy-Secret: ${{ secrets.LOOKER_DEPLOY_SECRET }}"

(Extend on: to include push: branches: [main] for this job.) The webhook secret is generated in the project's settings; rotate it like any other credential.

What this buys you

  • Fan-out and missing-key bugs caught by LAMS before review.
  • Fields with invalid SQL caught by Spectacles before a user finds them.
  • Business invariants enforced by data tests on every change.
  • Production that cannot drift from what CI validated.

It takes about a day to set up on a typical project and it pays for itself the first time a Friday-afternoon deploy would have broken the executive dashboard. We set this up as a standard part of engagements; if you would like it done for your project, talk to us.