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/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.
1 change: 1 addition & 0 deletions v-next/hardhat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"./types/solidity": "./dist/src/types/solidity.js",
"./console.sol": "./console.sol",
"./utils/contract-names": "./dist/src/utils/contract-names.js",
"./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,7 +4,6 @@ import type {
Remapping,
ResolvedNpmUserRemapping,
ResolvedUserRemapping,
Result,
} from "./types.js";
import type {
ImportResolutionError,
Expand All @@ -20,6 +19,7 @@ import type {
ProjectResolvedFile,
NpmPackageResolvedFile,
} from "../../../../../types/solidity/resolved-file.js";
import type { Result } from "../../../../../types/utils.js";

import path from "node:path";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import type {
ResolvedUserRemapping,
UnresolvedNpmUserRemapping,
RemappedNpmPackagesGraphJson,
Result,
} from "./types.js";
import type {
ResolvedFile,
ResolvedNpmPackage,
UserRemappingError,
} from "../../../../../types/solidity.js";
import type { Result } from "../../../../../types/utils.js";

import path from "node:path";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,7 @@ import type {
ResolvedFile,
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 };
import type { Result } from "../../../../../types/utils.js";

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

import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors";
import { ensureError } from "@nomicfoundation/hardhat-utils/error";
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
10 changes: 10 additions & 0 deletions v-next/hardhat/src/types/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type {
PositionalArgumentDefinition,
} from "./arguments.js";
import type { HardhatRuntimeEnvironment } from "./hre.js";
/* eslint-disable-next-line @typescript-eslint/no-unused-vars -- used in JSDoc {@link} */
Comment thread
schaable marked this conversation as resolved.
import type { Result } from "./utils.js";

// We add the TaskManager to the HRE with a module augmentation to avoid
// introducing a circular dependency that would look like this:
Expand Down Expand Up @@ -209,6 +211,10 @@ export interface NewTaskDefinitionBuilder<
*
* This method cannot be used together with {@link setInlineAction} on the same
* task. Use one or the other.
*
* Task actions may return a {@link Result} to signal success or failure.
* If a task returns a failed `Result`, the CLI will set the process exit code
* to 1.
*/
setAction(
action: LazyActionObject<NewTaskActionFunction<TaskArgumentsT>>,
Expand All @@ -225,6 +231,10 @@ export interface NewTaskDefinitionBuilder<
*
* This method cannot be used together with {@link setAction} on the same
* task. Use one or the other.
*
* Task actions may return a {@link Result} to signal success or failure.
* If a task returns a failed `Result`, the CLI will set the process exit code
* to 1.
*/
setInlineAction(
inlineAction: NewTaskActionFunction<TaskArgumentsT>,
Expand Down
7 changes: 7 additions & 0 deletions v-next/hardhat/src/types/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,10 @@ export type Return<T> = T extends (...args: any[]) => infer Ret ? Ret : never;
export type RequireField<T, K extends keyof T> = T & {
[P in K]-?: T[P];
};

/**
* 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/utils.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 successfulResult<ValueT = undefined>(
...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, successfulResult } from "../../../../src/utils/result.js";

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

const succeedingTask = task("succeeding-task")
.setInlineAction(() => {
return successfulResult(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 successfulResult();
})
.build();

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

export default config;
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
ProjectResolvedFile,
ResolvedFile,
} from "../../../../../../src/types/solidity/resolved-file.js";
import type { Result } from "../../../../../../src/types/utils.js";

import assert from "node:assert/strict";
import path from "node:path";
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
51 changes: 51 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,55 @@ 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 override process.exitCode to 1 when task returns Result with success: false", async function () {
process.exitCode = 123;
Comment thread
schaable marked this conversation as resolved.
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 override process.exitCode when task returns Result with success: true", async function () {
process.exitCode = 123;
await runMain("npx hardhat succeeding-task");
assert.equal(process.exitCode, 123);
});

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
Loading