Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-pandas-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@neuledge/context": minor
---

Add `isMissingRefError` helper to detect git "ref not found" errors (e.g. when a registry publishes a version before its git tag is pushed)
22 changes: 22 additions & 0 deletions packages/context/src/git.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import {
findLatestStableVersion,
isMissingRefError,
isTransientGitError,
parseMonorepoTag,
} from "./git.js";
Expand Down Expand Up @@ -39,6 +40,27 @@ describe("isTransientGitError", () => {
});
});

describe("isMissingRefError", () => {
it("detects a tag not pushed yet (npm ahead of git)", () => {
expect(
isMissingRefError(
"Git clone failed: Cloning into '/tmp/context-git-9itQYk'...\nfatal: Remote branch v4.1.9 not found in upstream origin",
),
).toBe(true);
});

it("detects a missing pathspec/reference on checkout", () => {
expect(
isMissingRefError("error: pathspec 'v4.1.9' did not match any file(s)"),
).toBe(true);
});

it("rejects transient and unrelated errors", () => {
expect(isMissingRefError("Could not resolve host: github.com")).toBe(false);
expect(isMissingRefError("remote: Repository not found.")).toBe(false);
});
});

describe("parseMonorepoTag", () => {
it("parses plain version tags", () => {
expect(parseMonorepoTag("1.2.3")).toEqual({
Expand Down
19 changes: 19 additions & 0 deletions packages/context/src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,25 @@ export function isTransientGitError(message: string): boolean {
return TRANSIENT_GIT_ERROR_PATTERNS.some((re) => re.test(message));
}

/**
* Git error patterns indicating the requested ref (tag/branch) doesn't exist
* in the remote. Happens when a registry (e.g. npm) publishes a version before
* the matching git tag is pushed — a transient state that self-heals once the
* tag lands, so callers can skip rather than hard-fail.
*/
const MISSING_REF_GIT_ERROR_PATTERNS = [
/remote branch .* not found in upstream/i,
/could not find remote (branch|ref)/i,
/(pathspec|reference) .* did not match/i,
];

/**
* Check if a git error message indicates the requested ref doesn't exist.
*/
export function isMissingRefError(message: string): boolean {
return MISSING_REF_GIT_ERROR_PATTERNS.some((re) => re.test(message));
}

/**
* Synchronous sleep (cloneRepository is sync, so no await available).
*/
Expand Down
1 change: 1 addition & 0 deletions packages/context/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export {
extractVersion,
type GitCloneResult,
isGitUrl,
isMissingRefError,
type LocalDocsResult,
parseGitUrl,
readLocalDocsFiles,
Expand Down
8 changes: 8 additions & 0 deletions packages/registry/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { mkdirSync, rmSync } from "node:fs";
import { resolve } from "node:path";
import { isMissingRefError } from "@neuledge/context";
import { Command } from "commander";
import {
buildFromDefinition,
Expand Down Expand Up @@ -291,6 +292,13 @@ program
succeeded++;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
// A registry can publish a version before its git tag is pushed.
// Skip (don't fail) — the next run picks it up once the tag lands.
if (isMissingRefError(message)) {
console.log(` Skipping ${id} (git tag not published yet)`);
skipped++;
continue;
}
console.error(` FAILED ${id}: ${message}`);
failures.push({ id, error: message });
}
Expand Down
Loading