Skip to content

🎨 Added separate Server and Admin build versions to About dialog#26769

Merged
cmraible merged 1 commit intomainfrom
feat/build-version-display
Mar 11, 2026
Merged

🎨 Added separate Server and Admin build versions to About dialog#26769
cmraible merged 1 commit intomainfrom
feat/build-version-display

Conversation

@rob-ghost
Copy link
Contributor

On Ghost(Pro), the About dialog currently shows a single version string that doesn't distinguish between the server container and the admin client — both ship independently and can be at different commits. This makes it harder to debug issues or verify which code is actually running.

This change introduces two new optional build-time inputs: GHOST_BUILD_VERSION (a Docker build arg baked as an ENV var, read by the public-config endpoint at runtime) and VITE_ADMIN_BUILD_VERSION (a Vite define constant baked into the admin bundle). When both are set — as they will be on Ghost(Pro) CI builds — the About dialog shows separate "Server" and "Admin" lines, each linking to their respective GitHub commit. When neither is set (self-hosted installs, tagged releases), the dialog continues to show a single "Version" line pointing at the release tag, identical to today's behavior.

The link-to-github-releases utility was extended to handle the new semver+epoch.sha (server) and semver+sha (admin) formats, alongside the existing legacy +moya and git-describe formats. New test cases cover all format variations.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 11, 2026

Walkthrough

CI now computes a build version from ghost/core/package.json and the commit SHA for non-tag builds, exporting it as GHOST_BUILD_VERSION and passing it into Docker builds as a build-arg. Dockerfile exposes GHOST_BUILD_VERSION into the runtime environment. Server public-config prefers GHOST_BUILD_VERSION when set. Vite exposes GHOST_BUILD_VERSION to the admin bundle. The admin About UI shows Server and Admin versions when available. The GitHub releases link utility gained parsing for build-metadata formats (epoch.sha, sha, full SHA) and tests were added to cover these cases.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change: introducing separate Server and Admin build versions to the About dialog, which is the core feature described in the PR.
Description check ✅ Passed The description comprehensively explains the purpose, implementation details, and behavior of the changes across multiple files, directly related to the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/build-version-display

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@rob-ghost rob-ghost marked this pull request as ready for review March 11, 2026 00:11
@rob-ghost rob-ghost requested a review from cmraible March 11, 2026 00:11
@rob-ghost rob-ghost added the ok to merge for me You can merge this on my behalf if you want. label Mar 11, 2026
@rob-ghost rob-ghost self-assigned this Mar 11, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/admin-x-settings/test/unit/utils/link-to-github-releases.test.ts (1)

35-49: Cover the non-SHA build-metadata fallback as well.

These additions pin the new positive SHA cases, but the utility now also has a new fallback path for build metadata that is neither moya nor a trailing hex SHA. One regression test like 6.21.2+build.1 -> releases/tag/v6.21.2 would protect the tagged/self-hosted path too.

💡 Suggested test
     it('handles full 40-char sha in build metadata', function () {
         const sha = 'a'.repeat(40);
         const link = linkToGitHubReleases(`6.21.2+1710072000.${sha}`);
         assert.equal(link, `https://github.com/TryGhost/Ghost/commit/${sha}`);
     });
+
+    it('falls back to the release tag for non-SHA build metadata', function () {
+        const link = linkToGitHubReleases('6.21.2+build.1');
+        assert.equal(link, 'https://github.com/TryGhost/Ghost/releases/tag/v6.21.2');
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/admin-x-settings/test/unit/utils/link-to-github-releases.test.ts` around
lines 35 - 49, Add a regression test for linkToGitHubReleases that covers the
non-SHA build-metadata fallback path: call linkToGitHubReleases with a version
like "6.21.2+build.1" (or similar non-hex metadata) and assert it returns the
releases/tag/v6.21.2 URL; update the test suite in
link-to-github-releases.test.ts to include this case alongside the existing
SHA-positive tests so the tagged/self-hosted fallback in linkToGitHubReleases is
verified.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/admin-x-settings/test/unit/utils/link-to-github-releases.test.ts`:
- Around line 35-49: Add a regression test for linkToGitHubReleases that covers
the non-SHA build-metadata fallback path: call linkToGitHubReleases with a
version like "6.21.2+build.1" (or similar non-hex metadata) and assert it
returns the releases/tag/v6.21.2 URL; update the test suite in
link-to-github-releases.test.ts to include this case alongside the existing
SHA-positive tests so the tagged/self-hosted fallback in linkToGitHubReleases is
verified.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7ad4adbb-edd2-4e06-b35b-156467261049

📥 Commits

Reviewing files that changed from the base of the PR and between d385641 and 43e812a.

📒 Files selected for processing (7)
  • .github/workflows/ci.yml
  • Dockerfile.production
  • apps/admin-x-framework/src/vite.ts
  • apps/admin-x-settings/src/components/settings/general/about.tsx
  • apps/admin-x-settings/src/utils/link-to-github-releases.ts
  • apps/admin-x-settings/test/unit/utils/link-to-github-releases.test.ts
  • ghost/core/core/server/services/public-config/config.js

@codecov
Copy link

codecov bot commented Mar 11, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.19%. Comparing base (8c5e239) to head (fff1feb).
⚠️ Report is 15 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #26769      +/-   ##
==========================================
- Coverage   73.20%   73.19%   -0.02%     
==========================================
  Files        1534     1534              
  Lines      120966   121014      +48     
  Branches    14630    14627       -3     
==========================================
+ Hits        88559    88571      +12     
- Misses      31393    31414      +21     
- Partials     1014     1029      +15     
Flag Coverage Δ
admin-tests 54.34% <ø> (+0.02%) ⬆️
e2e-tests 73.19% <100.00%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ErisDS
Copy link
Member

ErisDS commented Mar 11, 2026

🤖 Velo CI Failure Analysis

Classification: 🟠 SOFT FAIL

  • Workflow: CI
  • Failed Step: Run yarn nx run @tryghost/admin-x-settings:test:acceptance
  • Run: View failed run
    What failed: CI failure - likely code issue
    Why: The failure appears to be related to code changes. Check the error output for details.
    Action:
    Review the error logs and fix the issue in your code.

@rob-ghost rob-ghost force-pushed the feat/build-version-display branch from 43e812a to ff57916 Compare March 11, 2026 10:00
On Ghost(Pro), where CI sets GHOST_BUILD_VERSION and
VITE_ADMIN_BUILD_VERSION, the About dialog now shows two lines — Server
and Admin — each linking to their respective commits on GitHub. For
self-hosters (or tagged releases where neither env var is set), the
dialog continues to show a single "Version" line exactly as before.

The server version is injected via a Docker build arg baked into the
container as an ENV var, read at runtime by the public-config endpoint.
The admin version is baked into the Vite bundle at build time via a
define constant. The link-to-github-releases utility was updated to
handle the new semver+epoch.sha and semver+sha formats alongside the
existing legacy formats.
@rob-ghost rob-ghost force-pushed the feat/build-version-display branch from ff57916 to fff1feb Compare March 11, 2026 10:05
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/admin-x-settings/src/utils/link-to-github-releases.ts`:
- Around line 33-53: The fallback to a releases/tag URL must only be used for
valid semver strings: in link-to-github-releases.ts, after computing
versionWithoutBuild and before returning the tag URL, call
semverParse(versionWithoutBuild, {includePrerelease: true}) (same helper used
earlier) and if it returns null/undefined, return '' instead of constructing
`https://github.com/TryGhost/Ghost/releases/tag/v${versionWithoutBuild}`; keep
the existing prerelease/commit-hash logic (the current try block with
semverParse and prerelease handling) but add this explicit semver validity guard
so raw SHAs or non-semver values produce an empty string.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3f2c7e0b-c020-40b9-9798-f78da64f50bd

📥 Commits

Reviewing files that changed from the base of the PR and between 43e812a and ff57916.

📒 Files selected for processing (7)
  • .github/workflows/ci.yml
  • Dockerfile.production
  • apps/admin-x-framework/src/vite.ts
  • apps/admin-x-settings/src/components/settings/general/about.tsx
  • apps/admin-x-settings/src/utils/link-to-github-releases.ts
  • apps/admin-x-settings/test/unit/utils/link-to-github-releases.test.ts
  • ghost/core/core/server/services/public-config/config.js
🚧 Files skipped from review as they are similar to previous changes (6)
  • apps/admin-x-settings/src/components/settings/general/about.tsx
  • apps/admin-x-settings/test/unit/utils/link-to-github-releases.test.ts
  • .github/workflows/ci.yml
  • Dockerfile.production
  • apps/admin-x-framework/src/vite.ts
  • ghost/core/core/server/services/public-config/config.js

Comment on lines 33 to +53
try {
const semverVersion = semverParse(cleanedVersion, {includePrerelease: true} as any);
const semverVersion = semverParse(versionWithoutBuild, {includePrerelease: true} as any);
const prerelease = semverVersion?.prerelease;

if (prerelease && prerelease?.length > 0) {
if (prerelease && prerelease.length > 0) {
const splitPrerelease = String(prerelease[0]).split('-');
const commitHash = splitPrerelease[1];

if (!commitHash || !commitHash.startsWith('g')) {
return '';
if (commitHash?.startsWith('g')) {
return `https://github.com/TryGhost/Ghost/commit/${commitHash.slice(1)}`;
}

const commitHashWithoutG = commitHash.slice(1);

return `https://github.com/TryGhost/Ghost/commit/${commitHashWithoutG}`;
// Has pre-release but no recognizable commit hash
return '';
}

return `https://github.com/TryGhost/Ghost/releases/tag/v${cleanedVersion}`;
} catch {
return '';
}

// Plain semver (with or without +moya) → release tag
return `https://github.com/TryGhost/Ghost/releases/tag/v${versionWithoutBuild}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard the release-tag fallback for unsupported version strings.

apps/admin-x-framework/src/vite.ts:47-51 passes GHOST_BUILD_VERSION through verbatim, and apps/admin-x-settings/src/components/settings/general/about.tsx:9-17 will render any non-empty URL this helper returns. With the current fallback, a raw SHA or other non-semver value still becomes releases/tag/v..., which gives the About dialog a broken link instead of plain text. Return '' when versionWithoutBuild is not a valid semver before constructing the tag URL.

🔧 Proposed fix
     try {
         const semverVersion = semverParse(versionWithoutBuild, {includePrerelease: true} as any);
-        const prerelease = semverVersion?.prerelease;
+        if (!semverVersion) {
+            return '';
+        }
+
+        const prerelease = semverVersion.prerelease;
 
         if (prerelease && prerelease.length > 0) {
             const splitPrerelease = String(prerelease[0]).split('-');
             const commitHash = splitPrerelease[1];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const semverVersion = semverParse(cleanedVersion, {includePrerelease: true} as any);
const semverVersion = semverParse(versionWithoutBuild, {includePrerelease: true} as any);
const prerelease = semverVersion?.prerelease;
if (prerelease && prerelease?.length > 0) {
if (prerelease && prerelease.length > 0) {
const splitPrerelease = String(prerelease[0]).split('-');
const commitHash = splitPrerelease[1];
if (!commitHash || !commitHash.startsWith('g')) {
return '';
if (commitHash?.startsWith('g')) {
return `https://github.com/TryGhost/Ghost/commit/${commitHash.slice(1)}`;
}
const commitHashWithoutG = commitHash.slice(1);
return `https://github.com/TryGhost/Ghost/commit/${commitHashWithoutG}`;
// Has pre-release but no recognizable commit hash
return '';
}
return `https://github.com/TryGhost/Ghost/releases/tag/v${cleanedVersion}`;
} catch {
return '';
}
// Plain semver (with or without +moya) → release tag
return `https://github.com/TryGhost/Ghost/releases/tag/v${versionWithoutBuild}`;
try {
const semverVersion = semverParse(versionWithoutBuild, {includePrerelease: true} as any);
if (!semverVersion) {
return '';
}
const prerelease = semverVersion.prerelease;
if (prerelease && prerelease.length > 0) {
const splitPrerelease = String(prerelease[0]).split('-');
const commitHash = splitPrerelease[1];
if (commitHash?.startsWith('g')) {
return `https://github.com/TryGhost/Ghost/commit/${commitHash.slice(1)}`;
}
// Has pre-release but no recognizable commit hash
return '';
}
} catch {
return '';
}
// Plain semver (with or without +moya) → release tag
return `https://github.com/TryGhost/Ghost/releases/tag/v${versionWithoutBuild}`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/admin-x-settings/src/utils/link-to-github-releases.ts` around lines 33 -
53, The fallback to a releases/tag URL must only be used for valid semver
strings: in link-to-github-releases.ts, after computing versionWithoutBuild and
before returning the tag URL, call semverParse(versionWithoutBuild,
{includePrerelease: true}) (same helper used earlier) and if it returns
null/undefined, return '' instead of constructing
`https://github.com/TryGhost/Ghost/releases/tag/v${versionWithoutBuild}`; keep
the existing prerelease/commit-hash logic (the current try block with
semverParse and prerelease handling) but add this explicit semver validity guard
so raw SHAs or non-semver values produce an empty string.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ghost/core/test/unit/server/services/public-config/config.test.js`:
- Around line 48-65: The tests set process.env.GHOST_BUILD_VERSION inside the it
blocks but manually delete it at the end of each test, which is fragile; add
cleanup in the existing afterEach hook to delete process.env.GHOST_BUILD_VERSION
so the environment is reset even when a test fails, then remove the manual
delete lines from the two tests that call getConfigProperties() (the "should
return GHOST_BUILD_VERSION as version when set" and "should return package
version when GHOST_BUILD_VERSION is not set" tests) so they rely on the
centralized afterEach cleanup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3418adb2-00a3-4b46-bdb7-8ba3823e768e

📥 Commits

Reviewing files that changed from the base of the PR and between ff57916 and fff1feb.

📒 Files selected for processing (8)
  • .github/workflows/ci.yml
  • Dockerfile.production
  • apps/admin-x-framework/src/vite.ts
  • apps/admin-x-settings/src/components/settings/general/about.tsx
  • apps/admin-x-settings/src/utils/link-to-github-releases.ts
  • apps/admin-x-settings/test/unit/utils/link-to-github-releases.test.ts
  • ghost/core/core/server/services/public-config/config.js
  • ghost/core/test/unit/server/services/public-config/config.test.js
🚧 Files skipped from review as they are similar to previous changes (4)
  • ghost/core/core/server/services/public-config/config.js
  • Dockerfile.production
  • apps/admin-x-framework/src/vite.ts
  • apps/admin-x-settings/src/utils/link-to-github-releases.ts

Comment on lines +48 to +65
it('should return GHOST_BUILD_VERSION as version when set', function () {
process.env.GHOST_BUILD_VERSION = '6.21.2+abc1234';

const configProperties = getConfigProperties();

assert.equal(configProperties.version, '6.21.2+abc1234');

delete process.env.GHOST_BUILD_VERSION;
});

it('should return package version when GHOST_BUILD_VERSION is not set', function () {
delete process.env.GHOST_BUILD_VERSION;

const configProperties = getConfigProperties();

assert.match(configProperties.version, /^\d+\.\d+\.\d+/);
assert.notEqual(configProperties.version, '6.21.2+abc1234');
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Improve test isolation by cleaning up env var in afterEach.

The current pattern of manually deleting process.env.GHOST_BUILD_VERSION at the end of the test is fragile. If the test fails before reaching the cleanup line, the env var remains set and could pollute subsequent tests.

Consider moving the cleanup to the existing afterEach hook to ensure proper isolation regardless of test outcome:

Proposed fix
         afterEach(async function () {
             await configUtils.restore();
             sinon.restore();
+            delete process.env.GHOST_BUILD_VERSION;
         });

Then simplify the tests:

         it('should return GHOST_BUILD_VERSION as version when set', function () {
             process.env.GHOST_BUILD_VERSION = '6.21.2+abc1234';
 
             const configProperties = getConfigProperties();
 
             assert.equal(configProperties.version, '6.21.2+abc1234');
-
-            delete process.env.GHOST_BUILD_VERSION;
         });
 
         it('should return package version when GHOST_BUILD_VERSION is not set', function () {
-            delete process.env.GHOST_BUILD_VERSION;
-
             const configProperties = getConfigProperties();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/core/test/unit/server/services/public-config/config.test.js` around
lines 48 - 65, The tests set process.env.GHOST_BUILD_VERSION inside the it
blocks but manually delete it at the end of each test, which is fragile; add
cleanup in the existing afterEach hook to delete process.env.GHOST_BUILD_VERSION
so the environment is reset even when a test fails, then remove the manual
delete lines from the two tests that call getConfigProperties() (the "should
return GHOST_BUILD_VERSION as version when set" and "should return package
version when GHOST_BUILD_VERSION is not set" tests) so they rely on the
centralized afterEach cleanup.

Copy link
Collaborator

@cmraible cmraible left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice one, thanks!

@cmraible cmraible merged commit e7505d9 into main Mar 11, 2026
32 checks passed
@cmraible cmraible deleted the feat/build-version-display branch March 11, 2026 16:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ok to merge for me You can merge this on my behalf if you want.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants