taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 2def7e71ddfe87b816f9c71644b0478078c6796c
parent d869ee4aa1ce74f3db817f70dc45008f9f1b7423
Author: Florian Dold <dold@taler.net>
Date:   Sun,  6 Sep 2026 14:26:05 +0200

typescript-core: derive build versions from Git tags

Keep package manifests private at version 0.0.0 and resolve component
versions from the shared release tags. Preserve Git build metadata in
source archives and use the tag version for numeric browser manifests.

Remove package-version bump scripts, the NPM publish target, and the
unsupported Qtart CLI build script. Update the release instructions.

Diffstat:
M.gitignore | 1+
MMakefile | 13++++---------
MREADME | 16++++++++++++++++
MRELEASE.md | 26+++++++++++++++++---------
Mbuild-system/archive.py | 19++++++++++++++++++-
Abuild-system/version-cli.mjs | 17+++++++++++++++++
Abuild-system/version.d.mts | 23+++++++++++++++++++++++
Abuild-system/version.mjs | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcontrib/bump-taler-version.mjs | 41-----------------------------------------
Dcontrib/next-dev-version.sh | 21---------------------
Mpackage.json | 1+
Mpackages/anastasis-cli/build-node.mjs | 32+++++++-------------------------
Mpackages/anastasis-cli/package.json | 3++-
Mpackages/anastasis-core/package.json | 3++-
Mpackages/anastasis-webui/package.json | 2+-
Mpackages/challenger-webui/package.json | 2+-
Mpackages/idb-bridge/package.json | 4++--
Mpackages/libeufin-bank-webui/package.json | 2+-
Mpackages/pogen/example/proj1/package.json | 3++-
Mpackages/pogen/package.json | 3++-
Mpackages/qa-tooling/package.json | 2+-
Apackages/qa-tooling/test/build-version.test.mjs | 259+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-auditor-webui/package.json | 2+-
Mpackages/taler-exchange-aml-webui/package.json | 2+-
Mpackages/taler-exchange-kyc-webui/package.json | 2+-
Mpackages/taler-harness/build.mjs | 35+++++++++--------------------------
Mpackages/taler-harness/package.json | 3++-
Mpackages/taler-merchant-webui/contrib/qa/version.test.mjs | 52++++++++++++++++++++--------------------------------
Mpackages/taler-merchant-webui/esbuild.config.mjs | 39+++++----------------------------------
Mpackages/taler-merchant-webui/package.json | 2+-
Mpackages/taler-util/package.json | 4++--
Mpackages/taler-wallet-cli/build-node.mjs | 36+++++++++---------------------------
Dpackages/taler-wallet-cli/build-qtart.mjs | 65-----------------------------------------------------------------
Mpackages/taler-wallet-cli/package.json | 3++-
Mpackages/taler-wallet-core/package.json | 3++-
Mpackages/taler-wallet-embedded/build.mjs | 36+++++++++---------------------------
Mpackages/taler-wallet-embedded/package.json | 3++-
Mpackages/wallet-webui/build.mjs | 9++++-----
Mpackages/wallet-webui/manifest.mjs | 7+++++--
Apackages/wallet-webui/pack-cli.mjs | 31+++++++++++++++++++++++++++++++
Mpackages/wallet-webui/pack.d.mts | 1+
Mpackages/wallet-webui/pack.mjs | 23++++-------------------
Mpackages/wallet-webui/package.json | 10+++++-----
Mpackages/wallet-webui/test/manifests.test.ts | 24+++++++++++++++++++-----
Mpackages/wallet-webui/test/packaging.test.ts | 16++++++++++++++++
Mpackages/web-util/build.mjs | 27++++-----------------------
Mpackages/web-util/package.json | 4++--
Mpackages/web-util/src/index.build.test.ts | 40+++++++++++++++++++++++++++++++++++++++-
Mpackages/web-util/src/index.build.ts | 24++++--------------------
49 files changed, 675 insertions(+), 419 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -15,6 +15,7 @@ test/ # GNU-style build system configure .config.mk +build-info.json # Editor files \#*\# diff --git a/Makefile b/Makefile @@ -1,6 +1,7 @@ # This Makefile has been placed in the public domain. archive = ./build-system/archive.py +version = $(shell node ./build-system/version-cli.mjs) configurable-packages = $(patsubst %/,%,$(dir $(wildcard packages/*/Makefile))) configure-files = configure $(addsuffix /configure,$(configurable-packages)) archive-includes = $(addprefix --include ,$(configure-files)) @@ -21,19 +22,13 @@ build: .PHONY: dist dist: $(archive) $(archive-includes) \ - taler-typescript-core-$(shell git describe --tags --abbrev=0 | sed -e 's/^v//').tar.gz + taler-typescript-core-$(version).tar.gz -# Create tarball with git hash prefix in name +# Compatibility target; both archive targets retain the full build version. .PHONY: dist-git dist-git: $(archive) $(archive-includes) \ - taler-typescript-core-$(shell git describe --tags).tar.gz - -.PHONY: publish -publish: - pnpm i -r --frozen-lockfile --filter '!@gnu-taler/qa-tooling' - pnpm run build - pnpm publish -r --no-git-checks + taler-typescript-core-$(version).tar.gz .PHONY: clean clean: diff --git a/README b/README @@ -44,6 +44,22 @@ The CLI version of the wallet supports the normal GNU installation process. If you are compiling the code from git, you have to run `./bootstrap` before running `./configure`. +### Build versions and source archives + +All package manifests use version `0.0.0` and are private. Build versions come +from the shared repository tags, with the leading `v` removed. Commits after a +tag include the distance and commit hash. Inspect the version with +`node build-system/version-cli.mjs` (or add `--json` for the tag and full hash). + +Git builds require a reachable release tag. Fetch missing tags with +`git fetch --tags`; shallow clones may also need `git fetch --unshallow`. + +`make dist` and `make dist-git` include generated `build-info.json` metadata in +the source archive. Builds from these archives retain the same version and +commit hash without requiring Git. Source copies missing both Git metadata and +`build-info.json` cannot provide a build version. The metadata is generated only +inside the archive and is not maintained or committed by hand. + ## Pushing a new prebuilt version After compiling run diff --git a/RELEASE.md b/RELEASE.md @@ -1,13 +1,21 @@ # GNU Taler Wallet WebExtension -1. Checkout the version on the GIT workspace -2. On the root of the project folder run `make webextension` it MUST complete successfully -3. Open both stores before starting any submission -4. You should have a new tag and a clean workspace and a TAG - a) Latest tag can be queried by: 'git tag --list --sort=-version:refname | head -n1' - b) Update version names: './contrib/bump-taler-version.mjs 1.0.31' - c) Commit changes and then tag with the version (check the v before the number): 'git tag v1.0.31' - d) Push with tags: 'git push --tags' +1. Check out the release commit and ensure the working tree is clean. +2. Create the shared release tag before building, for example `git tag v1.6.44`. + Development releases use tags such as `v1.6.45-dev.1`. +3. Run `make webextension` from the repository root; it MUST complete successfully. +4. Publish the release tag through the repository's usual release process. +5. Open both stores before starting any submission. + +Package manifests remain at version `0.0.0` and are private. No package-version +bump or version-bump commit is needed. All components derive their build version +from the nearest reachable `v…` tag. Builds after a tag retain the commit distance +and hash, for example `1.6.45-dev.1-12-gabcdef123`. + +Browser manifests keep the tag's numeric version: `1.6.44` or `1.6.45.1` for +`v1.6.45-dev.1`. The wallet UI, Chrome's `version_name`, and ZIP filenames retain +the full build version. Between-tag builds share the tag's numeric version; +create a new release tag for a browser-store update. `make webextension` runs the browser-specific validation and produces unsigned Chrome and Firefox store-submission ZIP files. Store upload and signing remain @@ -25,7 +33,7 @@ The Firefox Manifest V3 wallet needs to be uploaded. 4. Upload `$ROOT/packages/wallet-webui/dist/extension/taler-wallet-firefox-<version>.zip`. 5. You will be asked if the source is minified, you should answer "yes". 6. You will be asked to upload the source code. Run `make dist` -7. Upload the tar.gz file in the root folder. ($ROOT/taler-wallet-<version>.tar.gz) +7. Upload the tar.gz file in the root folder. ($ROOT/taler-typescript-core-<version>.tar.gz) 8. Complete the changelog. You can review that using `git log <current-version>..<next-version> --pretty=oneline --abbrev-commit --stat -- packages/wallet-webui/`. ## Google Chrome Web Store diff --git a/build-system/archive.py b/build-system/archive.py @@ -7,6 +7,8 @@ from __future__ import annotations import argparse +import io +import json import os from pathlib import Path import subprocess @@ -127,6 +129,12 @@ def main() -> int: output = args.output.resolve() prefix = archive_prefix(output) entries = repository_entries(root) + build_info = subprocess.run( + ["node", str(root / "build-system/version-cli.mjs"), "--json", str(root)], + check=True, + stdout=subprocess.PIPE, + ).stdout + version_metadata = json.loads(build_info) included: list[tuple[Path, Path]] = [] for name in args.include: @@ -142,7 +150,16 @@ def main() -> int: with tarfile.open(output, "w:gz") as archive: for source, relative in [*included, *entries]: + if relative == Path("build-info.json"): + continue archive.add(source, arcname=prefix / relative, recursive=False) + metadata = tarfile.TarInfo(str(prefix / "build-info.json")) + metadata.size = len(build_info) + metadata.mode = 0o644 + metadata.mtime = int( + run_git(root, "show", "-s", "--format=%ct", version_metadata["gitHash"]) + ) + archive.addfile(metadata, io.BytesIO(build_info)) print(f"created {output}") return 0 @@ -151,6 +168,6 @@ def main() -> int: if __name__ == "__main__": try: sys.exit(main()) - except (OSError, RuntimeError, ValueError) as exc: + except (OSError, RuntimeError, ValueError, subprocess.CalledProcessError) as exc: print(f"archive: {exc}", file=sys.stderr) sys.exit(1) diff --git a/build-system/version-cli.mjs b/build-system/version-cli.mjs @@ -0,0 +1,17 @@ +// This file has been placed in the public domain. + +import { getBuildInfo } from "./version.mjs"; + +try { + const args = process.argv.slice(2); + const json = args[0] === "--json"; + if (json) args.shift(); + if (args.length > 1 || args[0]?.startsWith("-")) { + throw Error("usage: version-cli.mjs [--json] [DIRECTORY]"); + } + const info = getBuildInfo(args[0]); + console.log(json ? JSON.stringify(info) : info.version); +} catch (error) { + console.error(error.message); + process.exitCode = 1; +} diff --git a/build-system/version.d.mts b/build-system/version.d.mts @@ -0,0 +1,23 @@ +/* + This file is part of GNU Taler + (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +export interface BuildInfo { + version: string; + tag: string; + gitHash: string; +} + +export function getBuildInfo(startDirectory?: string): BuildInfo; diff --git a/build-system/version.mjs b/build-system/version.mjs @@ -0,0 +1,98 @@ +/* + This file is part of GNU Taler + (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { execFileSync } from "node:child_process"; +import { existsSync, readFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; + +function repositoryRoot(startDirectory) { + let directory = resolve(startDirectory); + while (!existsSync(join(directory, "pnpm-workspace.yaml"))) { + const parent = dirname(directory); + if (parent === directory) { + throw Error( + `build version: cannot find the workspace from ${startDirectory}`, + ); + } + directory = parent; + } + return directory; +} + +function validateBuildInfo(info) { + if ( + !info || + typeof info.tag !== "string" || + !/^v[0-9][0-9A-Za-z.+-]*$/.test(info.tag) || + typeof info.version !== "string" || + typeof info.gitHash !== "string" || + !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(info.gitHash) + ) { + throw Error("invalid version, tag, or commit hash"); + } + const version = info.tag.slice(1); + if (info.version !== version) { + const suffix = info.version.slice(version.length); + const match = /^-[1-9][0-9]*-g([0-9a-f]+)$/.exec(suffix); + if ( + !info.version.startsWith(version) || + !match || + !info.gitHash.startsWith(match[1]) + ) { + throw Error("version does not match the tag and commit hash"); + } + } + return { version: info.version, tag: info.tag, gitHash: info.gitHash }; +} + +/** Resolve at invocation time, even when this helper is bundled into web-util. */ +export function getBuildInfo(startDirectory = process.cwd()) { + const root = repositoryRoot(startDirectory); + if (existsSync(join(root, ".git"))) { + const git = (args) => + execFileSync("git", args, { + cwd: root, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }).trim(); + try { + const description = git(["describe", "--tags", "--match", "v[0-9]*"]); + return validateBuildInfo({ + version: description.slice(1), + tag: description.replace(/-[0-9]+-g[0-9a-f]+$/, ""), + gitHash: git(["rev-parse", "HEAD"]), + }); + } catch (cause) { + throw Error( + "build version: cannot describe this checkout; ensure Git is installed and fetch the release tags and history (git fetch --tags; git fetch --unshallow for a shallow clone)", + { cause }, + ); + } + } + + // An extracted archive can live inside another repository. Never use that + // enclosing repository's tags, or a package.json placeholder, as its version. + try { + return validateBuildInfo( + JSON.parse(readFileSync(join(root, "build-info.json"), "utf8")), + ); + } catch (cause) { + throw Error( + "build version: missing or invalid build-info.json; use a source archive created by make dist, or build from a checkout with release tags", + { cause }, + ); + } +} diff --git a/contrib/bump-taler-version.mjs b/contrib/bump-taler-version.mjs @@ -1,41 +0,0 @@ -#!/usr/bin/env node - -// Bump the package.json versions in Taler-related packagesin -// this repository. -// The version must be in one of the following formats: -// - x.y.z -// - x.y.z-dev.n - -import * as fs from "fs"; - -let requestedVersion = process.argv[2]; - -let dry = false; -if (process.argv.includes("--dry")) { - dry = true; -} - -const releaseVerRegex = /^(\d+)[.](\d+)[.](\d+)$/; -const devVerRegex = /^(\d+)[.](\d+)[.](\d+)-dev[.](\d+)$/; -if ( - !releaseVerRegex.test(requestedVersion) && - !devVerRegex.test(requestedVersion) -) { - console.error( - "wrong version format, it should have 3 numbers separated with dots or end with -dev.<buildnumber>", - ); - process.exit(1); -} - -const packages = fs.readdirSync("packages"); - -for (const pkg of packages) { - const p = `packages/${pkg}/package.json`; - if (!fs.existsSync(p)) continue; - const data = JSON.parse(fs.readFileSync(p)); - console.log(p, data.version); - if (!dry) { - data.version = requestedVersion; - fs.writeFileSync(p, JSON.stringify(data, undefined, 2) + "\n"); - } -} diff --git a/contrib/next-dev-version.sh b/contrib/next-dev-version.sh @@ -1,21 +0,0 @@ -#!/bin/bash - -set -e - -FILE=$1 -[ ! -w "$FILE" ] && echo first argument should be a writtable file && exit 1 -jq -e . $FILE >/dev/null 2>&1 || (echo $FILE should be a json file && exit 1) -jq -e .version $FILE >/dev/null 2>&1 || (echo $FILE does not have a version field to increment && exit 1) - -# looks for [string]-dev.[number] -# if not present returns [first].[second].[third+1]-dev.1 -# else returns [string]-dev.[version+1] -inc_version() { - jq '. + {"version": (if .version | contains("-dev") then [.version | match("^(.*)-dev.([0-9]*)$").captures[].string] | .[0] + "-dev." + (.[1]|tonumber|.+1|tostring) else [.version | match("^([0-9]*)\\.([0-9]*)\\.([0-9]*)$").captures[].string] | .[0] + "." + .[1] + "." + (.[2]|tonumber|.+1|tostring) + "-dev.1" end)}' -} - -# read file -# replace version -# save to buffer -# write the same file -cat <<< $(cat $FILE | inc_version) > $FILE diff --git a/package.json b/package.json @@ -1,5 +1,6 @@ { "private": true, + "version": "0.0.0", "scripts": { "preinstall": "npx only-allow pnpm", "build": "pnpm run --filter '@gnu-taler/*' build", diff --git a/packages/anastasis-cli/build-node.mjs b/packages/anastasis-cli/build-node.mjs @@ -16,32 +16,14 @@ */ import esbuild from "esbuild"; -import path from "node:path"; -import fs from "node:fs"; -import { execSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { getBuildInfo } from "../../build-system/version.mjs"; -const BASE = process.cwd(); - -let GIT_ROOT = BASE; -while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") { - GIT_ROOT = path.join(GIT_ROOT, "../"); -} -const GIT_HASH = GIT_ROOT === "/" ? "not defined" : git_hash(); - -let PACKAGE_VERSION = get_version(); -function get_version() { - try { - return JSON.parse(fs.readFileSync(path.join(BASE, "package.json"))).version; - } catch { - return "not defined"; - } -} - -function git_hash() { - return execSync(`git rev-parse HEAD`, { encoding: "utf-8" }).trim(); -} +const BASE = fileURLToPath(new URL("./", import.meta.url)); +const { version: PACKAGE_VERSION, gitHash: GIT_HASH } = getBuildInfo(BASE); export const buildConfig = { + absWorkingDir: BASE, entryPoints: ["src/index.ts"], outfile: "dist/anastasis-cli-bundled.cjs", bundle: true, @@ -52,8 +34,8 @@ export const buildConfig = { sourcemap: true, inject: ["src/import-meta-url.js"], define: { - __VERSION__: `"${PACKAGE_VERSION}"`, - __GIT_HASH__: `"${GIT_HASH}"`, + __VERSION__: JSON.stringify(PACKAGE_VERSION), + __GIT_HASH__: JSON.stringify(GIT_HASH), ["import.meta.url"]: "import_meta_url", }, }; diff --git a/packages/anastasis-cli/package.json b/packages/anastasis-cli/package.json @@ -1,6 +1,7 @@ { "name": "@gnu-taler/anastasis-cli", - "version": "1.6.42", + "version": "0.0.0", + "private": true, "description": "", "engines": { "node": ">=0.18.0" diff --git a/packages/anastasis-core/package.json b/packages/anastasis-core/package.json @@ -1,6 +1,7 @@ { "name": "@gnu-taler/anastasis-core", - "version": "1.6.42", + "version": "0.0.0", + "private": true, "description": "", "main": "./lib/index.js", "module": "./lib/index.js", diff --git a/packages/anastasis-webui/package.json b/packages/anastasis-webui/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@gnu-taler/anastasis-webui", - "version": "1.6.42", + "version": "0.0.0", "license": "MIT", "type": "module", "scripts": { diff --git a/packages/challenger-webui/package.json b/packages/challenger-webui/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@gnu-taler/challenger-webui", - "version": "1.6.42", + "version": "0.0.0", "author": "sebasjm", "license": "AGPL-3.0-OR-LATER", "description": "UI for GNU Challenger.", diff --git a/packages/idb-bridge/package.json b/packages/idb-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@gnu-taler/idb-bridge", - "version": "1.6.42", + "version": "0.0.0", "description": "IndexedDB implementation that uses SQLite3 as storage", "main": "./dist/idb-bridge.js", "module": "./lib/index.js", @@ -8,7 +8,7 @@ "types": "./lib/index.d.ts", "author": "Florian Dold", "license": "AGPL-3.0-or-later", - "private": false, + "private": true, "scripts": { "test:clean": "rm -rf lib tsconfig.tsbuildinfo", "test": "pnpm run test:clean && tsc && node --test 'lib/*.test.js' 'lib/**/*.test.js'", diff --git a/packages/libeufin-bank-webui/package.json b/packages/libeufin-bank-webui/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@gnu-taler/libeufin-bank-webui", - "version": "1.6.42", + "version": "0.0.0", "license": "AGPL-3.0-OR-LATER", "type": "module", "scripts": { diff --git a/packages/pogen/example/proj1/package.json b/packages/pogen/example/proj1/package.json @@ -1,6 +1,7 @@ { "name": "proj1", - "version": "1.0.0", + "version": "0.0.0", + "private": true, "description": "", "main": "index.js", "scripts": { diff --git a/packages/pogen/package.json b/packages/pogen/package.json @@ -1,6 +1,7 @@ { "name": "@gnu-taler/pogen", - "version": "1.6.42", + "version": "0.0.0", + "private": true, "bin": { "pogen": "bin/pogen" }, diff --git a/packages/qa-tooling/package.json b/packages/qa-tooling/package.json @@ -1,6 +1,6 @@ { "name": "@gnu-taler/qa-tooling", - "version": "1.6.42", + "version": "0.0.0", "description": "QA tooling dependencies for GNU Taler", "private": true, "license": "AGPL-3.0-or-later", diff --git a/packages/qa-tooling/test/build-version.test.mjs b/packages/qa-tooling/test/build-version.test.mjs @@ -0,0 +1,259 @@ +/* + This file is part of GNU Taler + (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { + copyFileSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import test from "node:test"; +import { getBuildInfo } from "../../../build-system/version.mjs"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); +const sampleInfo = { + version: "1.6.43-dev.1-12-gabcdef123", + tag: "v1.6.43-dev.1", + gitHash: "abcdef123".padEnd(40, "0"), +}; + +function temporary(t) { + const directory = mkdtempSync(join(tmpdir(), "taler version test ")); + t.after(() => rmSync(directory, { recursive: true, force: true })); + return directory; +} + +function git(directory, ...args) { + return execFileSync( + "git", + [ + "-c", + "user.name=Version Test", + "-c", + "user.email=version-test@example.invalid", + "-c", + "commit.gpgsign=false", + "-c", + "tag.gpgsign=false", + ...args, + ], + { cwd: directory, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }, + ).trim(); +} + +function workspace(directory) { + mkdirSync(join(directory, "packages/example"), { recursive: true }); + writeFileSync( + join(directory, "pnpm-workspace.yaml"), + "packages: ['packages/*']\n", + ); + writeFileSync( + join(directory, "packages/example/package.json"), + '{"version":"0.0.0","private":true}\n', + ); +} + +function checkout(t) { + const directory = temporary(t); + workspace(directory); + git(directory, "init", "--quiet", "--initial-branch=main"); + git(directory, "add", "."); + git(directory, "commit", "--quiet", "-m", "Initial files"); + return directory; +} + +function advance(directory) { + writeFileSync(join(directory, "change.txt"), "A change\n"); + git(directory, "add", "change.txt"); + git(directory, "commit", "--quiet", "-m", "Change after the release"); +} + +test("lightweight release and annotated development tags identify builds", (t) => { + const directory = checkout(t); + git(directory, "tag", "v1.6.42"); + assert.deepEqual(getBuildInfo(directory), { + version: "1.6.42", + tag: "v1.6.42", + gitHash: git(directory, "rev-parse", "HEAD"), + }); + advance(directory); + git(directory, "tag", "-a", "v1.6.43-dev.1", "-m", "Development release"); + assert.equal(getBuildInfo(directory).version, "1.6.43-dev.1"); +}); + +test("snapshots retain tag distance and hash, ignoring unrelated tags and dirty files", (t) => { + const directory = checkout(t); + git(directory, "tag", "v1.6.43-dev.1"); + advance(directory); + git(directory, "tag", "unrelated-release"); + writeFileSync(join(directory, "change.txt"), "Local edit\n"); + const info = getBuildInfo(join(directory, "packages/example")); + assert.equal(info.tag, "v1.6.43-dev.1"); + assert.match(info.version, /^1\.6\.43-dev\.1-1-g[0-9a-f]+$/); + assert.equal( + info.version, + git(directory, "describe", "--tags", "--match", "v[0-9]*").slice(1), + ); + assert.equal(info.gitHash, git(directory, "rev-parse", "HEAD")); +}); + +test("linked worktrees use their own HEAD", (t) => { + const directory = checkout(t); + git(directory, "tag", "v1.6.42"); + const worktree = join(temporary(t), "worktree"); + git(directory, "worktree", "add", "--detach", worktree, "HEAD"); + advance(worktree); + assert.match(getBuildInfo(worktree).version, /^1\.6\.42-1-g/); + assert.equal(getBuildInfo(directory).version, "1.6.42"); +}); + +test("missing tags fail with guidance, even when stale archive metadata exists", (t) => { + const directory = checkout(t); + writeFileSync(join(directory, "build-info.json"), JSON.stringify(sampleInfo)); + assert.throws( + () => getBuildInfo(directory), + /fetch the release tags and history/, + ); + git(directory, "tag", "v1.6.42"); + assert.equal(getBuildInfo(directory).version, "1.6.42"); +}); + +test("a shallow checkout without the tagged history fails with guidance", (t) => { + const directory = checkout(t); + git(directory, "tag", "v1.6.42"); + advance(directory); + const clone = join(temporary(t), "shallow"); + git(directory, "clone", "--quiet", "--depth=1", `file://${directory}`, clone); + assert.throws(() => getBuildInfo(clone), /git fetch --unshallow/); +}); + +test("archive metadata works without Git and ignores an enclosing checkout", (t) => { + const parent = checkout(t); + git(parent, "tag", "v9.9.9"); + const archive = join(parent, "extracted"); + workspace(archive); + writeFileSync(join(archive, "build-info.json"), JSON.stringify(sampleInfo)); + assert.deepEqual(getBuildInfo(join(archive, "packages/example")), sampleInfo); + rmSync(join(archive, "build-info.json")); + assert.throws( + () => getBuildInfo(archive), + /missing or invalid build-info.json/, + ); +}); + +test("missing and malformed archive metadata never fall back to package.json", (t) => { + const directory = temporary(t); + workspace(directory); + for (const content of [ + "{", + "null", + "{}", + JSON.stringify({ ...sampleInfo, gitHash: "not-a-hash" }), + JSON.stringify({ ...sampleInfo, version: "0.0.0" }), + JSON.stringify({ ...sampleInfo, version: "1.6.43-dev.1-12-gfedcba" }), + JSON.stringify({ ...sampleInfo, tag: "../../bad" }), + ]) { + writeFileSync(join(directory, "build-info.json"), content); + assert.throws( + () => getBuildInfo(directory), + /missing or invalid build-info.json/, + ); + } + rmSync(join(directory, "build-info.json")); + assert.throws( + () => getBuildInfo(directory), + /source archive created by make dist/, + ); +}); + +test("the version CLI accepts another working directory and emits text or JSON", (t) => { + const directory = checkout(t); + git(directory, "tag", "v1.6.42"); + const cli = join(root, "build-system/version-cli.mjs"); + const run = (...args) => + execFileSync(process.execPath, [cli, ...args], { + cwd: tmpdir(), + encoding: "utf8", + }).trim(); + assert.equal(run(directory), "1.6.42"); + assert.deepEqual( + JSON.parse(run("--json", join(directory, "packages/example"))), + getBuildInfo(directory), + ); +}); + +test("source archives preserve Git versions and hashes without changing the checkout", (t) => { + const directory = checkout(t); + mkdirSync(join(directory, "build-system")); + for (const file of [ + "archive.py", + "version-cli.mjs", + "version.mjs", + "version.d.mts", + ]) { + copyFileSync( + join(root, "build-system", file), + join(directory, "build-system", file), + ); + } + git(directory, "add", "build-system"); + git(directory, "commit", "--quiet", "-m", "Add archive tooling"); + git(directory, "tag", "v1.6.42"); + const artifacts = temporary(t); + for (const snapshot of [false, true]) { + if (snapshot) advance(directory); + const info = getBuildInfo(directory); + const archive = join( + artifacts, + `taler-typescript-core-${info.version}.tar.gz`, + ); + execFileSync( + "python3", + [join(directory, "build-system/archive.py"), archive], + { + cwd: directory, + stdio: "pipe", + }, + ); + execFileSync("tar", ["-xzf", archive, "-C", artifacts]); + const extracted = join(artifacts, `taler-typescript-core-${info.version}`); + assert.equal(existsSync(join(extracted, ".git")), false); + assert.deepEqual( + JSON.parse(readFileSync(join(extracted, "build-info.json"), "utf8")), + info, + ); + assert.deepEqual(getBuildInfo(extracted), info); + const output = execFileSync( + process.execPath, + [join(extracted, "build-system/version-cli.mjs"), "--json"], + { + cwd: join(extracted, "packages/example"), + encoding: "utf8", + }, + ); + assert.deepEqual(JSON.parse(output), info); + assert.equal(existsSync(join(directory, "build-info.json")), false); + assert.equal(git(directory, "status", "--porcelain"), ""); + } +}); diff --git a/packages/taler-auditor-webui/package.json b/packages/taler-auditor-webui/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@gnu-taler/taler-auditor-webui", - "version": "1.6.42", + "version": "0.0.0", "license": "AGPL-3.0-or-later", "type": "module", "scripts": { diff --git a/packages/taler-exchange-aml-webui/package.json b/packages/taler-exchange-aml-webui/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@gnu-taler/taler-exchange-aml-webui", - "version": "1.6.42", + "version": "0.0.0", "author": "sebasjm", "license": "AGPL-3.0-OR-LATER", "description": "Back-office SPA for GNU Taler Exchange.", diff --git a/packages/taler-exchange-kyc-webui/package.json b/packages/taler-exchange-kyc-webui/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@gnu-taler/taler-exchange-kyc-webui", - "version": "1.6.42", + "version": "0.0.0", "author": "sebasjm", "license": "AGPL-3.0-OR-LATER", "description": "UI for GNU Exchange KYC.", diff --git a/packages/taler-harness/build.mjs b/packages/taler-harness/build.mjs @@ -18,31 +18,13 @@ import esbuild from "esbuild"; import fs from "node:fs"; import path from "node:path"; -import { execSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { getBuildInfo } from "../../build-system/version.mjs"; -const BASE = process.cwd(); - -let GIT_ROOT = BASE; -while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") { - GIT_ROOT = path.join(GIT_ROOT, "../"); -} - -const GIT_HASH = GIT_ROOT === "/" ? "not defined" : git_hash(); +const BASE = fileURLToPath(new URL("./", import.meta.url)); +const { version: PACKAGE_VERSION, gitHash: GIT_HASH } = getBuildInfo(BASE); const INSTALL_PREFIX = get_install_prefix(); -let PACKAGE_VERSION = get_version(); -function get_version() { - try { - return JSON.parse(fs.readFileSync(path.join(BASE, "package.json"))).version; - } catch { - return "not defined"; - } -} - -function git_hash() { - return execSync(`git rev-parse HEAD`, { encoding: "utf-8" }).trim(); -} - function get_install_prefix() { const candidates = [ path.join(BASE, ".config.mk"), @@ -62,6 +44,7 @@ function get_install_prefix() { // Still commonjs, because axios doesn't work properly under mjs export const buildConfig = { + absWorkingDir: BASE, entryPoints: ["src/index.ts"], outfile: "dist/taler-harness-bundled.cjs", bundle: true, @@ -75,11 +58,11 @@ export const buildConfig = { external: ["playwright-core", "selenium-webdriver", "selenium-webdriver/*"], inject: ["src/import-meta-url.js"], define: { - __VERSION__: `"${PACKAGE_VERSION}"`, - __GIT_HASH__: `"${GIT_HASH}"`, + __VERSION__: JSON.stringify(PACKAGE_VERSION), + __GIT_HASH__: JSON.stringify(GIT_HASH), __INSTALL_PREFIX__: JSON.stringify(INSTALL_PREFIX), - "walletCoreBuildInfo.implementationSemver": `"${PACKAGE_VERSION}"`, - "walletCoreBuildInfo.implementationGitHash": `"${GIT_HASH}"`, + "walletCoreBuildInfo.implementationSemver": JSON.stringify(PACKAGE_VERSION), + "walletCoreBuildInfo.implementationGitHash": JSON.stringify(GIT_HASH), "import.meta.url": "import_meta_url", }, }; diff --git a/packages/taler-harness/package.json b/packages/taler-harness/package.json @@ -1,6 +1,7 @@ { "name": "@gnu-taler/taler-harness", - "version": "1.6.42", + "version": "0.0.0", + "private": true, "description": "", "engines": { "node": ">=0.12.0" diff --git a/packages/taler-merchant-webui/contrib/qa/version.test.mjs b/packages/taler-merchant-webui/contrib/qa/version.test.mjs @@ -15,39 +15,27 @@ */ import assert from "node:assert/strict"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { test } from "node:test"; import { resolveVersion } from "../../esbuild.config.mjs"; -test("an exact release tag is the build version", () => { - assert.equal(resolveVersion({ runGit: () => "v1.6.26" }), "v1.6.26"); -}); - -test("commits after a release retain the tag distance and hash", () => { - assert.equal( - resolveVersion({ runGit: () => "v1.6.26-80-gd3e8a1cf8" }), - "v1.6.26-80-gd3e8a1cf8", - ); -}); - -test("a tagless checkout combines the package version and commit", () => { - const runGit = (args) => { - if (args[0] === "describe") throw Error("no reachable tag"); - return "d3e8a1cf8d"; - }; - assert.equal( - resolveVersion({ version: "1.6.26", runGit }), - "v1.6.26-gd3e8a1cf8d", - ); -}); - -test("a source archive falls back to the package version", () => { - assert.equal( - resolveVersion({ - version: "1.6.26", - runGit: () => { - throw Error("Git metadata is unavailable"); - }, - }), - "v1.6.26", - ); +test("merchant builds use the archived Git version without a package version", (t) => { + const root = mkdtempSync(join(tmpdir(), "merchant-version-")); + t.after(() => rmSync(root, { recursive: true, force: true })); + writeFileSync(join(root, "pnpm-workspace.yaml"), "packages: []\n"); + for (const version of ["1.6.42", "1.6.42-12-gabcdef123"]) { + writeFileSync( + join(root, "build-info.json"), + JSON.stringify({ + version, + tag: "v1.6.42", + gitHash: "abcdef123".padEnd(40, "0"), + }), + ); + assert.equal(resolveVersion(root), version); + } + rmSync(join(root, "build-info.json")); + assert.throws(() => resolveVersion(root), /missing or invalid build-info/); }); diff --git a/packages/taler-merchant-webui/esbuild.config.mjs b/packages/taler-merchant-webui/esbuild.config.mjs @@ -21,42 +21,13 @@ import * as esbuild from "esbuild"; import { execFileSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { getBuildInfo } from "../../build-system/version.mjs"; -const here = path.dirname(new URL(import.meta.url).pathname); -const repositoryRoot = path.resolve(here, "../.."); +const here = path.dirname(fileURLToPath(import.meta.url)); -const packageVersion = JSON.parse( - fs.readFileSync(path.join(here, "package.json"), "utf8"), -).version; - -/** - * Resolve a useful version in Git checkouts without making source archives - * depend on Git metadata being present. - */ -export function resolveVersion({ - version = packageVersion, - runGit = (args) => { - if (!fs.existsSync(path.join(repositoryRoot, ".git"))) { - throw Error("Git metadata is unavailable"); - } - return execFileSync("git", args, { - cwd: repositoryRoot, - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - }).trim(); - }, -} = {}) { - const releaseVersion = version.startsWith("v") ? version : `v${version}`; - try { - return runGit(["describe", "--tags", "--match", "v[0-9]*"]); - } catch { - try { - const hash = runGit(["rev-parse", "--short=10", "HEAD"]); - return `${releaseVersion}-g${hash.replace(/^g/, "")}`; - } catch { - return releaseVersion; - } - } +export function resolveVersion(directory = here) { + return getBuildInfo(directory).version; } /** Entry points. Statistics is split out: chart.js is far heavier than the app. */ diff --git a/packages/taler-merchant-webui/package.json b/packages/taler-merchant-webui/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@gnu-taler/taler-merchant-webui", - "version": "1.6.42", + "version": "0.0.0", "license": "AGPL-3.0-or-later", "type": "module", "scripts": { diff --git a/packages/taler-util/package.json b/packages/taler-util/package.json @@ -1,12 +1,12 @@ { "name": "@gnu-taler/taler-util", - "version": "1.6.42", + "version": "0.0.0", "description": "Generic helper functionality for GNU Taler", "type": "module", "types": "./lib/index.node.d.ts", "author": "Florian Dold", "license": "AGPL-3.0-or-later", - "private": false, + "private": true, "exports": { ".": { "node": "./lib/index.node.js", diff --git a/packages/taler-wallet-cli/build-node.mjs b/packages/taler-wallet-cli/build-node.mjs @@ -16,32 +16,14 @@ */ import esbuild from "esbuild"; -import path from "node:path"; -import fs from "node:fs"; -import { execSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { getBuildInfo } from "../../build-system/version.mjs"; -const BASE = process.cwd(); - -let GIT_ROOT = BASE; -while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") { - GIT_ROOT = path.join(GIT_ROOT, "../"); -} -const GIT_HASH = GIT_ROOT === "/" ? undefined : git_hash(); - -let PACKAGE_VERSION = get_version(); -function get_version() { - try { - return JSON.parse(fs.readFileSync(path.join(BASE, "package.json"))).version; - } catch { - return "not defined"; - } -} - -function git_hash() { - return execSync(`git rev-parse HEAD`, { encoding: "utf-8" }).trim(); -} +const BASE = fileURLToPath(new URL("./", import.meta.url)); +const { version: PACKAGE_VERSION, gitHash: GIT_HASH } = getBuildInfo(BASE); export const buildConfig = { + absWorkingDir: BASE, entryPoints: ["src/index.ts"], outfile: "dist/taler-wallet-cli-bundled.cjs", bundle: true, @@ -52,10 +34,10 @@ export const buildConfig = { sourcemap: true, inject: ["src/import-meta-url.js"], define: { - __VERSION__: `"${PACKAGE_VERSION}"`, - __GIT_HASH__: `"${GIT_HASH}"`, - "walletCoreBuildInfo.implementationSemver": `"${PACKAGE_VERSION}"`, - "walletCoreBuildInfo.implementationGitHash": `"${GIT_HASH}"`, + __VERSION__: JSON.stringify(PACKAGE_VERSION), + __GIT_HASH__: JSON.stringify(GIT_HASH), + "walletCoreBuildInfo.implementationSemver": JSON.stringify(PACKAGE_VERSION), + "walletCoreBuildInfo.implementationGitHash": JSON.stringify(GIT_HASH), ["import.meta.url"]: "import_meta_url", }, }; diff --git a/packages/taler-wallet-cli/build-qtart.mjs b/packages/taler-wallet-cli/build-qtart.mjs @@ -1,65 +0,0 @@ -#!/usr/bin/env node -/* - This file is part of GNU Taler - (C) 2022 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -import esbuild from "esbuild"; -import path from "node:path"; -import fs from "node:fs"; -import { execSync } from "node:child_process"; - -const BASE = process.cwd(); - -let GIT_ROOT = BASE; -while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") { - GIT_ROOT = path.join(GIT_ROOT, "../"); -} -if (GIT_ROOT === "/") { - console.log("not found"); - process.exit(1); -} -const GIT_HASH = GIT_ROOT === "/" ? undefined : git_hash(); - -let _package = JSON.parse(fs.readFileSync(path.join(BASE, "package.json"))); - -function git_hash() { - return execSync(`git rev-parse HEAD`, { encoding: "utf-8" }).trim(); -} - -export const buildConfig = { - entryPoints: ["src/index.ts"], - outfile: "dist/taler-wallet-cli.qtart.mjs", - bundle: true, - minify: false, - target: ["es2023"], - format: "esm", - platform: "neutral", - mainFields: ["module", "main"], - conditions: ["qtart"], - sourcemap: true, - // quickjs standard library - external: ["std", "os", "node:child_process"], - define: { - __VERSION__: `"${_package.version}"`, - __GIT_HASH__: `"${GIT_HASH}"`, - "walletCoreBuildInfo.implementationSemver": `"${_package.version}"`, - "walletCoreBuildInfo.implementationGitHash": `"${GIT_HASH}"`, - }, -}; - -esbuild.build(buildConfig).catch((e) => { - console.log(e); - process.exit(1); -}); diff --git a/packages/taler-wallet-cli/package.json b/packages/taler-wallet-cli/package.json @@ -1,6 +1,7 @@ { "name": "@gnu-taler/taler-wallet-cli", - "version": "1.6.42", + "version": "0.0.0", + "private": true, "description": "", "engines": { "node": ">=0.18.0" diff --git a/packages/taler-wallet-core/package.json b/packages/taler-wallet-core/package.json @@ -1,6 +1,7 @@ { "name": "@gnu-taler/taler-wallet-core", - "version": "1.6.42", + "version": "0.0.0", + "private": true, "description": "", "engines": { "node": ">=0.18.0" diff --git a/packages/taler-wallet-embedded/build.mjs b/packages/taler-wallet-embedded/build.mjs @@ -16,32 +16,14 @@ */ import esbuild from "esbuild"; -import path from "node:path"; -import fs from "node:fs"; -import { execSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { getBuildInfo } from "../../build-system/version.mjs"; -const BASE = process.cwd(); - -let GIT_ROOT = BASE; -while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") { - GIT_ROOT = path.join(GIT_ROOT, "../"); -} -const GIT_HASH = GIT_ROOT === "/" ? "not defined" : git_hash(); - -let PACKAGE_VERSION = get_version(); -function get_version() { - try { - return JSON.parse(fs.readFileSync(path.join(BASE, "package.json"))).version; - } catch { - return "not defined"; - } -} - -function git_hash() { - return execSync(`git rev-parse HEAD`, { encoding: "utf-8" }).trim(); -} +const BASE = fileURLToPath(new URL("./", import.meta.url)); +const { version: PACKAGE_VERSION, gitHash: GIT_HASH } = getBuildInfo(BASE); export const buildConfig = { + absWorkingDir: BASE, entryPoints: ["src/wallet-qjs.ts"], outfile: "dist/taler-wallet-core-qjs.mjs", bundle: true, @@ -54,10 +36,10 @@ export const buildConfig = { conditions: ["qtart"], sourcemap: true, define: { - __VERSION__: `"${PACKAGE_VERSION}"`, - __GIT_HASH__: `"${GIT_HASH}"`, - "walletCoreBuildInfo.implementationSemver": `"${PACKAGE_VERSION}"`, - "walletCoreBuildInfo.implementationGitHash": `"${GIT_HASH}"`, + __VERSION__: JSON.stringify(PACKAGE_VERSION), + __GIT_HASH__: JSON.stringify(GIT_HASH), + "walletCoreBuildInfo.implementationSemver": JSON.stringify(PACKAGE_VERSION), + "walletCoreBuildInfo.implementationGitHash": JSON.stringify(GIT_HASH), }, }; diff --git a/packages/taler-wallet-embedded/package.json b/packages/taler-wallet-embedded/package.json @@ -1,6 +1,7 @@ { "name": "@gnu-taler/taler-wallet-embedded", - "version": "1.6.42", + "version": "0.0.0", + "private": true, "description": "", "engines": { "node": ">=0.18.0" diff --git a/packages/wallet-webui/build.mjs b/packages/wallet-webui/build.mjs @@ -9,6 +9,7 @@ import autoprefixer from "autoprefixer"; import { assertContentScriptBundleSize } from "./bundle-budget.mjs"; import tailwindConfig from "./tailwind.config.mjs"; import { makeManifest } from "./manifest.mjs"; +import { getBuildInfo } from "../../build-system/version.mjs"; const watch = process.argv.includes("--watch"); const pwaSqliteTest = process.argv.includes("--test-pwa-sqlite"); @@ -21,11 +22,9 @@ if ((pwaSqliteTest || extensionSqliteTest) && withoutSqlite) { } const sqliteEnabled = !withoutSqlite; const packageDirectory = fileURLToPath(new URL("./", import.meta.url)); -const packageMetadata = JSON.parse( - await readFile(new URL("./package.json", import.meta.url), "utf8"), -); +const { version: packageVersion } = getBuildInfo(packageDirectory); const defineBuildConstants = (extra = {}) => ({ - __WALLET_WEBUI_VERSION__: JSON.stringify(packageMetadata.version), + __WALLET_WEBUI_VERSION__: JSON.stringify(packageVersion), ...extra, }); const dist = new URL("./dist/", import.meta.url); @@ -362,7 +361,7 @@ for (const target of ["chrome", "firefox"]) { JSON.stringify( makeManifest(target, { sqliteEnabled: extensionSqliteTest, - packageVersion: packageMetadata.version, + packageVersion: packageVersion, }), null, 2, diff --git a/packages/wallet-webui/manifest.mjs b/packages/wallet-webui/manifest.mjs @@ -1,6 +1,9 @@ export function extensionManifestVersion(packageVersion) { - const match = /^(\d+)\.(\d+)\.(\d+)(?:-dev\.(\d+))?$/.exec(packageVersion); - if (!match) throw Error(`invalid wallet package version: ${packageVersion}`); + const match = + /^(\d+)\.(\d+)\.(\d+)(?:-dev\.(\d+))?(?:-[1-9]\d*-g[0-9a-f]+)?$/.exec( + packageVersion, + ); + if (!match) throw Error(`invalid wallet build version: ${packageVersion}`); return match[4] ? `${match[1]}.${match[2]}.${match[3]}.${match[4]}` : `${match[1]}.${match[2]}.${match[3]}`; diff --git a/packages/wallet-webui/pack-cli.mjs b/packages/wallet-webui/pack-cli.mjs @@ -0,0 +1,31 @@ +#!/usr/bin/env node +/* + This file is part of GNU Taler + (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { fileURLToPath } from "node:url"; +import { getBuildInfo } from "../../build-system/version.mjs"; +import { packageExtensions, parsePackageTargets } from "./pack.mjs"; + +try { + const targets = parsePackageTargets(process.argv.slice(2)); + const { version } = getBuildInfo( + fileURLToPath(new URL("./", import.meta.url)), + ); + await packageExtensions(targets, version); +} catch (error) { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; +} diff --git a/packages/wallet-webui/pack.d.mts b/packages/wallet-webui/pack.d.mts @@ -18,4 +18,5 @@ export function packageExtension( ): Promise<void>; export function packageExtensions( targets: readonly ExtensionTarget[], + packageVersion: string, ): Promise<void>; diff --git a/packages/wallet-webui/pack.mjs b/packages/wallet-webui/pack.mjs @@ -3,7 +3,7 @@ import { execFile } from "node:child_process"; import { access, mkdtemp, readFile, rename, rm } from "node:fs/promises"; import { resolve } from "node:path"; import { promisify } from "node:util"; -import { fileURLToPath, pathToFileURL } from "node:url"; +import { fileURLToPath } from "node:url"; import { extensionManifestVersion } from "./manifest.mjs"; const run = promisify(execFile); @@ -40,7 +40,7 @@ export function parsePackageTargets(args) { } else if (args.length === 1 && args[0].startsWith("--target=")) { requested = args[0].slice("--target=".length); } else { - fail("usage: pack.mjs [--target chrome|firefox|all]"); + fail("usage: pack-cli.mjs [--target chrome|firefox|all]"); } if (requested === "all") return [...targets]; if (targets.includes(requested)) return [requested]; @@ -204,24 +204,9 @@ export async function packageExtension( } } -export async function packageExtensions(requestedTargets) { +export async function packageExtensions(requestedTargets, packageVersion) { const packageDirectory = fileURLToPath(new URL("./", import.meta.url)); - const metadata = JSON.parse( - await readFile(resolve(packageDirectory, "package.json"), "utf8"), - ); for (const target of requestedTargets) { - await packageExtension(target, metadata.version, packageDirectory); + await packageExtension(target, packageVersion, packageDirectory); } } - -const isMain = - process.argv[1] !== undefined && - pathToFileURL(resolve(process.argv[1])).href === import.meta.url; -if (isMain) { - packageExtensions(parsePackageTargets(process.argv.slice(2))).catch( - (error) => { - console.error(error instanceof Error ? error.message : error); - process.exitCode = 1; - }, - ); -} diff --git a/packages/wallet-webui/package.json b/packages/wallet-webui/package.json @@ -1,6 +1,6 @@ { "name": "@gnu-taler/wallet-webui", - "version": "1.6.42", + "version": "0.0.0", "description": "Shared GNU Taler browser wallet for PWA and WebExtension hosts", "type": "module", "private": true, @@ -19,9 +19,9 @@ "build:test:extension-sqlite": "node build.mjs --test-extension-sqlite", "build:with-deps": "pnpm --filter \"{.}...\" run build", "build:store": "pnpm build:with-deps && pnpm package:extensions", - "package:chrome": "node pack.mjs --target chrome", - "package:firefox": "node pack.mjs --target firefox", - "package:extensions": "node pack.mjs --target all", + "package:chrome": "node pack-cli.mjs --target chrome", + "package:firefox": "node pack-cli.mjs --target firefox", + "package:extensions": "node pack-cli.mjs --target all", "typecheck": "tsc --build --pretty false", "test:clean": "rm -rf lib tsconfig.tsbuildinfo", "test": "pnpm run test:clean && tsc --build --pretty false && node --test 'lib/test/**/*.test.js'", @@ -29,7 +29,7 @@ "lint:firefox": "web-ext lint --source-dir dist/extension/firefox --boring", "lint": "../qa-tooling/bin/eslint.mjs .", "check": "pnpm typecheck && pnpm test:boundaries && pnpm test && pnpm build && pnpm package:extensions", - "pretty": "prettier --write src test build.mjs build-harness.mjs test-server.mjs bundle-budget.mjs bundle-budget.d.mts pack.mjs pack.d.mts tailwind.config.mjs", + "pretty": "prettier --write src test build.mjs build-harness.mjs test-server.mjs bundle-budget.mjs bundle-budget.d.mts pack.mjs pack-cli.mjs pack.d.mts tailwind.config.mjs", "i18n:source2po": "pogen extract && pogen merge", "i18n:po2strings": "pogen emit", "i18n:check": "pogen check" diff --git a/packages/wallet-webui/test/manifests.test.ts b/packages/wallet-webui/test/manifests.test.ts @@ -1,14 +1,11 @@ import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; import test from "node:test"; import { extensionManifestVersion, makeManifest, } from "../src/platform/manifests.js"; -const packageVersion = JSON.parse( - readFileSync(new URL("../../package.json", import.meta.url), "utf8"), -).version as string; +const packageVersion = "1.6.42"; function compareManifestVersions(left: string, right: string): number { const leftParts = left.split(".").map(Number); @@ -117,7 +114,14 @@ test("SQLite test manifests enable WASM only in the gated targets", () => { test("release tooling versions are valid and upgradeable browser versions", () => { assert.equal(extensionManifestVersion("1.6.25"), "1.6.25"); assert.equal(extensionManifestVersion("1.6.25-dev.7"), "1.6.25.7"); - assert.throws(() => extensionManifestVersion("0.1")); + assert.equal(extensionManifestVersion("1.6.42-12-gabcdef123"), "1.6.42"); + assert.equal( + extensionManifestVersion("1.6.43-dev.1-112-gef20aaf71"), + "1.6.43.1", + ); + for (const invalid of ["0.1", "1.6.42-rc.1", "1.6.42-1-gxyz", "1.6.42-1"]) { + assert.throws(() => extensionManifestVersion(invalid)); + } assert.ok( compareManifestVersions( extensionManifestVersion(packageVersion), @@ -126,3 +130,13 @@ test("release tooling versions are valid and upgradeable browser versions", () = "the generated extension must upgrade the published 1.6.32 release", ); }); + +test("snapshot manifests retain the tag version and full Chrome version name", () => { + const packageVersion = "1.6.43-dev.1-112-gef20aaf71"; + const chrome = makeManifest("chrome", { packageVersion }); + const firefox = makeManifest("firefox", { packageVersion }); + assert.equal(chrome.version, "1.6.43.1"); + assert.equal(firefox.version, chrome.version); + assert.equal(chrome.version_name, packageVersion); + assert.equal(Object.hasOwn(firefox, "version_name"), false); +}); diff --git a/packages/wallet-webui/test/packaging.test.ts b/packages/wallet-webui/test/packaging.test.ts @@ -165,3 +165,19 @@ test("failed packaging removes a stale store archive", async (t) => { ); await assert.rejects(access(destination)); }); + +test("snapshot packages use the full Git version and validate numeric manifests", () => { + for (const version of [ + "1.6.42-12-gabcdef123", + "1.6.43-dev.1-112-gef20aaf71", + ]) { + for (const target of ["chrome", "firefox"] as const) { + const manifest = makeManifest(target, { packageVersion: version }); + validateExtensionManifest(target, manifest, version); + assert.equal( + artifactFilename(target, version), + `taler-wallet-${target}-${version}.zip`, + ); + } + } +}); diff --git a/packages/web-util/build.mjs b/packages/web-util/build.mjs @@ -16,35 +16,16 @@ */ import esbuild from "esbuild"; -import fs from "node:fs"; import path from "node:path"; import nodeUrl from "node:url"; -import { execSync } from "node:child_process"; +import { getBuildInfo } from "../../build-system/version.mjs"; const root = path.dirname(nodeUrl.fileURLToPath(import.meta.url)); process.chdir(root); const BASE_DIR = process.cwd(); -let GIT_ROOT = BASE_DIR; -while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") { - GIT_ROOT = path.join(GIT_ROOT, "../"); -} -const GIT_HASH = GIT_ROOT === "/" ? "not defined" : git_hash(); - -let PACKAGE_VERSION = get_version(); -function get_version() { - try { - return JSON.parse(fs.readFileSync(path.join(BASE_DIR, "package.json"))) - .version; - } catch { - return "not defined"; - } -} - -function git_hash() { - return execSync(`git rev-parse HEAD`, { encoding: "utf-8" }).trim(); -} +const { version: PACKAGE_VERSION, gitHash: GIT_HASH } = getBuildInfo(BASE_DIR); const buildConfigBase = { outdir: "lib", @@ -60,8 +41,8 @@ const buildConfigBase = { }, sourcemap: true, define: { - __VERSION__: `"${PACKAGE_VERSION}"`, - __GIT_HASH__: `"${GIT_HASH}"`, + __VERSION__: JSON.stringify(PACKAGE_VERSION), + __GIT_HASH__: JSON.stringify(GIT_HASH), }, }; diff --git a/packages/web-util/package.json b/packages/web-util/package.json @@ -1,11 +1,11 @@ { "name": "@gnu-taler/web-util", - "version": "1.6.42", + "version": "0.0.0", "description": "Generic helper functionality for GNU Taler Web Apps", "type": "module", "author": "Sebastian Marchano", "license": "AGPL-3.0-or-later", - "private": false, + "private": true, "exports": { "./i18n": { "types": "./lib/i18n/strings.d.ts", diff --git a/packages/web-util/src/index.build.test.ts b/packages/web-util/src/index.build.test.ts @@ -2,8 +2,9 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { pathToFileURL } from "node:url"; import test from "node:test"; -import { emptyTestOutputDirectory } from "./index.build.js"; +import { computeConfig, emptyTestOutputDirectory } from "./index.build.js"; test("test output is emptied without deleting the package root", () => { const packageRoot = fs.mkdtempSync(path.join(os.tmpdir(), "web-util-test-")); @@ -30,3 +31,40 @@ test("test output cleanup rejects unsafe directories", () => { fs.rmSync(packageRoot, { recursive: true, force: true }); } }); + +test("build configuration reads the calling workspace version on each invocation", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "web-util-version-")); + const previousDirectory = process.cwd(); + try { + const packageRoot = path.join(root, "packages/example"); + fs.mkdirSync(packageRoot, { recursive: true }); + fs.writeFileSync(path.join(root, "pnpm-workspace.yaml"), "packages: []\n"); + const gitHash = "abcdef123".padEnd(40, "0"); + for (const version of ["1.6.42", "1.6.42-12-gabcdef123"]) { + fs.writeFileSync( + path.join(root, "build-info.json"), + JSON.stringify({ version, tag: "v1.6.42", gitHash }), + ); + const config = computeConfig({ + type: "production", + importMeta: { + url: pathToFileURL(path.join(packageRoot, "build.mjs")).href, + } as ImportMeta, + source: { js: [], assets: [] }, + destination: "dist", + }); + assert.equal(config.version, version); + assert.equal( + config.esBuildOptions.define?.__VERSION__, + JSON.stringify(version), + ); + assert.equal( + config.esBuildOptions.define?.__GIT_HASH__, + JSON.stringify(gitHash), + ); + } + } finally { + process.chdir(previousDirectory); + fs.rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/packages/web-util/src/index.build.ts b/packages/web-util/src/index.build.ts @@ -1,5 +1,5 @@ import esbuild, { PluginBuild } from "esbuild"; -import { execFileSync } from "node:child_process"; +import { getBuildInfo } from "../../../build-system/version.mjs"; import fs from "node:fs"; import { default as nodePath, default as path } from "node:path"; import nodeUrl from "node:url"; @@ -41,12 +41,6 @@ export function getFilesInDirectory(startPath: string, regex?: RegExp): Assets { }; } -function git_hash(gitRoot: string): string { - return execFileSync("git", ["-C", gitRoot, "rev-parse", "HEAD"], { - encoding: "utf-8", - }).trim(); -} - // FIXME: Put this into some helper library. function copyFilesPlugin(assets: Assets | Assets[]) { return { @@ -211,17 +205,7 @@ function getPackageAndGitRoot(meta: undefined | ImportMeta) { } const baseDir = process.cwd(); - let GIT_ROOT = baseDir; - while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") { - GIT_ROOT = path.join(GIT_ROOT, "../"); - } - if (GIT_ROOT === "/") { - return { version: "not defined", hash: "not defined", baseDir }; - } - const hash = git_hash(GIT_ROOT); - - const buf = fs.readFileSync(path.join(baseDir, "package.json")); - const version = JSON.parse(buf.toString("utf-8")).version; + const { version, gitHash: hash } = getBuildInfo(baseDir); return { version, hash, baseDir }; } @@ -279,8 +263,8 @@ export function computeConfig(params: BuildParams): { sourcemap: true, //params.type !== "production", define: { "process.env.NODE_ENV": JSON.stringify(params.type), - __VERSION__: `"${version}"`, - __GIT_HASH__: `"${GIT_HASH}"`, + __VERSION__: JSON.stringify(version), + __GIT_HASH__: JSON.stringify(GIT_HASH), }, // Test files import node: built-in modules (e.g. node:test, node:assert) // which can't be resolved with platform:"browser". Mark them as external