Add release process, dynamic versioning, and changelog catch-up

Replace hardcoded version strings with build-time injection:
- Frontend: Vite __APP_VERSION__ from env or package.json
- Backend: APP_VERSION env var exposed via /health endpoint
- Docker: build arg propagated through both stages
- CI: release workflow extracts version from git tag

Document branching strategy and release process in CONTRIBUTING.md.
Catch up CHANGELOG with v0.2.0 and Unreleased sections.
Sync package.json version to 0.3.0.
This commit is contained in:
Pier-Jean Malandrino 2026-04-02 16:08:37 +02:00
parent 84645f0d70
commit 1ff8c5108f
14 changed files with 139 additions and 13 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 \

View file

@ -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

View file

@ -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"),

View file

@ -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,
}

2
frontend/env.d.ts vendored
View file

@ -1,5 +1,7 @@
/// <reference types="vite/client" />
declare const __APP_VERSION__: string
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>

View file

@ -9,6 +9,9 @@ export default [
{
files: ['src/**/*.{ts,js,vue}'],
languageOptions: {
globals: {
__APP_VERSION__: 'readonly',
},
parserOptions: {
parser: tseslint.parser,
},

View file

@ -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",

View file

@ -1,6 +1,6 @@
{
"name": "docling-studio",
"version": "0.1.0",
"version": "0.3.0",
"private": true,
"type": "module",
"scripts": {

View file

@ -22,7 +22,7 @@
<div class="setting-group">
<label class="setting-label">{{ t('settings.version') }}</label>
<span class="setting-value">0.1.0</span>
<span class="setting-value">{{ version }}</span>
</div>
</div>
</template>
@ -31,6 +31,7 @@
import { useSettingsStore } from '../store'
import { useI18n } from '../../../shared/i18n'
const version = __APP_VERSION__
const store = useSettingsStore()
const { t } = useI18n()
</script>

View file

@ -28,7 +28,7 @@
</nav>
<div class="sidebar-footer">
<span class="version">v0.1.0</span>
<span class="version">v{{ version }}</span>
</div>
</aside>
</template>
@ -37,6 +37,7 @@
import { RouterLink, useRoute } from 'vue-router'
import { useI18n } from '../i18n'
const version = __APP_VERSION__
const route = useRoute()
const { t } = useI18n()

View file

@ -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: {