diff --git a/.github/workflows/deploy-containers.yaml b/.github/workflows/deploy-containers.yaml index 5f954a212..19dfdcb42 100644 --- a/.github/workflows/deploy-containers.yaml +++ b/.github/workflows/deploy-containers.yaml @@ -57,11 +57,17 @@ jobs: digest: ${{ steps.build.outputs.digest }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Compute image prefix id: meta run: echo "prefix=${REGISTRY}/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT" + - name: Compute dashboard version + id: dashboard_version + run: echo "dashboard_version=$(git describe --tags --always)" >> "$GITHUB_OUTPUT" + - name: Log in to GHCR uses: docker/login-action@v3 with: @@ -79,6 +85,8 @@ jobs: context: . file: ./dashboard/Dockerfile push: true + build-args: | + DASHBOARD_VERSION=${{ steps.dashboard_version.outputs.dashboard_version }} tags: | ${{ steps.meta.outputs.prefix }}/dashboard-frontend:latest ${{ steps.meta.outputs.prefix }}/dashboard-frontend:${{ github.sha }} diff --git a/.github/workflows/deploy-staging.yaml b/.github/workflows/deploy-staging.yaml index e51b67a35..c2c337231 100644 --- a/.github/workflows/deploy-staging.yaml +++ b/.github/workflows/deploy-staging.yaml @@ -9,12 +9,17 @@ jobs: outputs: has_new_migrations: ${{ steps.check.outputs.has_new_migrations }} migration_list: ${{ steps.check.outputs.migration_list }} + dashboard_version: ${{ steps.dashboard_version.outputs.dashboard_version }} steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Compute dashboard version + id: dashboard_version + run: echo "dashboard_version=$(git describe --tags --always)" >> "$GITHUB_OUTPUT" + - name: Check for new Django migrations id: check run: | @@ -68,6 +73,7 @@ jobs: cp ~/.env-staging dashboard-staging/.env && cd dashboard-staging && git checkout ${GITHUB_SHA} && + export DASHBOARD_VERSION=${DASHBOARD_VERSION} && docker compose pull --ignore-buildable --policy=always && docker compose build --no-cache && docker compose up -d --remove-orphans @@ -76,6 +82,7 @@ jobs: SSH_USER: ${{ secrets.STAGING_USER }} SSH_HOST: ${{ secrets.STAGING_HOST }} SSH_KEY: ${{ secrets.STAGING_KEY }} + DASHBOARD_VERSION: ${{ needs.check-migrations.outputs.dashboard_version }} cleanup: needs: deploy-staging diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index ea1498ec3..99e533791 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -205,6 +205,28 @@ docker compose -f docker-compose-next.yml pull docker compose -f docker-compose-next.yml up -d ``` +### Tagging a release + +Every production deployment must be preceded by a release tag. +The dashboard displays its version (`git describe --tags`) at the bottom of the +side menu, so an untagged deployment shows only a commit hash, making it hard to +tell which release is live. + +1. Tag the `main` commit being released, following the `release/YYYYMMDD.N` +convention (`N` starts at `0` and increments for further releases on the same day): + + ```bash + git fetch --tags + git tag release/20260729.0 + git push origin release/20260729.0 + ``` + +2. Manually trigger the `Publish GHCR Images` workflow. Images built by the +earlier push to `main` were baked before the tag existed, so they still carry the +previous version string. +3. Trigger the `Deploy production Dashboard` workflow with the new tag. +4. Confirm the version shown in the side menu matches the tag. + --- ## 3. Staging diff --git a/dashboard/Dockerfile b/dashboard/Dockerfile index c09726261..53fd5237b 100644 --- a/dashboard/Dockerfile +++ b/dashboard/Dockerfile @@ -8,6 +8,9 @@ RUN npm install -g pnpm@10.33.3 && pnpm install --frozen-lockfile COPY dashboard/. ./ +ARG DASHBOARD_VERSION +ENV VITE_DASHBOARD_VERSION=$DASHBOARD_VERSION + RUN pnpm build # Stage 2: Copy the static files from the builder stage diff --git a/dashboard/src/components/SideMenu/SideMenuContent.tsx b/dashboard/src/components/SideMenu/SideMenuContent.tsx index 31fde4cf7..fbb1421cc 100644 --- a/dashboard/src/components/SideMenu/SideMenuContent.tsx +++ b/dashboard/src/components/SideMenu/SideMenuContent.tsx @@ -16,6 +16,8 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/Tooltip'; import { ExternalLinkIcon } from '@/components/Icons/ExternalLink'; +import { REPO_URL } from '@/utils/constants/general'; + import SendFeedback from './SendFeedback'; import NavLink from './NavLink'; import { @@ -25,6 +27,17 @@ import { type RouteMenuItems, } from './menuItems'; +const dashboardVersion: string = import.meta.env.VITE_DASHBOARD_VERSION; + +const versionRepoUrl = (version: string): string => { + if (version.endsWith('-dirty')) { + return REPO_URL; + } + + const commitAfterTag = version.match(/-g([0-9a-f]+)$/)?.[1]; + return `${REPO_URL}/tree/${commitAfterTag ?? version}`; +}; + type SideMenuItemProps = { item: RouteMenuItems; }; @@ -124,6 +137,19 @@ const SideMenuContent = ({
{dashboardElements}
+ {dashboardVersion && ( + <> + + + {dashboardVersion} + + + )} ); diff --git a/dashboard/vite.config.ts b/dashboard/vite.config.ts index a0ca62a28..eaee9fcad 100644 --- a/dashboard/vite.config.ts +++ b/dashboard/vite.config.ts @@ -1,4 +1,6 @@ import path from 'path'; +import { execSync } from 'node:child_process'; + import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import tsconfigPaths from 'vite-tsconfig-paths'; @@ -6,9 +8,22 @@ import tailwindcss from '@tailwindcss/vite'; import { TanStackRouterVite } from '@tanstack/router-plugin/vite'; +const dashboardVersion = + process.env.VITE_DASHBOARD_VERSION || + ((): string => { + try { + return execSync('git describe --tags --always --dirty').toString().trim(); + } catch { + return ''; + } + })(); + // https://vitejs.dev/config/ export default defineConfig({ plugins: [tailwindcss(), tsconfigPaths(), react(), TanStackRouterVite()], + define: { + 'import.meta.env.VITE_DASHBOARD_VERSION': JSON.stringify(dashboardVersion), + }, resolve: { alias: { '@': path.resolve(__dirname, './src'), diff --git a/docker-compose.yml b/docker-compose.yml index eb4fc4a21..109478add 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -114,6 +114,8 @@ services: build: context: . dockerfile: ./dashboard/Dockerfile + args: + DASHBOARD_VERSION: ${DASHBOARD_VERSION:-} image: ${IMAGE_REGISTRY:-ghcr.io}/${IMAGE_REPOSITORY:-local}/dashboard-frontend:${IMAGE_TAG:-latest} volumes: - static-data:/data/static