Skip to content

Commit fc3818b

Browse files
Marketenclaude
andcommitted
Detect an unregistered package with current ethers
Publishing a package for the first time fails with: execution reverted: ResolverNotFound(bytes) Having no APM repository yet is the expected state for a first publish, and both call sites already handle it: one falls back to the manifest version, the other builds a newRepoWithVersion transaction. They recognise the condition by matching the error text, and the text changed. @dappnode/toolkit resolves the ENS name through ethers ^6.9.0, so the exact ethers release is decided at install time. Current releases decode the ENS custom error and raise ResolverNotFound, where earlier ones reported NOREPO or "Could not resolve name". Neither guard matches, so the error propagates and the publish aborts. Pinning an older dappnodesdk does not help, because the toolkit and ethers ranges float independently of it. Match all three spellings from one helper. Errors that reverted for any other reason are deliberately not matched, so a genuine registry failure is never mistaken for an unpublished package and silently restarted from the manifest version. Verified against mainnet: an unregistered name now falls back to the manifest version, and a registered one still reads its on-chain version and bumps it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 072d289 commit fc3818b

5 files changed

Lines changed: 541 additions & 543 deletions

File tree

‎src/tasks/generatePublishTxs/index.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import repoAbi from "../../contracts/RepoAbi.json" with { type: "json" };
1212
import { VerbosityOptions } from "../../commands/build/types.js";
1313
import { getRegistryAddressFromDnpName } from "./getRegistryAddressFromEns.js";
1414
import { Repo } from "./types.js";
15+
import { isPackageNotRegisteredError } from "../../utils/isPackageNotRegisteredError.js";
1516

1617
const isZeroAddress = (address: string): boolean => parseInt(address) === 0;
1718

@@ -145,7 +146,8 @@ async function getRepoContractIfExists({
145146
try {
146147
return await apm.getRepoContract(ensName);
147148
} catch (e) {
148-
if (e.message.includes("Could not resolve name")) return null;
149+
// No repository yet: the caller builds a newRepoWithVersion transaction.
150+
if (isPackageNotRegisteredError(e)) return null;
149151
else throw e;
150152
}
151153
}

‎src/tasks/publish/subtasks/getFetchApmVersionsTask.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ListrTask } from "listr";
22
import { PackageToBuildProps, ListrContextPublish, ReleaseType } from "../../../types.js";
33
import { getNextVersionFromApm } from "../../../utils/versions/getNextVersionFromApm.js";
44
import { Manifest } from "@dappnode/types";
5+
import { isPackageNotRegisteredError } from "../../../utils/isPackageNotRegisteredError.js";
56

67
export function getFetchNextVersionsFromApmTask({
78
releaseType,
@@ -53,7 +54,9 @@ export async function getNextPackageVersion({
5354
ensName: name
5455
});
5556
} catch (e) {
56-
if (e.message.includes("NOREPO")) return version;
57+
// Publishing this package for the first time: there is no repository to
58+
// read a current version from, so start from the manifest version.
59+
if (isPackageNotRegisteredError(e)) return version;
5760
else throw e;
5861
}
5962
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* True when an APM lookup failed because the package has no repository on the
3+
* registry yet, rather than because something actually went wrong.
4+
*
5+
* This is the normal state when publishing a package for the first time, and
6+
* callers use it to fall back to the version declared in the manifest.
7+
*
8+
* The condition has to be recognised from the error message because it surfaces
9+
* differently depending on the ethers version resolving the ENS name:
10+
*
11+
* - `NOREPO` is raised by @dappnode/toolkit when the name resolves but carries
12+
* no repository.
13+
* - `Could not resolve name` is what ethers v6 reported for an unresolvable
14+
* name in earlier releases.
15+
* - `ResolverNotFound` is the ENS custom error current ethers decodes and
16+
* raises instead. Without it, a first publish fails with
17+
* `execution reverted: ResolverNotFound(bytes)`.
18+
*
19+
* Errors that merely reverted for some other reason are deliberately not
20+
* matched, so a genuine registry failure is never mistaken for an unpublished
21+
* package.
22+
*/
23+
export function isPackageNotRegisteredError(error: unknown): boolean {
24+
const message =
25+
error instanceof Error ? error.message : typeof error === "string" ? error : "";
26+
27+
return (
28+
message.includes("NOREPO") ||
29+
message.includes("Could not resolve name") ||
30+
message.includes("ResolverNotFound")
31+
);
32+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { expect } from "chai";
2+
import { isPackageNotRegisteredError } from "../../src/utils/isPackageNotRegisteredError.js";
3+
4+
describe("isPackageNotRegisteredError", () => {
5+
// Each string is what an APM lookup for an unpublished package actually
6+
// produced, so the first publish must keep working across ethers versions.
7+
const notRegistered: { name: string; message: string }[] = [
8+
{
9+
name: "toolkit reports no repository",
10+
message: "Could not resolve name foo.dnp.dappnode.eth: NOREPO"
11+
},
12+
{
13+
name: "ethers v6 before the custom-error decoding",
14+
message: "Could not resolve name foo.dnp.dappnode.eth"
15+
},
16+
{
17+
name: "current ethers decodes the ENS custom error",
18+
message: "execution reverted: ResolverNotFound(bytes)"
19+
}
20+
];
21+
22+
for (const { name, message } of notRegistered) {
23+
it(`detects an unregistered package: ${name}`, () => {
24+
expect(isPackageNotRegisteredError(new Error(message))).to.equal(true);
25+
});
26+
}
27+
28+
// A registry problem must never be mistaken for an unpublished package, or
29+
// publish would silently restart versioning from the manifest.
30+
const realFailures: { name: string; message: string }[] = [
31+
{ name: "unrelated revert", message: "execution reverted: Unauthorized()" },
32+
{ name: "network failure", message: "could not detect network" },
33+
{ name: "bad response", message: "missing response for request" }
34+
];
35+
36+
for (const { name, message } of realFailures) {
37+
it(`does not swallow a real failure: ${name}`, () => {
38+
expect(isPackageNotRegisteredError(new Error(message))).to.equal(false);
39+
});
40+
}
41+
42+
it("tolerates a non-Error value", () => {
43+
expect(isPackageNotRegisteredError(undefined)).to.equal(false);
44+
expect(isPackageNotRegisteredError("execution reverted: ResolverNotFound(bytes)")).to.equal(true);
45+
});
46+
});

0 commit comments

Comments
 (0)