diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cde2426..9066a50 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,11 +38,16 @@ jobs: type=semver,pattern={{major}}.{{minor}} type=raw,value=latest + - name: Extract version from tag + id: version + run: echo "value=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + - uses: docker/build-push-action@v6 with: context: . push: true platforms: linux/amd64,linux/arm64 + build-args: APP_VERSION=${{ steps.version.outputs.value }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d913ad..6bebfa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,38 @@ All notable changes to Docling Studio will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/). +## [Unreleased] + +### Added + +- Chunking support: domain objects, persistence, API endpoints, and frontend Prepare mode +- Chunk-to-bbox hover highlighting in Prepare mode +- Page filtering and collapsible config in Prepare mode +- Feature flipping mechanism +- Reusable pagination composable and PaginationBar component +- Version display in sidebar, settings page, and health endpoint + +### Fixed + +- Feature flag health check blocked by CORS +- Zombie jobs and unprotected JSON parse +- Upload error not displayed in DocumentUpload component +- Serve API contract: send `to_formats` as repeated form fields +- Audit findings: security, robustness, dead code, domain-infra violation + +### Changed + +- Refactored backend to hexagonal architecture for converter extensibility +- Added ServeConverter adapter for remote Docling Serve integration +- Moved `@vitest/mocker` from dependencies to devDependencies + +## [0.2.0] - 2025-05-14 + +### Added + +- Multi-arch Docker image release pipeline (GitHub Actions) +- Docker image published to `ghcr.io/scub-france/Docling-Studio` + ## [0.1.0] - 2025-01-01 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5010328..bea8895 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -80,6 +80,74 @@ All tests must pass before submitting a PR. 4. Describe **what** changed and **why** in the PR description 5. Ensure CI passes (tests + build) +## Branching Strategy + +We follow a simplified Git Flow: + +| Branch | Purpose | +|--------|---------| +| `main` | Always stable — latest release merged back | +| `release/X.Y.Z` | Release preparation (freeze, bugfixes, changelog) | +| `feature/*` | New features — PR to `main` | +| `fix/*` | Bug fixes — PR to `main` (or `release/*` for pre-release fixes) | +| `hotfix/X.Y.Z` | Urgent fix on a released version — PR to `main` | + +Rules: +- All PRs target `main` (never stack branches on other feature branches) +- `release/*` branches are created from `main` when preparing a release +- `hotfix/*` branches are created from the release tag + +## Versioning + +We use [Semantic Versioning](https://semver.org/): `MAJOR.MINOR.PATCH`. + +- **Source of truth**: the git tag (`vX.Y.Z`) +- `package.json` version should match the current release branch +- The build injects the version automatically (Vite `__APP_VERSION__` for frontend, `APP_VERSION` env var for backend) + +## Release Process + +1. **Create the release branch** from `main`: + ```bash + git checkout main && git pull + git checkout -b release/X.Y.Z + ``` + +2. **On the release branch**, only: + - Bug fixes + - Move `[Unreleased]` to `[X.Y.Z] - YYYY-MM-DD` in `CHANGELOG.md` + - Update `version` in `frontend/package.json` + +3. **Merge into `main`** via PR, then **tag on `main`**: + ```bash + git checkout main && git pull + git tag vX.Y.Z + git push origin vX.Y.Z + ``` + +4. The tag triggers the **release workflow** which builds and pushes the Docker image to `ghcr.io`. + +### Docker Image Tags + +| Tag | Description | +|-----|-------------| +| `X.Y.Z` | Exact version | +| `X.Y` | Latest patch of this minor | +| `latest` | Latest stable release | + +### Hotfix + +```bash +git checkout vX.Y.Z # from the release tag +git checkout -b hotfix/X.Y.Z+1 +# fix, commit, PR to main +git tag vX.Y.Z+1 # tag on main after merge +``` + +### Changelog + +We follow [Keep a Changelog](https://keepachangelog.com/). Every PR should add a line under `[Unreleased]` in `CHANGELOG.md`. The release branch moves `[Unreleased]` to the versioned section. + ## Pull Request Guidelines - Keep PRs focused — one feature or fix per PR diff --git a/Dockerfile b/Dockerfile index 4c33624..bbfbe36 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,15 +10,20 @@ # --- Stage 1: Build frontend assets --- FROM node:20-alpine AS frontend-build +ARG APP_VERSION=dev + WORKDIR /build COPY frontend/package*.json ./ RUN npm ci COPY frontend/ . -RUN npm run build +RUN VITE_APP_VERSION=${APP_VERSION} npm run build # --- Stage 2: Runtime (Python + Nginx) --- FROM python:3.12-slim +ARG APP_VERSION=dev +ENV APP_VERSION=${APP_VERSION} + # System deps: poppler (pdf2image), nginx, and OpenCV runtime libs RUN apt-get update && apt-get install -y --no-install-recommends \ poppler-utils \ diff --git a/README.md b/README.md index 2477cc3..5ac1323 100644 --- a/README.md +++ b/README.md @@ -167,14 +167,11 @@ GitHub Actions pipelines (see [`.github/workflows/`](.github/workflows/)): | Workflow | Trigger | What it does | |----------|---------|--------------| -| **CI** | push to `main`, pull requests | Lint + type check + Backend tests (99) + Frontend tests (81) + build | +| **CI** | push to `main`, pull requests | Lint + type check + Backend tests + Frontend tests + build | | **Release** | push tag `v*` | Build & push multi-arch Docker image to `ghcr.io` | +| **Docs** | push to `main` (docs changes) | Build & deploy MkDocs to GitHub Pages | -To publish a new version: -```bash -git tag v0.2.0 -git push origin v0.2.0 -``` +We follow [Semantic Versioning](https://semver.org/) with a simplified Git Flow. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full release process. ## Performance & System Requirements diff --git a/document-parser/infra/settings.py b/document-parser/infra/settings.py index 348f551..b08bf2b 100644 --- a/document-parser/infra/settings.py +++ b/document-parser/infra/settings.py @@ -8,6 +8,7 @@ from dataclasses import dataclass, field @dataclass(frozen=True) class Settings: + app_version: str = "dev" conversion_engine: str = "local" # "local" or "remote" docling_serve_url: str = "http://localhost:5001" docling_serve_api_key: str | None = None @@ -22,6 +23,7 @@ class Settings: def from_env(cls) -> Settings: cors_raw = os.environ.get("CORS_ORIGINS", "http://localhost:3000,http://localhost:5173") return cls( + app_version=os.environ.get("APP_VERSION", "dev"), conversion_engine=os.environ.get("CONVERSION_ENGINE", "local"), docling_serve_url=os.environ.get("DOCLING_SERVE_URL", "http://localhost:5001"), docling_serve_api_key=os.environ.get("DOCLING_SERVE_API_KEY"), diff --git a/document-parser/main.py b/document-parser/main.py index f6d1058..aaf0275 100644 --- a/document-parser/main.py +++ b/document-parser/main.py @@ -106,4 +106,8 @@ app.include_router(analyses_router) @app.get("/health") def health(): """Health check endpoint.""" - return {"status": "ok", "engine": settings.conversion_engine} + return { + "status": "ok", + "version": settings.app_version, + "engine": settings.conversion_engine, + } diff --git a/frontend/env.d.ts b/frontend/env.d.ts index 8b53f84..f1a72ef 100644 --- a/frontend/env.d.ts +++ b/frontend/env.d.ts @@ -1,5 +1,7 @@ /// +declare const __APP_VERSION__: string + declare module '*.vue' { import type { DefineComponent } from 'vue' const component: DefineComponent, Record, unknown> diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 8ae252b..8b4be65 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -9,6 +9,9 @@ export default [ { files: ['src/**/*.{ts,js,vue}'], languageOptions: { + globals: { + __APP_VERSION__: 'readonly', + }, parserOptions: { parser: tseslint.parser, }, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fa8af36..469c71b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "docling-studio", - "version": "0.1.0", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "docling-studio", - "version": "0.1.0", + "version": "0.3.0", "dependencies": { "dompurify": "^3.3.3", "marked": "^17.0.4", diff --git a/frontend/package.json b/frontend/package.json index 580d0b3..d779941 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "docling-studio", - "version": "0.1.0", + "version": "0.3.0", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/features/settings/ui/SettingsPanel.vue b/frontend/src/features/settings/ui/SettingsPanel.vue index 25581fa..356585d 100644 --- a/frontend/src/features/settings/ui/SettingsPanel.vue +++ b/frontend/src/features/settings/ui/SettingsPanel.vue @@ -22,7 +22,7 @@
- 0.1.0 + {{ version }}
@@ -31,6 +31,7 @@ import { useSettingsStore } from '../store' import { useI18n } from '../../../shared/i18n' +const version = __APP_VERSION__ const store = useSettingsStore() const { t } = useI18n() diff --git a/frontend/src/shared/ui/AppSidebar.vue b/frontend/src/shared/ui/AppSidebar.vue index 315df01..db1f25f 100644 --- a/frontend/src/shared/ui/AppSidebar.vue +++ b/frontend/src/shared/ui/AppSidebar.vue @@ -28,7 +28,7 @@ @@ -37,6 +37,7 @@ import { RouterLink, useRoute } from 'vue-router' import { useI18n } from '../i18n' +const version = __APP_VERSION__ const route = useRoute() const { t } = useI18n() diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 9a8b9da..4f0011d 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,8 +1,14 @@ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' +import { readFileSync } from 'fs' + +const pkg = JSON.parse(readFileSync('./package.json', 'utf-8')) export default defineConfig({ plugins: [vue()], + define: { + __APP_VERSION__: JSON.stringify(process.env.VITE_APP_VERSION || pkg.version), + }, server: { port: 3000, proxy: {