diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml
index cbcd3568..848ffa9f 100644
--- a/.github/workflows/release-build.yml
+++ b/.github/workflows/release-build.yml
@@ -28,6 +28,9 @@ jobs:
ref: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.release-tag }}
- name: Build release assets
uses: ./.github/actions/build-release-assets
+ env:
+ # The footer shows the theme's version.
+ THEME_VERSION: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.release-tag }}
- name: Upload assets to release
env:
GH_TOKEN: ${{ github.token }}
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index a379df71..535e95b6 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -177,6 +177,13 @@ only. Templates therefore link assets without a version, and the assembly adds o
the content hash of the file it loads, the theme link in `base/head_style.tmpl` gets the hash of all theme files, and
each auto theme's `@import` gets the hash of the imported file.
+### Theme version
+
+The footer in `base/footer_content.tmpl` links the theme's repository with its version. The template declares
+`{{$themeVersion := "dev"}}` and the assembly replaces the value: with `THEME_VERSION`, which the release build sets to
+the release tag, or else with `git describe --tags`, such as `1.4.0-3-g54b4d73` for a build three commits after 1.4.0.
+Admins hide the credit with `--custom-footer-credit: none`.
+
## Local preview
`dev/docker-compose.yml` runs the Forgejo release pinned in `package.json` (`17.0.1-rootless`) and mounts
diff --git a/README.md b/README.md
index 15a759b5..c05c6579 100644
--- a/README.md
+++ b/README.md
@@ -236,6 +236,7 @@ Add the following code at the beginning or end of the theme's CSS file
| --custom-clone-menu-width | Clone button menu width | Gitea | 332px | 200px | 150px | 400px |
| --custom-user-menu-width | User menu width | 192px | 200px | | Gitea | 320px |
| --custom-owner-switcher | Owner switcher in the navbar; `none` hides it | flex | flex | | | |
+| --custom-footer-credit | Theme name and version in the footer; `none` hides it | inline | inline | | | |
| --custom-explore-repolist-columns | Number of repository list columns on explore page | 2 | 2 | 2 | | |
| --custom-explore-userlist-columns | Number of user/organization list columns on explore page | 3 | 1 | 2/3 | | |
| --custom-user-repolist-columns | Number of repository list columns on user page | 2 | 2 | 1/2 | | |
diff --git a/scripts/assembleForgejo.ts b/scripts/assembleForgejo.ts
index 7c67543c..eac84840 100644
--- a/scripts/assembleForgejo.ts
+++ b/scripts/assembleForgejo.ts
@@ -1,3 +1,4 @@
+import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { cpSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
@@ -102,3 +103,27 @@ const stampAssetLinks = (directory: string): void => {
}
};
stampAssetLinks(templates);
+
+// The footer credits the theme with its version: the release tag in the release build, otherwise the nearest tag as
+// git describes it, such as 1.4.0-3-g54b4d73 for a build three commits after 1.4.0.
+const describeVersion = (): string => {
+ try {
+ return execFileSync("git", ["describe", "--tags", "--match", "[0-9]*", "--always", "--dirty"], {
+ cwd: root,
+ encoding: "utf-8",
+ stdio: ["ignore", "pipe", "ignore"],
+ }).trim();
+ } catch {
+ return "dev";
+ }
+};
+const releaseVersion = (process.env.THEME_VERSION?.trim() || describeVersion()).replace(/^v/, "");
+if (!/^[\w.+-]+$/.test(releaseVersion)) throw new Error(`Unexpected theme version: ${releaseVersion}`);
+const footer = join(templates, "base", "footer_content.tmpl");
+const versionVariable = /\{\{\$themeVersion := "[^"]*"\}\}/;
+const footerSource = readFileSync(footer, "utf-8");
+if (!versionVariable.test(footerSource)) throw new Error(`No theme version to stamp in ${footer}`);
+writeFileSync(
+ footer,
+ footerSource.replace(versionVariable, () => `{{$themeVersion := "${releaseVersion}"}}`)
+);
diff --git a/src/build-output.test.ts b/src/build-output.test.ts
index e7c5fdd5..0f0979a7 100644
--- a/src/build-output.test.ts
+++ b/src/build-output.test.ts
@@ -514,6 +514,24 @@ describe("Forgejo 15 native integration", () => {
);
});
+ it("credits the theme with its version in the footer", () => {
+ const source = fs.readFileSync(path.join(ROOT_DIR, "templates", "base", "footer_content.tmpl"), "utf-8");
+ const footer = fs.readFileSync(path.join(DIST_DIR, "forgejo", "templates", "base", "footer_content.tmpl"), "utf-8");
+
+ // The link sits with Forgejo's own footer links and shows only the theme's name and version.
+ expect(source).toContain('{{$themeVersion := "dev"}}');
+ expect(source.indexOf('class="footer-theme"')).toBeGreaterThan(source.indexOf("/api/swagger"));
+ expect(source.indexOf('class="footer-theme"')).toBeLessThan(source.indexOf('"custom/extra_links_footer"'));
+ expect(source).toContain(
+ ''
+ );
+ // The assembly stamps the release tag or git's description of the build.
+ expect(footer).toMatch(/\{\{\$themeVersion := "(?!dev")[\w.+-]+"\}\}/);
+ // Admins can hide it with a custom variable; it shows by default.
+ const css = fs.readFileSync(path.join(DIST_DIR, `${PREFIX}light.css`), "utf-8");
+ expect(css).toContain(".github-footer .footer-theme{display:var(--custom-footer-credit,inline)}");
+ });
+
it("ships the mobile drawer assets and keeps its toggle independent of Forgejo's inline menu", () => {
const navbar = fs.readFileSync(path.join(ROOT_DIR, "templates", "base", "head_navbar.tmpl"), "utf-8");
expect(navbar).toContain('id="navbar-drawer-toggle" aria-controls="navbar-drawer" aria-haspopup="dialog"');
diff --git a/src/types/vars.ts b/src/types/vars.ts
index 46de8c07..2e4e861a 100644
--- a/src/types/vars.ts
+++ b/src/types/vars.ts
@@ -61,6 +61,7 @@ const customVars = {
cloneMenuWidth: "clone-menu-width",
userMenuWidth: "user-menu-width",
ownerSwitcher: "owner-switcher",
+ footerCredit: "footer-credit",
explore: { repolistColumns: "explore-repolist-columns", userlistColumns: "explore-userlist-columns" },
userRepolistColumns: "user-repolist-columns",
org: { repolistColumns: "org-repolist-columns", userlistColumns: "org-userlist-columns" },
diff --git a/styles/components/footer.ts b/styles/components/footer.ts
index 184f43c8..bfb1c8d3 100644
--- a/styles/components/footer.ts
+++ b/styles/components/footer.ts
@@ -17,7 +17,8 @@
* limitations under the License.
*/
-import { css, cssCombine, themeVars } from "@lutinglt/gitea-github-theme/core";
+import { css, cssCombine, customThemeVars, themeVars } from "@lutinglt/gitea-github-theme/core";
+import { fallbackVar } from "@vanilla-extract/css";
const footer = css`
.page-footer {
@@ -67,4 +68,12 @@ const footer = css`
}
`;
-export default cssCombine(footer);
+const themeCredit = css`
+ .github-footer .footer-theme {
+ /* Admins can hide the theme's credit with
+ --custom-footer-credit: none. */
+ display: ${fallbackVar(customThemeVars.footerCredit, "inline")};
+ }
+`;
+
+export default cssCombine(footer, themeCredit);
diff --git a/templates/base/footer_content.tmpl b/templates/base/footer_content.tmpl
index 449db520..8d27cb0e 100644
--- a/templates/base/footer_content.tmpl
+++ b/templates/base/footer_content.tmpl
@@ -1,4 +1,6 @@
{{/* Keep Forgejo's native footer settings, language menu, and custom links. */}}
+{{/* scripts/assembleForgejo.ts stamps the theme's version here. */}}
+{{$themeVersion := "dev"}}