diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml index 3c52b52ce..0a3bc871e 100644 --- a/.github/workflows/bot.yml +++ b/.github/workflows/bot.yml @@ -20,13 +20,24 @@ jobs: permissions: contents: read packages: write + outputs: + image_tag: ${{ steps.revision.outputs.image_tag }} steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false + - name: Revision tag (YYYYMMDD-shortsha + full SHA aliases) + id: revision + run: | + set -eu + short=$(git rev-parse --short=7 HEAD) + d=$(TZ=UTC0 git log -1 --format=%cd --date=format:%Y%m%d HEAD) + echo "image_tag=${d}-${short}" >> "$GITHUB_OUTPUT" + - name: Log in to GHCR + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 with: registry: ghcr.io @@ -49,11 +60,12 @@ jobs: provenance: true sbom: true tags: | + ghcr.io/zkldi/tachi-bot:${{ steps.revision.outputs.image_tag }} ghcr.io/zkldi/tachi-bot:${{ github.sha }} ghcr.io/zkldi/tachi-bot:main ghcr.io/zkldi/tachi-bot:latest build-args: | - COMMIT_HASH=${{ github.sha }} + GIT_REVISION=${{ github.sha }} BUILD_TIME=${{ steps.build_time.outputs.value }} cache-from: type=gha,scope=${{ github.ref_name }}-bot cache-to: type=gha,mode=max,scope=${{ github.ref_name }}-bot @@ -73,10 +85,12 @@ jobs: GH_TOKEN: ${{ secrets.DEPLOY_DISPATCH_TOKEN }} DEST: ${{ matrix.destination }} SHA: ${{ github.sha }} + IMAGE_TAG: ${{ needs.docker-push.outputs.image_tag }} run: | gh api repos/zkldi/tachi-deploy/dispatches \ -f event_type=deploy \ -F "client_payload[app]=bot" \ -F "client_payload[dest]=${DEST}" \ - -F "client_payload[sha]=${SHA}" - echo "::notice::Dispatched bot@${DEST} ${SHA} — logs: https://github.com/zkldi/tachi-deploy/actions/workflows/deploy.yml" + -F "client_payload[sha]=${SHA}" \ + -F "client_payload[image_tag]=${IMAGE_TAG}" + echo "::notice::Dispatched bot@${DEST} ${IMAGE_TAG} (${SHA}) — logs: https://github.com/zkldi/tachi-deploy/actions/workflows/deploy.yml" diff --git a/.github/workflows/dev-image.yml b/.github/workflows/dev-image.yml index 23c5aac0b..ff6db15a1 100644 --- a/.github/workflows/dev-image.yml +++ b/.github/workflows/dev-image.yml @@ -34,6 +34,14 @@ jobs: with: persist-credentials: false + - name: Revision tag (YYYYMMDD-shortsha + full SHA aliases) + id: revision + run: | + set -eu + short=$(git rev-parse --short=7 HEAD) + d=$(TZ=UTC0 git log -1 --format=%cd --date=format:%Y%m%d HEAD) + echo "image_tag=${d}-${short}" >> "$GITHUB_OUTPUT" + - name: Log in to GHCR if: ${{ github.event_name != 'pull_request' }} uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 @@ -54,6 +62,7 @@ jobs: tags: | ghcr.io/zkldi/tachi-dev:main ghcr.io/zkldi/tachi-dev:latest + ghcr.io/zkldi/tachi-dev:${{ steps.revision.outputs.image_tag }} ghcr.io/zkldi/tachi-dev:${{ github.sha }} cache-from: type=gha,scope=dev-image cache-to: type=gha,mode=max,scope=dev-image diff --git a/.github/workflows/github-bot.yml b/.github/workflows/github-bot.yml index b292906b6..585482a32 100644 --- a/.github/workflows/github-bot.yml +++ b/.github/workflows/github-bot.yml @@ -19,13 +19,24 @@ jobs: permissions: contents: read packages: write + outputs: + image_tag: ${{ steps.revision.outputs.image_tag }} steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false + - name: Revision tag (YYYYMMDD-shortsha + full SHA aliases) + id: revision + run: | + set -eu + short=$(git rev-parse --short=7 HEAD) + d=$(TZ=UTC0 git log -1 --format=%cd --date=format:%Y%m%d HEAD) + echo "image_tag=${d}-${short}" >> "$GITHUB_OUTPUT" + - name: Log in to GHCR + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 with: registry: ghcr.io @@ -48,11 +59,12 @@ jobs: provenance: true sbom: true tags: | + ghcr.io/zkldi/tachi-ghbot:${{ steps.revision.outputs.image_tag }} ghcr.io/zkldi/tachi-ghbot:${{ github.sha }} ghcr.io/zkldi/tachi-ghbot:main ghcr.io/zkldi/tachi-ghbot:latest build-args: | - COMMIT_HASH=${{ github.sha }} + GIT_REVISION=${{ github.sha }} BUILD_TIME=${{ steps.build_time.outputs.value }} cache-from: type=gha,scope=${{ github.ref_name }}-github-bot cache-to: type=gha,mode=max,scope=${{ github.ref_name }}-github-bot @@ -67,10 +79,12 @@ jobs: env: GH_TOKEN: ${{ secrets.DEPLOY_DISPATCH_TOKEN }} SHA: ${{ github.sha }} + IMAGE_TAG: ${{ needs.docker-push.outputs.image_tag }} run: | gh api repos/zkldi/tachi-deploy/dispatches \ -f event_type=deploy \ -F "client_payload[app]=ghbot" \ -F "client_payload[dest]=-" \ - -F "client_payload[sha]=${SHA}" - echo "::notice::Dispatched ghbot ${SHA} — logs: https://github.com/zkldi/tachi-deploy/actions/workflows/deploy.yml" + -F "client_payload[sha]=${SHA}" \ + -F "client_payload[image_tag]=${IMAGE_TAG}" + echo "::notice::Dispatched ghbot ${IMAGE_TAG} (${SHA}) — logs: https://github.com/zkldi/tachi-deploy/actions/workflows/deploy.yml" diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml index ddb5572bc..7b103a388 100644 --- a/.github/workflows/server.yml +++ b/.github/workflows/server.yml @@ -105,12 +105,22 @@ jobs: permissions: contents: read packages: write + outputs: + image_tag: ${{ steps.revision.outputs.image_tag }} steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false + - name: Revision tag (YYYYMMDD-shortsha + full SHA aliases) + id: revision + run: | + set -eu + short=$(git rev-parse --short=7 HEAD) + d=$(TZ=UTC0 git log -1 --format=%cd --date=format:%Y%m%d HEAD) + echo "image_tag=${d}-${short}" >> "$GITHUB_OUTPUT" + - name: Log in to GHCR uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 with: @@ -140,12 +150,14 @@ jobs: provenance: true sbom: true tags: | + ghcr.io/zkldi/tachi:${{ steps.revision.outputs.image_tag }} ghcr.io/zkldi/tachi:${{ github.sha }} ghcr.io/zkldi/tachi:main ghcr.io/zkldi/tachi:latest build-args: | VERSION=${{ steps.server_version.outputs.version }} - COMMIT_HASH=${{ github.sha }} + VERSION_DETAIL=${{ steps.revision.outputs.image_tag }} + GIT_REVISION=${{ github.sha }} BUILD_TIME=${{ steps.build_time.outputs.value }} cache-from: type=gha,scope=server-${{ github.ref_name }} cache-to: type=gha,mode=max,scope=server-${{ github.ref_name }} @@ -170,10 +182,12 @@ jobs: GH_TOKEN: ${{ secrets.DEPLOY_DISPATCH_TOKEN }} DEST: ${{ matrix.destination }} SHA: ${{ github.sha }} + IMAGE_TAG: ${{ needs.docker-push.outputs.image_tag }} run: | gh api repos/zkldi/tachi-deploy/dispatches \ -f event_type=deploy \ -F "client_payload[app]=server" \ -F "client_payload[dest]=${DEST}" \ - -F "client_payload[sha]=${SHA}" - echo "::notice::Dispatched server@${DEST} ${SHA} — logs: https://github.com/zkldi/tachi-deploy/actions/workflows/deploy.yml" + -F "client_payload[sha]=${SHA}" \ + -F "client_payload[image_tag]=${IMAGE_TAG}" + echo "::notice::Dispatched server@${DEST} ${IMAGE_TAG} (${SHA}) — logs: https://github.com/zkldi/tachi-deploy/actions/workflows/deploy.yml" diff --git a/Justfile-apps b/Justfile-apps index 366337979..76c05ca07 100644 --- a/Justfile-apps +++ b/Justfile-apps @@ -18,8 +18,8 @@ server: set -euo pipefail cd typescript/server VERSION="${VERSION:-"$(jq -r .version package.json)"}" - COMMIT_HASH="${COMMIT_HASH:-"$(git rev-parse --short HEAD)"}" - export VERSION COMMIT_HASH + VERSION_DETAIL="${VERSION_DETAIL:-$(TZ=UTC0 git log -1 --format=%cd --date=format:%Y%m%d HEAD)-$(git rev-parse --short=7 HEAD)}" + export VERSION VERSION_DETAIL sidecar_pids=() # Crons: Postgres-backed scheduler. Jobs: `job_queue` (SKIP LOCKED; run multiple for load). if [[ -z "${TACHI_SERVER_NO_SIDECARS:-}" ]]; then diff --git a/Justfile-repo b/Justfile-repo index 227093f36..bbd1ef591 100644 --- a/Justfile-repo +++ b/Justfile-repo @@ -12,16 +12,27 @@ check-all-docker-builds: assert-docker-host #!/usr/bin/env bash set -euo pipefail root="$(git rev-parse --show-toplevel)" - sha="$(git -C "$root" rev-parse --short HEAD)" server_ver="$(jq -r .version "$root/typescript/server/package.json")" + detail="$(TZ=UTC0 git -C "$root" log -1 --format=%cd --date=format:%Y%m%d HEAD)-$(git -C "$root" rev-parse --short=7 HEAD)" + full="$(git -C "$root" rev-parse HEAD)" + bt="$(date -u +%Y-%m-%dT%H:%M:%SZ)" echo "==> server (prod)" docker build -f "$root/docker/Dockerfile.server" --target prod \ --build-arg "VERSION=$server_ver" \ - --build-arg "COMMIT_HASH=$sha" -t tachi-server:check "$root" + --build-arg "VERSION_DETAIL=$detail" \ + --build-arg "GIT_REVISION=$full" \ + --build-arg "BUILD_TIME=$bt" \ + -t tachi-server:check "$root" echo "==> bot" - docker build -f "$root/docker/Dockerfile.bot" -t tachi-bot:check "$root" + docker build -f "$root/docker/Dockerfile.bot" \ + --build-arg "GIT_REVISION=$full" \ + --build-arg "BUILD_TIME=$bt" \ + -t tachi-bot:check "$root" echo "==> github-bot" - docker build -f "$root/docker/Dockerfile.ghbot" -t tachi-ghbot:check "$root" + docker build -f "$root/docker/Dockerfile.ghbot" \ + --build-arg "GIT_REVISION=$full" \ + --build-arg "BUILD_TIME=$bt" \ + -t tachi-ghbot:check "$root" echo "==> dev image" docker build -f "$root/Dockerfile.dev" -t tachi-dev:check "$root" echo "All Docker builds succeeded." diff --git a/docker/Dockerfile.bot b/docker/Dockerfile.bot index deb4235b7..8ff68be40 100644 --- a/docker/Dockerfile.bot +++ b/docker/Dockerfile.bot @@ -1,6 +1,6 @@ # Build context must be the repo root. Full tree + .dockerignore (see repo root). # docker build -f docker/Dockerfile.bot \ -# --build-arg COMMIT_HASH="$(git rev-parse --short HEAD)" \ +# --build-arg GIT_REVISION="$(git rev-parse HEAD)" \ # --build-arg BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ # . @@ -28,11 +28,11 @@ RUN bun build \ # --- prod: nothing but the binary --- FROM alpine:3.21 AS prod -ARG COMMIT_HASH +ARG GIT_REVISION ARG BUILD_TIME # Fail the build if any arg is omitted (Docker has no required-ARG syntax). RUN set -eu; \ - : "${COMMIT_HASH:?Dockerfile.bot: COMMIT_HASH build-arg is required}"; \ + : "${GIT_REVISION:?Dockerfile.bot: GIT_REVISION build-arg is required}"; \ : "${BUILD_TIME:?Dockerfile.bot: BUILD_TIME build-arg is required}"; RUN apk add --no-cache curl libstdc++ libgcc @@ -41,7 +41,7 @@ COPY --from=build /bot /bot LABEL service="tachi-bot" \ org.opencontainers.image.source="https://github.com/zkldi/Tachi3" \ - org.opencontainers.image.revision="${COMMIT_HASH}" \ + org.opencontainers.image.revision="${GIT_REVISION}" \ org.opencontainers.image.created="${BUILD_TIME}" HEALTHCHECK --interval=15s --timeout=5s \ diff --git a/docker/Dockerfile.ghbot b/docker/Dockerfile.ghbot index d19b88334..bfb1a36ed 100644 --- a/docker/Dockerfile.ghbot +++ b/docker/Dockerfile.ghbot @@ -1,6 +1,6 @@ # Build context must be the repo root. Full tree + .dockerignore (see repo root). # docker build -f docker/Dockerfile.ghbot \ -# --build-arg COMMIT_HASH="$(git rev-parse --short HEAD)" \ +# --build-arg GIT_REVISION="$(git rev-parse HEAD)" \ # --build-arg BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ # . @@ -28,11 +28,11 @@ RUN bun build \ # --- prod: nothing but the binary --- FROM alpine:3.21 AS prod -ARG COMMIT_HASH +ARG GIT_REVISION ARG BUILD_TIME # Fail the build if any arg is omitted (Docker has no required-ARG syntax). RUN set -eu; \ - : "${COMMIT_HASH:?Dockerfile.ghbot: COMMIT_HASH build-arg is required}"; \ + : "${GIT_REVISION:?Dockerfile.ghbot: GIT_REVISION build-arg is required}"; \ : "${BUILD_TIME:?Dockerfile.ghbot: BUILD_TIME build-arg is required}"; RUN apk add --no-cache curl libstdc++ libgcc @@ -41,7 +41,7 @@ COPY --from=build /ghbot /ghbot LABEL service="tachi-ghbot" \ org.opencontainers.image.source="https://github.com/zkldi/Tachi3" \ - org.opencontainers.image.revision="${COMMIT_HASH}" \ + org.opencontainers.image.revision="${GIT_REVISION}" \ org.opencontainers.image.created="${BUILD_TIME}" HEALTHCHECK --interval=15s --timeout=5s \ diff --git a/docker/Dockerfile.server b/docker/Dockerfile.server index eee58d542..fea5a4d44 100644 --- a/docker/Dockerfile.server +++ b/docker/Dockerfile.server @@ -2,7 +2,8 @@ # Respects .dockerignore (see repo root). Full tree copy keeps workspace installs correct. # docker build -f docker/Dockerfile.server \ # --build-arg VERSION="$(jq -r .version typescript/server/package.json)" \ -# --build-arg COMMIT_HASH="$(git rev-parse --short HEAD)" \ +# --build-arg VERSION_DETAIL="$(TZ=UTC0 git log -1 --format=%cd --date=format:%Y%m%d HEAD)-$(git rev-parse --short=7 HEAD)" \ +# --build-arg GIT_REVISION="$(git rev-parse HEAD)" \ # --build-arg BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ # -t tachi-server:local . # @@ -24,16 +25,18 @@ RUN bun install --frozen-lockfile FROM oven/bun:alpine AS prod ARG VERSION -ARG COMMIT_HASH +ARG VERSION_DETAIL +ARG GIT_REVISION ARG BUILD_TIME # Fail the build if any arg is omitted (Docker has no required-ARG syntax). RUN set -eu; \ : "${VERSION:?Dockerfile.server: VERSION build-arg is required}"; \ - : "${COMMIT_HASH:?Dockerfile.server: COMMIT_HASH build-arg is required}"; \ + : "${VERSION_DETAIL:?Dockerfile.server: VERSION_DETAIL build-arg is required}"; \ + : "${GIT_REVISION:?Dockerfile.server: GIT_REVISION build-arg is required}"; \ : "${BUILD_TIME:?Dockerfile.server: BUILD_TIME build-arg is required}"; ENV VERSION=${VERSION} -ENV COMMIT_HASH=${COMMIT_HASH} +ENV VERSION_DETAIL=${VERSION_DETAIL} ENV MIGRATIONS_DIR=/app/db/migrations ENV PORT=8080 @@ -45,7 +48,7 @@ COPY --from=deps /app . LABEL service="tachi-server" \ org.opencontainers.image.source="https://github.com/zkldi/Tachi3" \ - org.opencontainers.image.revision="${COMMIT_HASH}" \ + org.opencontainers.image.revision="${GIT_REVISION}" \ org.opencontainers.image.version="${VERSION}" \ org.opencontainers.image.created="${BUILD_TIME}" diff --git a/typescript/server/.env.example b/typescript/server/.env.example index 95b9a0962..9032070d0 100644 --- a/typescript/server/.env.example +++ b/typescript/server/.env.example @@ -5,6 +5,8 @@ REDIS_URL= POSTGRES_URL= MIGRATIONS_DIR= VERSION= +# YYYYMMDD-shortsha from commit date (UTC) + abbreviated SHA; COMMIT_HASH is still read as fallback +VERSION_DETAIL= COMMIT_HASH= LOG_LEVEL= diff --git a/typescript/server/.env.test b/typescript/server/.env.test index ea1338fec..3287120d4 100644 --- a/typescript/server/.env.test +++ b/typescript/server/.env.test @@ -5,7 +5,7 @@ PORT=8080 MIGRATIONS_DIR=/tachi/db/migrations LOG_LEVEL=warn VERSION=test -COMMIT_HASH=test +VERSION_DETAIL=test-detail TACHI_CAPTCHA_SECRET_KEY=unused TACHI_SESSION_SECRET=unused diff --git a/typescript/server/src/lib/constants/version.ts b/typescript/server/src/lib/constants/version.ts index 9b7d7f975..5281c4c0d 100644 --- a/typescript/server/src/lib/constants/version.ts +++ b/typescript/server/src/lib/constants/version.ts @@ -1,4 +1,4 @@ import { Env } from "#lib/setup/config"; export const VERSION_STR = Env.VERSION; -export const VERSION_PRETTY = `v${Env.VERSION} [${Env.COMMIT_HASH ?? "unknown commit"}]`; +export const VERSION_PRETTY = `v${Env.VERSION} [${Env.VERSION_DETAIL}]`; diff --git a/typescript/server/src/lib/setup/config.ts b/typescript/server/src/lib/setup/config.ts index 90cde4f1f..49734feee 100644 --- a/typescript/server/src/lib/setup/config.ts +++ b/typescript/server/src/lib/setup/config.ts @@ -522,11 +522,21 @@ if (!version) { version = "0.0.0"; } -let commitHash = process.env.COMMIT_HASH; +/** Human-readable build identity: `{commit date UTC YYYYMMDD}-{short sha}`, e.g. `20260516-a1b2c3d`. */ +let versionDetail = process.env.VERSION_DETAIL?.trim(); -if (!commitHash) { - log.warn(`No COMMIT_HASH specified in environment. defaulting to unknown commit.`); - commitHash = "unknown"; +if (!versionDetail) { + const legacy = process.env.COMMIT_HASH?.trim(); + if (legacy) { + versionDetail = legacy; + } +} + +if (!versionDetail) { + log.warn( + `No VERSION_DETAIL (or COMMIT_HASH) specified in environment. defaulting detail to unknown.`, + ); + versionDetail = "unknown"; } export const Env = { @@ -535,7 +545,7 @@ export const Env = { POSTGRES_URL, MIGRATIONS_DIR, VERSION: version, - COMMIT_HASH: commitHash, + VERSION_DETAIL: versionDetail, NODE_ENV: NODE_ENV as "dev" | "production" | "staging" | "test", LOG_LEVEL: logLevel as "crit" | "debug" | "error" | "info" | "severe" | "verbose" | "warn", }; diff --git a/typescript/server/vitest.config.ts b/typescript/server/vitest.config.ts index 75a83581a..970f2c72c 100644 --- a/typescript/server/vitest.config.ts +++ b/typescript/server/vitest.config.ts @@ -38,7 +38,7 @@ export default defineConfig({ MIGRATIONS_DIR: "/tachi/db/migrations", LOG_LEVEL: "warn", VERSION: "test", - COMMIT_HASH: "test", + VERSION_DETAIL: "test-detail", }, // Parallel test execution - each worker gets its own isolated Postgres database.