mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-10 03:08:10 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The server reports its own version through `server/src/version.ts`, which is read by the `/health` endpoint and the telemetry client > - When running from a cloned source tree, `server/package.json` is frozen at the last published release version (e.g. `0.3.1`), so the reported `serverVersion` never reflects how far the local checkout has drifted from that release > - Operators and support staff cannot tell from telemetry or health output whether they are running a tagged release or a development build with local commits on top > - A `git describe --tags --match v* --long --dirty` call at startup gives the exact nearest tag, number of commits since it, the current SHA, and whether the tree is dirty — all the information needed to compute a semantically meaningful version > - This pull request replaces the static `pkg.version` export with a `resolveServerVersion()` call that parses `git describe` output into `YYYY.MDD.P+N.git.<sha>` (drift), `YYYY.MDD.P` (clean on-tag), or appends `.dirty` for a modified tree, with a non-throwing fallback to `package.json` when git is unavailable > - The benefit is that from-source installs now report a version string that lets operators and support quickly identify their exact checkout state without running additional git commands ## Linked Issues or Issue Description No pre-existing public issue. Inline description: **What happened?** When Paperclip is installed from source (git clone + pnpm), `GET /health` and the telemetry envelope report the version frozen at the last published `package.json` value (e.g. `0.3.1`) regardless of how many commits ahead of that tag the local checkout is. **Expected behavior** The reported version should reflect the actual local state — nearest release tag, number of commits since that tag, abbreviated commit SHA, and a dirty marker when the working tree has uncommitted changes. **Steps to reproduce** Clone the repo, run `pnpm install && pnpm --filter @paperclipai/server start`, then call `GET /health` or inspect telemetry envelopes. The `serverVersion` field shows the `package.json` version even when the checkout is dozens of commits ahead of that tag. **Paperclip version or commit** Affects all source-tree installs where `package.json` has not been updated to match the current HEAD. **Deployment mode** Source install (git clone). ## What Changed - `server/src/version.ts`: extracted `resolveServerVersion()` (replaces the module-level `const serverVersion`) and `parseGitDescribeVersion()` (exported for unit testing); the default implementation shells out to `git describe --tags --match v* --long --dirty` with a 1 500 ms timeout; falls back to `pkg.version ?? "0.0.0"` without throwing when git is unavailable or the output cannot be parsed; replaced `logger` import with a `console.debug`-based default to avoid pulling pino transport side effects into a zero-dependency utility module - `server/src/__tests__/version.test.ts`: 7-test unit suite covering drift, clean on-tag collapse, dirty on-tag edge case, unparseable fallback, `resolveServerVersion` happy path, and git-unavailable fallback — all exercised via injected stubs without spawning a real git process ## Verification ```sh # Unit tests (7 tests) pnpm exec vitest run server/src/__tests__/version.test.ts # Type check pnpm --filter @paperclipai/server typecheck # Health and telemetry regression pnpm exec vitest run server/src/__tests__/health.test.ts server/src/__tests__/telemetry-client-flush.test.ts # Runtime smoke (from-source checkout) # git describe --tags --match 'v*' --long => v2026.626.0-58-g518fc71ce # server startup => serverVersion = 2026.626.0+59.git.3367571cc ``` All commands passed at the committed HEAD. ## Risks Low. The change is additive and self-contained to `server/src/version.ts`: - `git describe` is called once at module load with a 1 500 ms timeout; failure (non-git environment, git not on PATH, timeout) is silently caught and falls back to `pkg.version`, preserving existing behavior for published-package installs - No API surface, database schema, or migration is touched - The telemetry envelope already carried `serverVersion`; only the value changes for source-tree installs ## Model Used Claude Sonnet 4.6 (`claude-sonnet-4-6`) with tool use and code execution. Context window: 200 k tokens. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [ ] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [ ] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
79 lines
2.2 KiB
TypeScript
79 lines
2.2 KiB
TypeScript
import { createRequire } from "node:module";
|
|
import { execFileSync } from "node:child_process";
|
|
|
|
type PackageJson = {
|
|
version?: string;
|
|
};
|
|
|
|
type GitDescribeCommand = () => string;
|
|
type DebugLog = (fields: Record<string, unknown>, message: string) => void;
|
|
|
|
const requirePackage = createRequire(import.meta.url);
|
|
const pkg = requirePackage("../package.json") as PackageJson;
|
|
|
|
const GIT_DESCRIBE_RE =
|
|
/^v(?<publicVersion>\d+\.\d+\.\d+)-(?<commitsSinceTag>\d+)-g(?<sha>[0-9a-f]{7,40})(?<dirty>-dirty)?$/i;
|
|
|
|
function defaultDebugLog(fields: Record<string, unknown>, message: string): void {
|
|
console.debug(message, fields);
|
|
}
|
|
|
|
function defaultGitDescribeCommand(): string {
|
|
return execFileSync(
|
|
"git",
|
|
["describe", "--tags", "--match", "v*", "--long", "--dirty"],
|
|
{
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
timeout: 1500,
|
|
},
|
|
);
|
|
}
|
|
|
|
export function parseGitDescribeVersion(output: string): string | null {
|
|
const match = output.trim().match(GIT_DESCRIBE_RE);
|
|
if (!match?.groups) return null;
|
|
|
|
const publicVersion = match.groups.publicVersion;
|
|
const commitsSinceTag = match.groups.commitsSinceTag;
|
|
const sha = match.groups.sha;
|
|
const isDirty = Boolean(match.groups.dirty);
|
|
|
|
if (commitsSinceTag === "0" && !isDirty) {
|
|
return publicVersion;
|
|
}
|
|
|
|
return `${publicVersion}+${commitsSinceTag}.git.${sha}${isDirty ? ".dirty" : ""}`;
|
|
}
|
|
|
|
export function resolveServerVersion(
|
|
opts: {
|
|
gitDescribeCommand?: GitDescribeCommand;
|
|
packageVersion?: string;
|
|
debugLog?: DebugLog;
|
|
} = {},
|
|
): string {
|
|
const packageVersion = opts.packageVersion ?? pkg.version ?? "0.0.0";
|
|
const gitDescribeCommand = opts.gitDescribeCommand ?? defaultGitDescribeCommand;
|
|
const debugLog = opts.debugLog ?? defaultDebugLog;
|
|
|
|
try {
|
|
const parsedVersion = parseGitDescribeVersion(gitDescribeCommand());
|
|
if (parsedVersion) return parsedVersion;
|
|
|
|
debugLog(
|
|
{ reason: "invalid_git_describe" },
|
|
"falling back to package version for server version",
|
|
);
|
|
} catch (err) {
|
|
debugLog(
|
|
{ err, reason: "git_describe_unavailable" },
|
|
"falling back to package version for server version",
|
|
);
|
|
}
|
|
|
|
return packageVersion;
|
|
}
|
|
|
|
export const serverVersion = resolveServerVersion();
|