Skip to content

Version Control

How App-name versions, displays, and releases itself. The scheme is plain semantic versioning, the version string is shown in the UI alongside the environment, and pushing a tag is what ships production.

Semantic versioning scheme

Versions follow MAJOR.MINOR.PATCH.

Bump When Examples
MAJOR (1.0.0 → 2.0.0) Breaking changes Incompatible API changes, removed features, breaking schema migrations
MINOR (1.0.0 → 1.1.0) Backwards-compatible features New endpoints, new screens, new dashboard widgets
PATCH (1.0.0 → 1.0.1) Backwards-compatible fixes Bug fixes, performance work, security patches

No non-semantic versions

Always three numeric segments. Never 1.0.1.2 or date-based strings — the git tag, the display string, and the package version must all agree.

Where the version lives

The single source of truth is frontend/package.json:

{
  "version": "1.0.0"
}

The frontend reads this at build time and exposes it through a small version helper that the UI consumes. The git tag (vX.Y.Z) must match the package.json version for every release.

How the app displays version and environment

The UI shows the version in two places:

  • Login page — the version string under the sign-in card.
  • Application footer — fixed at the bottom of every authenticated page.

The environment is derived from the hostname, and a badge is shown for every non-production environment so it is always obvious where you are.

Hostname Environment Badge
localhost / 127.0.0.1 Development shown
test.<your-domain> Test shown
<your-domain> Production hidden

Hovering the version reveals the build time (see below). In production with no build time, it falls back to a "Development Build" label.

How build time is injected via Docker

The build timestamp is not committed — it is injected at image-build time by CI and baked into the frontend bundle as a Vite environment variable.

- name: Set build timestamp
  id: build-time
  run: echo "BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> "$GITHUB_OUTPUT"

- name: Build and push frontend image
  uses: docker/build-push-action@v6
  with:
    build-args: |
      VITE_BUILD_TIME=${{ steps.build-time.outputs.BUILD_TIME }}

The Dockerfile accepts the build arg and promotes it to a VITE_-prefixed variable so Vite inlines it:

ARG VITE_BUILD_TIME
ENV VITE_BUILD_TIME=$VITE_BUILD_TIME

Local builds have no build time

Running npm run dev locally leaves VITE_BUILD_TIME unset, which is why the footer shows "Development Build". This is expected.

The release workflow

Releasing is four steps: bump, commit, tag, push. Pushing the tag is what triggers the production deploy — see Deploy to Production.

# 1. Bump the version in frontend/package.json (e.g. 1.0.0 -> 1.1.0)

# 2. Commit the bump
git add frontend/package.json
git commit -m "Release v1.1.0: <summary of changes>"

# 3. Tag the release (annotated)
git tag -a v1.1.0 -m "Release v1.1.0"

# 4. Push the branch and the tag
git push origin main
git push origin v1.1.0

What happens next:

sequenceDiagram
    participant Dev
    participant GH as GitHub
    participant CI as GitHub Actions
    participant Prod as Production

    Dev->>GH: push main
    GH->>CI: build, test, scan, deploy
    CI->>CI: deploy to test (automatic)
    Dev->>GH: push tag vX.Y.Z
    GH->>CI: production deploy (awaits approval)
    CI->>Prod: build image with VITE_BUILD_TIME, deploy
    Prod-->>Dev: vX.Y.Z live, no env badge

Verify the release

After production deploys, confirm the login page and footer show the new vX.Y.Z, the environment badge is absent in production, and the build time is recent.

Version rollback

Rolling back is re-deploying a previous tag — the version string and image follow the tag, so there is nothing to hand-edit on the host.

  1. Identify the last known-good tag: git tag --list 'v*' --sort=-v:refname.
  2. Re-run the production deploy for that tag (re-trigger the deploy workflow for vX.Y.Z, approving the gate).
  3. If the rollback should become the new baseline, cut a fresh PATCH release from the good commit rather than leaving production on an older tag.

Editing version in production does nothing

The version is compiled into the frontend bundle at build time. Changing files on the host has no effect until a new image is built and deployed — always roll back through a tag and a rebuild.

Best practices

Do Don't
Keep the git tag and frontend/package.json in sync Skip the version bump on a release
Use annotated tags with descriptive messages Use non-semantic versions
Verify on test before tagging production Deploy straight to production untested
Roll back via a tag + rebuild Hand-edit version files on the server

See New Project Setup for cutting the very first v1.0.0, and Pipeline Overview for how the deploy stages fit together.