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
45 changes: 45 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Keep the build context tight — Docker uploads the whole working
# directory to the daemon otherwise, which is slow on big repos.

# VCS + tooling
.git
.github
.gitignore
.lycheeignore

# Local builds / artefacts
node_modules
data
target
rust/target
*.log
coverage
.nyc_output

# Docs and large assets we don't need inside the image
docs
research
*.png
*.jpg
*.pdf
SEARCH_README.md

# Non-runtime artefacts
api-patterns.json
limitations-found.json
*.html
*.css
*.jsx

# Tests / scripts that don't ship in the container
js/tests
js/scripts
rust/src
rust/tests
rust/Cargo.*
rust/changelog.d
rust/README.md
rust/rust-toolchain.toml
rust/rustfmt.toml
examples
experiments
138 changes: 122 additions & 16 deletions .github/workflows/js.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
name: JS

# Single, unified pipeline that subsumes the previous test.yml, pages.yml,
# and links.yml workflows. Structured to fail fast: cheap checks
# (syntax-check, unit tests, link audit) gate the slower jobs (e2e against
# a locally-served build, Pages deploy, e2e against the deployed site).
# Single, unified JavaScript pipeline that subsumes the previous test.yml,
# pages.yml, links.yml, and release.yml responsibilities. Structured to fail
# fast: cheap checks (syntax-check, unit tests, link audit) gate the slower jobs
# (e2e against a locally-served build, Pages deploy, e2e against the deployed
# site, npm publish, Docker publish).
#
# Layout adopted from link-foundation/js-ai-driven-development-pipeline-template
# (release.yml), trimmed for this repo:
# * no npm publish path — this is a static site, not a library
# * no changeset PR / manual release jobs
# * no Docker publish job
# (release.yml), kept in this repository as js.yml per PR review:
# * npm publish is gated on package.json version bumps
# * Docker publish targets ghcr.io
# * changeset PR and manual release jobs are deferred
# The template's `detect-changes` matrix is intentionally omitted: every job
# below already either runs in <1 minute or is gated by a `needs:` chain,
# so partial skipping wouldn't meaningfully improve wall time and would
Expand All @@ -22,6 +23,11 @@ on:
pull_request:
types: [opened, synchronize, reopened]
workflow_dispatch:
inputs:
force_publish:
description: 'Publish even if package.json version is unchanged'
type: boolean
default: false

# Cancel in-flight runs for the same ref so the latest commit wins on
# `main` and on PR force-pushes. Matches the template's policy.
Expand Down Expand Up @@ -58,9 +64,11 @@ jobs:
- uses: actions/setup-node@v6
with:
node-version: '20.x'
- name: Install dependencies
run: npm ci
- name: Run unit and integration suites
# `run-unit-tests.mjs` distinguishes gating (zero-dep node:test
# suites: routing.test.mjs, ipa.test.mjs) from informational
# `run-unit-tests.mjs` distinguishes gating node:test suites
# (including dependency-backed config/cache tests) from informational
# (live-Wikidata cache/transformer scripts). Only gating failures
# set a non-zero exit code.
run: node js/scripts/run-unit-tests.mjs
Expand Down Expand Up @@ -116,11 +124,7 @@ jobs:
with:
node-version: '20.x'
- name: Install dependencies
# We don't have a deep dependency tree (only @playwright/test as a
# devDep), so a vanilla `npm install` is fast and produces a clean
# node_modules. `npm ci` would require a committed lockfile that
# matches package.json exactly — overkill for this repo.
run: npm install
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run e2e suite (boots js/scripts/serve-static.mjs)
Expand Down Expand Up @@ -191,7 +195,7 @@ jobs:
with:
node-version: '20.x'
- name: Install dependencies
run: npm install
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Wait for new content to propagate
Expand All @@ -208,3 +212,105 @@ jobs:
name: playwright-report-deployed
path: playwright-report/
retention-days: 7

# === npm + Docker release (main only) =====================================

detect-version-bump:
name: Detect package.json version bump
runs-on: ubuntu-latest
timeout-minutes: 5
needs: [unit-tests, e2e-local, link-check]
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
outputs:
should_publish: ${{ steps.diff.outputs.should_publish }}
version: ${{ steps.diff.outputs.version }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 2
- id: diff
name: Compare versions
run: node js/scripts/check-package-version.mjs
env:
FORCE_PUBLISH: ${{ inputs.force_publish }}

publish-npm:
name: npm publish
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [detect-version-bump, unit-tests]
if: needs.detect-version-bump.outputs.should_publish == 'true'
permissions:
contents: read
id-token: write
environment:
name: npm
url: https://www.npmjs.com/package/human-language
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm ci
- name: npm publish --provenance
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

publish-docker:
name: Docker publish (ghcr.io)
runs-on: ubuntu-latest
timeout-minutes: 25
needs: [detect-version-bump, unit-tests]
if: needs.detect-version-bump.outputs.should_publish == 'true'
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v6
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/human-language
tags: |
type=raw,value=latest
type=raw,value=${{ needs.detect-version-bump.outputs.version }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

create-release:
name: Create GitHub release
runs-on: ubuntu-latest
timeout-minutes: 5
needs: [detect-version-bump, publish-npm, publish-docker]
if: needs.detect-version-bump.outputs.should_publish == 'true'
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- name: Create release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ needs.detect-version-bump.outputs.version }}
run: |
gh release create "v${VERSION}" \
--title "v${VERSION}" \
--generate-notes \
--latest
118 changes: 118 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
name: Rust

# Lint, test, and release pipeline for the Rust crate that lives under
# `rust/`. Mirrors the cadence of `js.yml`: PRs are gated on fmt+clippy+test;
# `main` pushes additionally publish a crates.io release when the version
# in `rust/Cargo.toml` advances.
#
# Adopted from rust-ai-driven-development-pipeline-template, trimmed to
# the surface this repo actually uses.

on:
push:
branches:
- main
paths:
- 'rust/**'
- '.github/workflows/rust.yml'
pull_request:
types: [opened, synchronize, reopened]
paths:
- 'rust/**'
- '.github/workflows/rust.yml'
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref == 'refs/heads/main' }}

permissions:
contents: read

defaults:
run:
working-directory: rust

jobs:
fmt:
name: cargo fmt --check
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- run: cargo fmt --all -- --check

clippy:
name: cargo clippy
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [fmt]
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
with:
workspaces: rust
- run: cargo clippy --all-targets -- -D warnings

test:
name: cargo test
runs-on: ${{ matrix.os }}
timeout-minutes: 15
needs: [fmt]
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: rust
- run: cargo test --all-targets

# === Publish to crates.io on version-advancing pushes to main =============

publish-check:
name: Detect Cargo.toml version bump
runs-on: ubuntu-latest
timeout-minutes: 5
needs: [clippy, test]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
outputs:
should_publish: ${{ steps.diff.outputs.should_publish }}
version: ${{ steps.diff.outputs.version }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 2
- id: diff
name: Compare versions
working-directory: .
run: node rust/scripts/check-cargo-version.mjs

publish:
name: cargo publish
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [publish-check]
if: needs.publish-check.outputs.should_publish == 'true'
environment:
name: crates-io
url: https://crates.io/crates/human-language
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: rust
- name: cargo publish --token $CARGO_REGISTRY_TOKEN
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: cargo publish --locked
51 changes: 51 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# syntax=docker/dockerfile:1.7
#
# Multi-stage build for the `human-language` microservice.
#
# Stage 1 ("deps") installs production npm dependencies into a fresh layer
# so the resulting image only contains what the runtime needs.
#
# Stage 2 ("runtime") starts from a minimal node base image, drops to a
# non-root user, and runs `human-language serve`. Configuration is read
# from environment variables (HUMAN_LANGUAGE_*) — see js/src/config.js.
#
# Build:
# docker build -t human-language:dev .
# Run:
# docker run --rm -p 8080:8080 human-language:dev

ARG NODE_VERSION=22.11.0

FROM node:${NODE_VERSION}-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
# `npm ci --omit=dev` will fail if no lockfile exists, so prefer install.
RUN if [ -f package-lock.json ]; then npm ci --omit=dev; else npm install --omit=dev; fi

FROM node:${NODE_VERSION}-alpine AS runtime
WORKDIR /app

# Drop privileges. The `node:alpine` image already ships a `node` user.
USER node

# Copy only the runtime surface: production deps, source, and metadata.
COPY --chown=node:node --from=deps /app/node_modules ./node_modules
COPY --chown=node:node package.json ./
COPY --chown=node:node js ./js

ENV NODE_ENV=production \
HUMAN_LANGUAGE_HOST=0.0.0.0 \
HUMAN_LANGUAGE_PORT=8080 \
HUMAN_LANGUAGE_CACHE_TYPE=file \
HUMAN_LANGUAGE_CACHE_DIR=/app/data/wikidata-cache

EXPOSE 8080

# A small inline healthcheck so orchestrators can see the server is live
# without depending on `curl` being in the image. We use Node's built-in
# fetch so no extra binaries are needed.
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:'+(process.env.HUMAN_LANGUAGE_PORT||8080)+'/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"

ENTRYPOINT ["node", "js/src/cli.js"]
CMD ["serve"]
Loading
Loading