Add CalVer build-time version tracking

- Dockerfile: ARG BUILD_VERSION=dev → ENV APP_VERSION baked into image
- ci.yml: BUILD_VERSION passed as build-arg; set to git tag name on v*
  tag builds, "dev" on branch builds
- routes/api.py: GET /api/version returns {"version": APP_VERSION}
- SettingsView: fetches /api/version on mount, displays in About section
  under General tab

Version source of truth is the git tag (YY.MM.DD.N CalVer).
pyproject.toml / package.json versions are no longer maintained.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 16:10:57 -04:00
parent 767fa73e1d
commit 4636c9a973
4 changed files with 46 additions and 1 deletions
+5 -1
View File
@@ -104,18 +104,21 @@ jobs:
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- name: Generate image tags - name: Generate image tags and version
id: tags id: tags
run: | run: |
TAGS="${{ env.IMAGE }}:${{ github.sha }}" TAGS="${{ env.IMAGE }}:${{ github.sha }}"
BUILD_VERSION="dev"
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
TAGS="$TAGS,${{ env.IMAGE }}:latest" TAGS="$TAGS,${{ env.IMAGE }}:latest"
elif [[ "${{ github.ref }}" == "refs/heads/dev" ]]; then elif [[ "${{ github.ref }}" == "refs/heads/dev" ]]; then
TAGS="$TAGS,${{ env.IMAGE }}:dev" TAGS="$TAGS,${{ env.IMAGE }}:dev"
elif [[ "${{ github.ref }}" == refs/tags/* ]]; then elif [[ "${{ github.ref }}" == refs/tags/* ]]; then
TAGS="$TAGS,${{ env.IMAGE }}:latest,${{ env.IMAGE }}:${{ github.ref_name }}" TAGS="$TAGS,${{ env.IMAGE }}:latest,${{ env.IMAGE }}:${{ github.ref_name }}"
BUILD_VERSION="${{ github.ref_name }}"
fi fi
echo "value=$TAGS" >> $GITHUB_OUTPUT echo "value=$TAGS" >> $GITHUB_OUTPUT
echo "build_version=$BUILD_VERSION" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@v4
@@ -133,5 +136,6 @@ jobs:
context: . context: .
push: true push: true
tags: ${{ steps.tags.outputs.value }} tags: ${{ steps.tags.outputs.value }}
build-args: BUILD_VERSION=${{ steps.tags.outputs.build_version }}
cache-from: type=registry,ref=${{ env.IMAGE }}:cache cache-from: type=registry,ref=${{ env.IMAGE }}:cache
cache-to: type=registry,ref=${{ env.IMAGE }}:cache,mode=max cache-to: type=registry,ref=${{ env.IMAGE }}:cache,mode=max
+5
View File
@@ -21,5 +21,10 @@ COPY alembic/ alembic/
# Ensure Python finds the source tree (where static files live) before site-packages # Ensure Python finds the source tree (where static files live) before site-packages
ENV PYTHONPATH=/app/src ENV PYTHONPATH=/app/src
# Version is injected at build time via --build-arg BUILD_VERSION=YY.MM.DD.N
# Falls back to "dev" for local / untagged builds
ARG BUILD_VERSION=dev
ENV APP_VERSION=$BUILD_VERSION
EXPOSE 5000 EXPOSE 5000
CMD ["sh", "-c", "alembic upgrade head && hypercorn 'fabledassistant.app:create_app()' --bind 0.0.0.0:5000 --keep-alive 600"] CMD ["sh", "-c", "alembic upgrade head && hypercorn 'fabledassistant.app:create_app()' --bind 0.0.0.0:5000 --keep-alive 600"]
+29
View File
@@ -28,6 +28,7 @@ const saving = ref(false);
const saved = ref(false); const saved = ref(false);
const exporting = ref(false); const exporting = ref(false);
const restoring = ref(false); const restoring = ref(false);
const appVersion = ref('dev');
const restoreFileInput = ref<HTMLInputElement | null>(null); const restoreFileInput = ref<HTMLInputElement | null>(null);
// Migrate stored "admin" → "config"; unknown tabs fall back to "general" // Migrate stored "admin" → "config"; unknown tabs fall back to "general"
@@ -176,6 +177,10 @@ const searchLoading = ref(false);
const searchError = ref(""); const searchError = ref("");
onMounted(async () => { onMounted(async () => {
try {
const v = await apiGet<{ version: string }>('/api/version')
appVersion.value = v.version
} catch { /* non-critical */ }
await store.fetchSettings(); await store.fetchSettings();
pushStore.checkSubscription(); pushStore.checkSubscription();
assistantName.value = store.assistantName; assistantName.value = store.assistantName;
@@ -1031,6 +1036,11 @@ function formatUserDate(iso: string): string {
</div> </div>
</section> </section>
<section class="settings-section full-width">
<h2>About</h2>
<p class="version-line">Fabled Assistant <span class="version-badge">{{ appVersion }}</span></p>
</section>
</div> </div>
<!-- Integrations --> <!-- Integrations -->
@@ -2206,6 +2216,25 @@ function formatUserDate(iso: string): string {
margin-right: 0.25rem; margin-right: 0.25rem;
} }
/* ── About / version ─────────────────────────────────────────── */
.version-line {
margin: 0;
font-size: 0.9rem;
color: var(--color-text-secondary);
display: flex;
align-items: center;
gap: 0.6rem;
}
.version-badge {
font-family: ui-monospace, monospace;
font-size: 0.8rem;
padding: 0.15rem 0.5rem;
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 4px;
color: var(--color-text-muted);
}
/* ── Groups tab ──────────────────────────────────────────────── */ /* ── Groups tab ──────────────────────────────────────────────── */
.btn-primary { .btn-primary {
padding: 0.4rem 0.9rem; padding: 0.4rem 0.9rem;
+7
View File
@@ -1,3 +1,5 @@
import os
from quart import Blueprint, jsonify from quart import Blueprint, jsonify
api = Blueprint("api", __name__, url_prefix="/api") api = Blueprint("api", __name__, url_prefix="/api")
@@ -6,3 +8,8 @@ api = Blueprint("api", __name__, url_prefix="/api")
@api.route("/health") @api.route("/health")
async def health(): async def health():
return jsonify({"status": "ok"}) return jsonify({"status": "ok"})
@api.route("/version")
async def version():
return jsonify({"version": os.environ.get("APP_VERSION", "dev")})