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
72 changes: 72 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# CodeLens .dockerignore — keep build context lean (Phase 2, issue #54)
# Excludes files that are not needed inside the container image.

# Version control — never needed in image, and .git is large.
.git
.gitignore
.gitattributes

# CI / GitHub metadata
.github/
.gitlab-ci.yml

# Tests — not run inside the container, excluded to shrink context.
# (The maximal image installs pytest as an extra, but tests themselves
# are not copied in. Run tests on the host or in a dedicated CI job.)
tests/
pytest.ini
.pytest_cache/
.coverage
htmlcov/

# Benchmarks + intentional vulnerable fixtures (would pollute scan results
# if accidentally scanned from inside the container).
benchmarks/

# VS Code extension — separate concern, shipped via the VS Code marketplace.
vscode-codelens/

# Python build artifacts
__pycache__/
*.pyc
*.pyo
*.pyd
*.egg-info/
*.egg
build/
dist/
.eggs/

# Virtual environments
.venv/
venv/
env/

# CodeLens own runtime state — must never be baked into the image.
.codelens/
*.db
*.sqlite
*.sqlite3

# Editor / OS noise
.vscode/
.idea/
*.swp
*.swo
.DS_Store
Thumbs.db

# Docs (installed via pip or viewed on GitHub; not needed in image)
docs/
*.md
!README.md

# Docker files themselves — not needed inside the image.
Dockerfile
Dockerfile.maximal
.dockerignore
.docker/

# Logs
*.log
logs/
151 changes: 151 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
name: Docker Build & Publish (GHCR)

# Phase 2 of issue #54 — container image distribution.
#
# Builds two images on every relevant trigger:
# 1. minimal -> ghcr.io/wolfvin/codelens:latest, :v<tag>, :sha-<sha>
# 2. maximal -> ghcr.io/wolfvin/codelens:maximal-latest, :maximal-v<tag>
#
# Both are built multi-arch (linux/amd64 + linux/arm64) via docker buildx +
# QEMU. Images are pushed to GitHub Container Registry (GHCR) using the
# auto-provisioned GITHUB_TOKEN (no extra secret needed).
#
# Triggers:
# - push to main touching Dockerfile / Dockerfile.maximal / .dockerignore /
# the workflow itself -> :latest + :sha-<sha>
# - tag push matching v* (release) -> :v<tag> + :latest
# - workflow_dispatch (manual) -> :latest + :sha-<sha>
#
# The workflow is a no-op on forks (guard at job level) so contributors can
# fork-test without leaking images to the upstream GHCR namespace.

on:
push:
branches:
- main
paths:
- 'Dockerfile'
- 'Dockerfile.maximal'
- '.dockerignore'
- '.github/workflows/docker-publish.yml'
- 'scripts/**'
- 'pyproject.toml'
tags:
- 'v*'
workflow_dispatch:
inputs:
dry_run:
description: 'Build but do not push to GHCR'
required: false
default: 'false'
type: choice
options: ['false', 'true']

permissions:
contents: read
packages: write # needed to push to GHCR (ghcr.io/wolfvin/*)

jobs:
build-and-push:
name: Build & Push ${{ matrix.variant }}
runs-on: ubuntu-latest
if: github.repository == 'Wolfvin/CodeLens'
strategy:
fail-fast: false
matrix:
include:
- variant: minimal
dockerfile: Dockerfile
image_suffix: ''
tag_latest: latest
- variant: maximal
dockerfile: Dockerfile.maximal
image_suffix: -maximal
tag_latest: maximal-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up QEMU (for cross-arch emulation)
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
if: github.event_name != 'workflow_dispatch' || inputs.dry_run != 'true'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata for image tags
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/wolfvin/codelens${{ matrix.image_suffix }}
# Produces:
# - On main push: type=sha,prefix=sha-,format=short + type=raw,value=${{ matrix.tag_latest }}
# - On tag v*: type=semver,pattern=v{{version}} + type=semver,pattern=v{{major}}.{{minor}}
# + type=raw,value=${{ matrix.tag_latest }}
tags: |
type=raw,value=${{ matrix.tag_latest }},enable={{is_default_branch}}
type=sha,prefix=sha-,format=short
type=semver,pattern=v{{version}}
type=semver,pattern=v{{major}}.{{minor}}
labels: |
org.opencontainers.image.title=CodeLens (${{ matrix.variant }})
org.opencontainers.image.source=https://github.com/Wolfvin/CodeLens
org.opencontainers.image.licenses=MIT

- name: Build & push ${{ matrix.variant }} image
id: build
uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'workflow_dispatch' || inputs.dry_run != 'true' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.variant }}
cache-to: type=gha,mode=max,scope=${{ matrix.variant }}
build-args: |
PYTHON_VERSION=3.11
# No provenance file for now — Phase 5 (release signing) will add
# SBOM + SLSA via Cosign. Keeping this job focused on image build.

- name: Smoke-test the built image (minimal only, amd64)
# Quick sanity check that the CLI loads and the command registry
# builds. Skipped for maximal (heavier, slower) and for dry-run
# (image not pushed, but buildx can load locally for smoke test).
if: matrix.variant == 'minimal' && github.event_name != 'pull_request'
run: |
# Build a local single-arch copy for smoke testing (does not push).
docker buildx build \
--platform linux/amd64 \
--load \
-t codelens:smoke \
-f Dockerfile \
.
echo "--- codelens --command-count ---"
docker run --rm codelens:smoke --command-count
echo "--- codelens --help (first 20 lines) ---"
docker run --rm codelens:smoke --help 2>&1 | head -20

- name: Job summary
if: always()
run: |
echo "## Docker Build Summary — ${{ matrix.variant }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY
echo "|---|---|" >> $GITHUB_STEP_SUMMARY
echo "| Variant | \`${{ matrix.variant }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Dockerfile | \`${{ matrix.dockerfile }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Platforms | linux/amd64, linux/arm64 |" >> $GITHUB_STEP_SUMMARY
echo "| Image | \`ghcr.io/wolfvin/codelens${{ matrix.image_suffix }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Tags | \`${{ steps.meta.outputs.tags }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Pushed | ${{ steps.build.outputs.pushed }} |" >> $GITHUB_STEP_SUMMARY
echo "| Digest | \`${{ steps.build.outputs.digest }}\` |" >> $GITHUB_STEP_SUMMARY
102 changes: 102 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# syntax=docker/dockerfile:1.6
# CodeLens — minimal Docker image (Phase 2, issue #54)
#
# @WHO: Dockerfile
# @WHAT: Minimal container image for CodeLens CLI — python:3.11-slim + core deps + tree-sitter grammars
# @PART: distribution
# @ENTRY: codelens (wrapper -> python3 /opt/codelens/scripts/codelens.py)
#
# Build:
# docker build -t codelens:latest .
# docker build -t codelens:maximal -f Dockerfile.maximal .
#
# Run (acceptance criterion from issue #54):
# docker run --rm -v "$(pwd):/workspace" ghcr.io/wolfvin/codelens scan /workspace
#
# Forward-compat note: Phase 1 (PR #144) will add a `codelens` console script via
# pyproject.toml [project.scripts]. Once merged, replace the wrapper at
# /usr/local/bin/codelens with `pip install .` and drop the manual copy step.
# Until then the code uses sys.path-based imports (see pyproject.toml lines 77-79)
# and must be run via `python3 scripts/codelens.py`.

ARG PYTHON_VERSION=3.11

FROM python:${PYTHON_VERSION}-slim AS base

# Label the image with OCI-standard metadata so GHCR / tooling can identify it.
LABEL org.opencontainers.image.title="CodeLens" \
org.opencontainers.image.description="AI-native static code intelligence CLI + MCP server" \
org.opencontainers.image.source="https://github.com/Wolfvin/CodeLens" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.authors="Wolfvin"

# Install runtime deps only. tree-sitter + grammar packages ship manylinux/arm64
# wheels on PyPI, so no build toolchain is required. git is needed by some
# CodeLens commands (ownership/blame, diff-base) and adds ~30MB — acceptable.
# curl is included for HEALTHCHECK debugging and download fallbacks.
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
git \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/*

# Create non-root user (UID/GID 1000) for least-privilege execution.
# The user owns /workspace (the mount point) so scanned files are readable
# and generated .codelens/ artifacts are writable.
RUN groupadd --gid 1000 codelens \
&& useradd --uid 1000 --gid codelens --create-home --shell /bin/bash codelens

# Copy the repository into /opt/codelens. .dockerignore excludes tests,
# .git, benchmarks, and other build-context noise to keep layers small.
COPY --chown=codelens:codelens . /opt/codelens/

# Install Python dependencies. We install the same set that setup.sh installs
# (tree-sitter core + 6 grammars + watchdog) plus PyYAML for the rules engine.
# --no-cache-dir keeps the layer small. We do NOT `pip install .` here because
# the project has no console-script entry point yet (Phase 1 will add one).
RUN pip install --no-cache-dir \
"tree-sitter>=0.21.0" \
tree-sitter-html \
tree-sitter-css \
tree-sitter-javascript \
tree-sitter-typescript \
tree-sitter-rust \
tree-sitter-python \
watchdog \
"PyYAML>=6.0"

# Create a wrapper script so the container exposes a `codelens` command.
# This matches the acceptance criterion `docker run ... codelens scan /workspace`
# and is forward-compatible: when Phase 1 merges, `pip install .` will overwrite
# this file with the real console-script entry point.
RUN printf '#!/bin/sh\nexec python3 /opt/codelens/scripts/codelens.py "$@"\n' \
> /usr/local/bin/codelens \
&& chmod +x /usr/local/bin/codelens

# Default workspace mount point. Users mount their codebase here:
# docker run -v "$(pwd):/workspace" ghcr.io/wolfvin/codelens scan /workspace
RUN mkdir -p /workspace \
&& chown codelens:codelens /workspace

USER codelens
WORKDIR /workspace

# Persist the per-user .codelens/ config dir to a volume so registry/SQLite
# state survives container restarts when the user mounts it.
ENV CODELENS_CONFIG_DIR=/home/codelens/.codelens \
PYTHONUNBUFFERED=1 \
PYTHONUTF8=1 \
PYTHONDONTWRITEBYTECODE=1

# HEALTHCHECK: `--command-count` exercises the full command registry import
# path (loads every command module + tree-sitter grammars), so it verifies
# the runtime is functional — a deeper check than a static `--version` print.
# Interval is 30s with 30s timeout, 3 retries before unhealthy.
HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
CMD codelens --command-count > /dev/null 2>&1 || exit 1

# ENTRYPOINT is the codelens wrapper so `docker run image <args>` works the
# same way `codelens <args>` works on the host. No CMD — the user must supply
# a command (scan, query, dead-code, etc.) plus the workspace path.
ENTRYPOINT ["codelens"]
Loading
Loading