mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-21 02:05:45 +00:00
feat(frontend): show real project version on About page (#4126)
* feat(frontend): show real project version on About page The About page heading was hardcoded "About DeerFlow 2.0" while the real version is 2.1.0, kept in lockstep across backend/pyproject.toml, frontend/package.json, and Chart.yaml by scripts/verify_versions.sh. With the nightly workflow shipping unreleased main daily, a hardcoded version drifts further from reality every day. Wire the heading to the real version, nightly-aware: - src/version.ts reads NEXT_PUBLIC_APP_VERSION (nightly override) with a package.json fallback for release/local builds. - about-content.ts interpolates APP_VERSION into the heading; the "DeerFlow 1.0 and 2.0" milestone copy in acknowledgments is left as-is. - Dockerfile adds ARG APP_VERSION / ENV NEXT_PUBLIC_APP_VERSION in the builder stage so nightly CI can stamp the version. - nightly.yaml computes the nightly string once in prepare (<base>-nightly.<date>-<sha>, mirroring the chart scheme) and feeds it to both the frontend image build-arg and the chart publish (refactored to consume the shared output, so the displayed version and chart version can't drift). Release builds (container.yaml) are untouched and fall back to package.json. Tests cover the version branches and the heading interpolation; RELEASING.md notes the About page as a derived version consumer. * test(frontend): assert positive About-page version value Address review feedback on #4126: the unset-env assertion was negative-only (checked the stale "2.0" literal was gone), so it would still pass if APP_VERSION interpolated to "" or undefined. Mirror version.test.ts by asserting the heading carries the real resolved APP_VERSION. * ci(nightly): fail loudly on empty base version Address review feedback on #4126: the prepare step's BASE (parsed from Chart.yaml) had no empty-guard. A malformed/missing version would yield `-nightly.<date>-<sha>` (invalid semver) that now flows into both the chart publish and the frontend About-page version. Add `set -euo pipefail` plus a `test -n "$BASE"` guard mirroring publish-chart. `pipefail` matters - without it the pipeline's exit code is awk's, so `set -e` alone wouldn't catch a grep miss.
This commit is contained in:
@@ -42,12 +42,37 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
date: ${{ steps.date.outputs.date }}
|
||||
nightly_version: ${{ steps.nightly_version.outputs.nightly_version }}
|
||||
steps:
|
||||
- name: Set nightly date (UTC)
|
||||
id: date
|
||||
# Shared by build-images (image tag) and publish-chart (chart version)
|
||||
# so the two stay in sync for a given run.
|
||||
run: echo "date=$(date -u +%Y%m%d)" >> "$GITHUB_OUTPUT"
|
||||
- name: Checkout repository
|
||||
# Needed to read Chart.yaml's base version for the nightly version
|
||||
# string computed below.
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
- name: Compute nightly version
|
||||
id: nightly_version
|
||||
# Single source of truth for the nightly version string:
|
||||
# <base>-nightly.<YYYYMMDD>-<short_sha> (mirrors the chart's nightly
|
||||
# scheme). build-images injects it into the frontend image (About-page
|
||||
# version) and publish-chart stamps it on Chart.yaml, so the version
|
||||
# users see and the chart version can't drift apart.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BASE=$(grep -m1 '^version:' deploy/helm/deer-flow/Chart.yaml | awk '{print $2}')
|
||||
# A malformed/missing Chart.yaml version would yield an empty BASE ->
|
||||
# `-nightly.<date>-<sha>` (invalid semver) that now flows into both
|
||||
# the chart publish and the frontend About-page version. Fail loudly.
|
||||
# `pipefail` matters: without it the pipeline's exit code is awk's,
|
||||
# so `set -e` alone wouldn't catch a grep miss.
|
||||
test -n "$BASE" || { echo "::error::empty base version from Chart.yaml"; exit 1; }
|
||||
SHORT_SHA="${GITHUB_SHA::7}"
|
||||
NIGHTLY="${BASE}-nightly.${{ steps.date.outputs.date }}-${SHORT_SHA}"
|
||||
echo "nightly_version=${NIGHTLY}" >> "$GITHUB_OUTPUT"
|
||||
echo "Nightly version: ${NIGHTLY}"
|
||||
|
||||
validate-chart:
|
||||
if: github.repository == 'bytedance/deer-flow'
|
||||
@@ -132,7 +157,13 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: ${{ matrix.build-args }}
|
||||
# APP_VERSION is consumed only by the frontend Dockerfile (it stamps
|
||||
# the About-page version); backend/provisioner Dockerfiles don't
|
||||
# declare it. Passed via build-arg because build-push-action doesn't
|
||||
# forward host env into the BuildKit build.
|
||||
build-args: |
|
||||
${{ matrix.build-args }}
|
||||
${{ matrix.component == 'frontend' && format('APP_VERSION={0}', needs.prepare.outputs.nightly_version) || '' }}
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be #v2.4.0
|
||||
with:
|
||||
@@ -151,25 +182,25 @@ jobs:
|
||||
contents: read
|
||||
packages: write
|
||||
env:
|
||||
DATE: ${{ needs.prepare.outputs.date }}
|
||||
OWNER: ${{ github.repository_owner }}
|
||||
NIGHTLY: ${{ needs.prepare.outputs.nightly_version }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
|
||||
- name: Patch chart to a nightly version + nightly image defaults
|
||||
# Bumps Chart.yaml version/appVersion to <base>-nightly.<date>-<sha>
|
||||
# (valid semver prerelease; the short SHA makes each dispatch's version
|
||||
# unique, so a same-day re-dispatch re-publishes cleanly - OCI chart
|
||||
# versions are immutable and otherwise can't be overwritten). Repoints
|
||||
# the chart's default image registry/tag at the nightly build. Patches
|
||||
# are in-workflow only - nothing is committed back.
|
||||
# Bumps Chart.yaml version/appVersion to the nightly version computed in
|
||||
# the prepare job (<base>-nightly.<date>-<sha>, a valid semver
|
||||
# prerelease; the short SHA makes each dispatch's version unique, so a
|
||||
# same-day re-dispatch re-publishes cleanly - OCI chart versions are
|
||||
# immutable and otherwise can't be overwritten). The same string is
|
||||
# injected into the frontend image, so the About-page version and the
|
||||
# chart version match. Repoints the chart's default image registry/tag
|
||||
# at the nightly build. Patches are in-workflow only - nothing is
|
||||
# committed back.
|
||||
run: |
|
||||
set -eu
|
||||
CHART=deploy/helm/deer-flow
|
||||
BASE=$(grep -m1 '^version:' "$CHART/Chart.yaml" | awk '{print $2}')
|
||||
SHORT_SHA="${GITHUB_SHA::7}"
|
||||
NIGHTLY="${BASE}-nightly.${DATE}-${SHORT_SHA}"
|
||||
echo "Nightly chart version: ${NIGHTLY}"
|
||||
sed -i "s|^version:.*|version: ${NIGHTLY}|" "$CHART/Chart.yaml"
|
||||
sed -i "s|^appVersion:.*|appVersion: \"${NIGHTLY}\"|" "$CHART/Chart.yaml"
|
||||
|
||||
@@ -23,6 +23,13 @@ Container images are tagged from the git tag (not from these files), and the
|
||||
Helm chart version is validated against the tag — so if any source lags the
|
||||
tag, the release is blocked (see [Version gate](#version-gate)).
|
||||
|
||||
The frontend's in-app About page (Settings ▸ About) is a *derived* consumer, not
|
||||
a fifth source: it reads `frontend/package.json`'s version at build time, so it
|
||||
tracks the table above automatically with no bump needed. Nightly builds override
|
||||
it with the chart's nightly string (`<base>-nightly.<YYYYMMDD>-<short_sha>`) via
|
||||
the `APP_VERSION` build-arg in `nightly.yaml`, so a nightly image's About page
|
||||
distinguishes it from a release.
|
||||
|
||||
## Helper scripts
|
||||
|
||||
- `scripts/bump_version.sh <version>` — set all four fields at once, then
|
||||
|
||||
@@ -32,6 +32,11 @@ EXPOSE 3000
|
||||
FROM base AS builder
|
||||
RUN cd /app/frontend && pnpm install --frozen-lockfile
|
||||
# Skip env validation — runtime vars are injected by nginx/container
|
||||
# App version stamp for the About page. Nightly CI passes
|
||||
# APP_VERSION=<base>-nightly.<date>-<sha>; release/local builds leave it empty
|
||||
# and the frontend falls back to package.json's version.
|
||||
ARG APP_VERSION=""
|
||||
ENV NEXT_PUBLIC_APP_VERSION=$APP_VERSION
|
||||
RUN cd /app/frontend && SKIP_ENV_VALIDATION=1 pnpm build
|
||||
|
||||
# ── Prod: minimal runtime with pre-built output ───────────────────────────────
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
* About DeerFlow markdown content. Inlined to avoid raw-loader dependency
|
||||
* (Turbopack cannot resolve raw-loader for .md imports).
|
||||
*/
|
||||
export const aboutMarkdown = `# 🦌 [About DeerFlow 2.0](https://github.com/bytedance/deer-flow)
|
||||
import { APP_VERSION } from "@/version";
|
||||
|
||||
export const aboutMarkdown = `# 🦌 [About DeerFlow ${APP_VERSION}](https://github.com/bytedance/deer-flow)
|
||||
|
||||
> **From Open Source, Back to Open Source**
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* App version shown in the UI (About page, etc.). Prefers the build-time
|
||||
* NEXT_PUBLIC_APP_VERSION, which nightly CI sets to
|
||||
* `<base>-nightly.<YYYYMMDD>-<short_sha>` (matching the Helm chart's nightly
|
||||
* scheme). Falls back to package.json's version for local dev and tag-driven
|
||||
* release builds, where package.json is verified to match the git tag
|
||||
* (see scripts/verify_versions.sh + .github/workflows/verify-versions.yml).
|
||||
*/
|
||||
import pkg from "../package.json";
|
||||
|
||||
// `||` (not `??`) is intentional: the frontend Dockerfile sets
|
||||
// NEXT_PUBLIC_APP_VERSION="" for release/local builds, and that empty string
|
||||
// must fall through to pkg.version just like an unset var. `??` would keep "".
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
export const APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION || pkg.version;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { afterEach, expect, test, rs } from "@rstest/core";
|
||||
|
||||
const original = process.env.NEXT_PUBLIC_APP_VERSION;
|
||||
|
||||
afterEach(() => {
|
||||
rs.resetModules();
|
||||
if (original === undefined) {
|
||||
delete process.env.NEXT_PUBLIC_APP_VERSION;
|
||||
} else {
|
||||
process.env.NEXT_PUBLIC_APP_VERSION = original;
|
||||
}
|
||||
});
|
||||
|
||||
test("aboutMarkdown heading interpolates the app version", async () => {
|
||||
process.env.NEXT_PUBLIC_APP_VERSION = "9.9.9-test";
|
||||
const { aboutMarkdown } =
|
||||
await import("@/components/workspace/settings/about-content");
|
||||
// The heading link text carries the version stamp.
|
||||
expect(aboutMarkdown).toContain("[About DeerFlow 9.9.9-test]");
|
||||
// Milestone copy in the acknowledgments refers to the 1.0/2.0 product
|
||||
// generations and must NOT be parameterized.
|
||||
expect(aboutMarkdown).toContain("DeerFlow 1.0 and 2.0");
|
||||
});
|
||||
|
||||
test("aboutMarkdown heading reflects the package version when env is unset", async () => {
|
||||
delete process.env.NEXT_PUBLIC_APP_VERSION;
|
||||
const { APP_VERSION } = await import("@/version");
|
||||
const { aboutMarkdown } =
|
||||
await import("@/components/workspace/settings/about-content");
|
||||
// Positive: the heading carries the real resolved version. This catches an
|
||||
// empty or undefined APP_VERSION interpolation (`About DeerFlow ]` /
|
||||
// `About DeerFlow undefined]`), not just removal of the old literal.
|
||||
expect(aboutMarkdown).toContain(`[About DeerFlow ${APP_VERSION}]`);
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { afterEach, expect, test, rs } from "@rstest/core";
|
||||
|
||||
import pkg from "../../package.json";
|
||||
|
||||
const original = process.env.NEXT_PUBLIC_APP_VERSION;
|
||||
|
||||
afterEach(() => {
|
||||
rs.resetModules();
|
||||
if (original === undefined) {
|
||||
delete process.env.NEXT_PUBLIC_APP_VERSION;
|
||||
} else {
|
||||
process.env.NEXT_PUBLIC_APP_VERSION = original;
|
||||
}
|
||||
});
|
||||
|
||||
test("APP_VERSION uses NEXT_PUBLIC_APP_VERSION when set (nightly build-arg)", async () => {
|
||||
process.env.NEXT_PUBLIC_APP_VERSION = "2.1.0-nightly.20260712-abc1234";
|
||||
const { APP_VERSION } = await import("@/version");
|
||||
expect(APP_VERSION).toBe("2.1.0-nightly.20260712-abc1234");
|
||||
});
|
||||
|
||||
test("APP_VERSION falls back to package.json version when env is unset (local dev)", async () => {
|
||||
delete process.env.NEXT_PUBLIC_APP_VERSION;
|
||||
const { APP_VERSION } = await import("@/version");
|
||||
expect(APP_VERSION).toBe(pkg.version);
|
||||
});
|
||||
|
||||
test("APP_VERSION treats an empty NEXT_PUBLIC_APP_VERSION as unset (release Docker build)", async () => {
|
||||
// The frontend Dockerfile sets ENV NEXT_PUBLIC_APP_VERSION="" when nightly CI
|
||||
// doesn't pass APP_VERSION; the empty string must fall through to the
|
||||
// package.json version just like an unset var.
|
||||
process.env.NEXT_PUBLIC_APP_VERSION = "";
|
||||
const { APP_VERSION } = await import("@/version");
|
||||
expect(APP_VERSION).toBe(pkg.version);
|
||||
});
|
||||
Reference in New Issue
Block a user