Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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/nice-parrots-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hardhat": patch
---

Expose `Result` type for task action success/failure signaling.
2 changes: 2 additions & 0 deletions v-next/hardhat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
"./types/solidity": "./dist/src/types/solidity.js",
"./console.sol": "./console.sol",
"./utils/contract-names": "./dist/src/utils/contract-names.js",
"./types/result": "./dist/src/types/result.js",
Comment thread
schaable marked this conversation as resolved.
Outdated
"./utils/result": "./dist/src/utils/result.js",
"./types/runtime": "./dist/src/internal/deprecated-module-imported-from-hardhat2-plugin.js",
"./builtin-tasks/task-names": "./dist/src/internal/deprecated-module-imported-from-hardhat2-plugin.js",
"./internal/artifacts": "./dist/src/internal/deprecated-module-imported-from-hardhat2-plugin.js"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import type {
Remapping,
ResolvedNpmUserRemapping,
ResolvedUserRemapping,
Result,
} from "./types.js";
import type { Result } from "../../../../../types/result.js";
import type {
ImportResolutionError,
NpmRootResolutionError,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import type {
ResolvedUserRemapping,
UnresolvedNpmUserRemapping,
RemappedNpmPackagesGraphJson,
Result,
} from "./types.js";
import type { Result } from "../../../../../types/result.js";
import type {
ResolvedFile,
ResolvedNpmPackage,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Result } from "../../../../../types/result.js";
import type {
ImportResolutionError,
NpmRootResolutionError,
Expand All @@ -11,13 +12,6 @@ import type {
ResolvedNpmPackage,
} from "../../../../../types/solidity/resolved-file.js";

/**
* A result that can either have a value or an error.
*/
export type Result<ValueT, ErrorT> =
| { readonly success: true; readonly value: ValueT }
| { readonly success: false; readonly error: ErrorT };

/**
* A solc remapping.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Result } from "./types.js";
import type { Result } from "../../../../../types/result.js";
import type { ResolvedNpmPackage } from "../../../../../types/solidity.js";

import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors";
Expand Down
10 changes: 9 additions & 1 deletion v-next/hardhat/src/internal/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
type OptionDefinition,
type PositionalArgumentDefinition,
} from "../../types/arguments.js";
import { isResult } from "../../utils/result.js";
import { BUILTIN_GLOBAL_OPTIONS_DEFINITIONS } from "../builtin-global-options.js";
import { builtinPlugins } from "../builtin-plugins/index.js";
import {
Expand Down Expand Up @@ -217,7 +218,14 @@ export async function main(

log(`Running task "${task.id.join(" ")}"`);

await Promise.all([task.run(taskArguments), sendTaskAnalytics(task.id)]);
const [taskResult] = await Promise.all([
task.run(taskArguments),
sendTaskAnalytics(task.id),
]);

if (isResult(taskResult) && !taskResult.success) {
Comment thread
schaable marked this conversation as resolved.
process.exitCode = 1;
}
} catch (error) {
ensureError(error);
printErrorMessages(error, builtinGlobalOptions?.showStackTraces);
Expand Down
1 change: 1 addition & 0 deletions v-next/hardhat/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export * from "./solidity.js";
export * from "./tasks.js";
export * from "./test.js";
export * from "./user-interruptions.js";
export * from "./result.js";
export * from "./utils.js";
6 changes: 6 additions & 0 deletions v-next/hardhat/src/types/result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* A result that can either have a value or an error.
*/
export type Result<ValueT, ErrorT> =
| { readonly success: true; readonly value: ValueT }
| { readonly success: false; readonly error: ErrorT };
57 changes: 57 additions & 0 deletions v-next/hardhat/src/utils/result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { Result } from "../types/result.js";

import { isObject } from "@nomicfoundation/hardhat-utils/lang";

/**
* Creates a successful Result with the given value.
*
* @param value The value to include in the result.
* @returns A Result with success: true and the given value.
*/
export function successResult<ValueT = undefined>(
Comment thread
schaable marked this conversation as resolved.
Outdated
...args: ValueT extends undefined | void ? [] : [value: ValueT]
): { success: true; value: ValueT } {
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions
-- Cast needed because TS loses the conditional rest parameter context */
return { success: true, value: args[0] as ValueT };
}

/**
* Creates a failed Result with the given error.
*
* @param error The error to include in the result.
* @returns A Result with success: false and the given error.
*/
export function errorResult<ErrorT = undefined>(
...args: ErrorT extends undefined | void ? [] : [error: ErrorT]
): { success: false; error: ErrorT } {
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions
-- Cast needed because TS loses the conditional rest parameter context */
return { success: false, error: args[0] as ErrorT };
}

/**
* A type guard that checks if a value is a Result.
*
* Optionally validates the value and error fields using type guard functions.
*
* @param value The value to check.
* @param isValue Optional type guard for the value field.
* @param isError Optional type guard for the error field.
* @returns true if the value is a Result.
*/
export function isResult<ValueT = unknown, ErrorT = unknown>(
value: unknown,
isValue?: (v: unknown) => v is ValueT,
isError?: (e: unknown) => e is ErrorT,
): value is Result<ValueT, ErrorT> {
if (!isObject(value) || typeof value.success !== "boolean") {
return false;
}

if (value.success === true) {
return "value" in value && (isValue === undefined || isValue(value.value));
}

Comment thread
schaable marked this conversation as resolved.
return "error" in value && (isError === undefined || isError(value.error));
}
51 changes: 51 additions & 0 deletions v-next/hardhat/test/fixture-projects/cli/result/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { HardhatUserConfig } from "../../../../src/config.js";

import { task } from "../../../../src/config.js";
import { errorResult, successResult } from "../../../../src/utils/result.js";

const failingTask = task("failing-task")
.setInlineAction(() => {
return errorResult();
})
.build();

const succeedingTask = task("succeeding-task")
.setInlineAction(() => {
return successResult(42);
})
.build();

const undefinedTask = task("undefined-task")
.setInlineAction(() => {})
.build();

const plainObjectTask = task("plain-object-task")
.setInlineAction(() => {
return { failed: 2, passed: 5 };
})
.build();

const failingTaskWithValue = task("failing-task-with-value")
.setInlineAction(() => {
return errorResult({ failed: 2, passed: 5 });
})
.build();

const succeedingTaskNoValue = task("succeeding-task-no-value")
.setInlineAction(() => {
return successResult();
})
.build();

const config: HardhatUserConfig = {
tasks: [
failingTask,
succeedingTask,
undefinedTask,
plainObjectTask,
failingTaskWithValue,
succeedingTaskNoValue,
],
};

export default config;
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Result } from "../../../../../../src/types/result.js";
import type {
ImportResolutionError,
NpmRootResolutionError,
Expand All @@ -17,7 +18,6 @@ import { readUtf8File } from "@nomicfoundation/hardhat-utils/fs";
import { ResolverImplementation } from "../../../../../../src/internal/builtin-plugins/solidity/build-system/resolver/dependency-resolver.js";
import {
type Resolver,
type Result,
UserRemappingType,
} from "../../../../../../src/internal/builtin-plugins/solidity/build-system/resolver/types.js";
import {
Expand Down
39 changes: 39 additions & 0 deletions v-next/hardhat/test/internal/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1767,4 +1767,43 @@ GLOBAL OPTIONS:
assert.deepEqual(parsedArgs, ["task", "-a=value1"]);
});
});

describe("Result handling", function () {
useFixtureProject("cli/result");

afterEach(function () {
process.exitCode = undefined;
resetGlobalHardhatRuntimeEnvironment();
});

it("should set process.exitCode = 1 when task returns Result with success: false", async function () {
await runMain("npx hardhat failing-task");
assert.equal(process.exitCode, 1);
});

it("should not set process.exitCode when task returns Result with success: true", async function () {
await runMain("npx hardhat succeeding-task");
assert.equal(process.exitCode, undefined);
});

it("should not set process.exitCode when task returns undefined", async function () {
await runMain("npx hardhat undefined-task");
assert.equal(process.exitCode, undefined);
});

it("should not set process.exitCode when task returns a plain object", async function () {
await runMain("npx hardhat plain-object-task");
assert.equal(process.exitCode, undefined);
});

it("should set process.exitCode = 1 when task returns Result with success: false and an error", async function () {
await runMain("npx hardhat failing-task-with-value");
assert.equal(process.exitCode, 1);
});

it("should not set process.exitCode when task returns Result with success: true and no value", async function () {
await runMain("npx hardhat succeeding-task-no-value");
assert.equal(process.exitCode, undefined);
});
});
Comment thread
schaable marked this conversation as resolved.
});
Loading