diff --git a/.eslintrc.js b/.eslintrc.js
index c9138b5c43f497fead2465087d6017de86cab9fc..19699d6632e41634a4c0cc56b59401f7ea1f9ee1 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -136,7 +136,7 @@ module.exports = {
       },
     },
     {
-      files: ['**/*.mjs'],
+      files: ['**/*.{js,mjs}'],
 
       rules: {
         '@typescript-eslint/explicit-function-return-type': 0,
@@ -144,5 +144,31 @@ module.exports = {
         '@typescript-eslint/restrict-template-expressions': 0,
       },
     },
+    {
+      files: ['tools/**/*.{ts,js,mjs}'],
+      env: {
+        node: true,
+      },
+      rules: {
+        'import/no-extraneous-dependencies': [
+          'error',
+          { devDependencies: true },
+        ],
+      },
+    },
+    {
+      files: ['tools/**/*.js'],
+      rules: {
+        // need commonjs
+        '@typescript-eslint/no-var-requires': 'off',
+      },
+    },
+    {
+      files: ['*.mjs'],
+      rules: {
+        // esm always requires extensions
+        'import/extensions': 0,
+      },
+    },
   ],
 };
diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml
index 42f82b83178ea07fcd89e8b01cd00fe62c27a98b..980b7fc97c05e7590fd2be1b11b5d56492455c7b 100644
--- a/.github/workflows/build-pr.yml
+++ b/.github/workflows/build-pr.yml
@@ -97,7 +97,7 @@ jobs:
       - name: Lint
         run: |
           yarn ls-lint
-          yarn eslint
+          yarn eslint -f gha
           yarn prettier
           yarn markdown-lint
 
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 5ce8c1f366c5e175fe9b174d42f6f8526611c0d7..56e1549952fbc5a9a1cfb24e4d6e4f5ee0dda174 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -131,7 +131,7 @@ jobs:
       - name: Lint
         run: |
           yarn ls-lint
-          yarn eslint -f ./tmp/tools/eslint-gh-reporter.js
+          yarn eslint -f gha
           yarn prettier
           yarn markdown-lint
 
diff --git a/jest.config.ts b/jest.config.ts
index b36ecdf882a2a2feb08bbbcfa4484815b1e10b8c..6459ff6288ae05233a4da78947bf6f2faa001529 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -25,7 +25,7 @@ const config: InitialOptionsTsJest = {
     },
   },
   modulePathIgnorePatterns: ['<rootDir>/dist/', '/__fixtures__/'],
-  reporters: ['default', './tmp/tools/jest-gh-reporter.js'],
+  reporters: ci ? ['default', 'jest-github-actions-reporter'] : ['default'],
   setupFilesAfterEnv: ['jest-extended', '<rootDir>/test/setup.ts'],
   snapshotSerializers: ['<rootDir>/test/newline-snapshot-serializer.ts'],
   testEnvironment: 'node',
diff --git a/package.json b/package.json
index 8df2075186938a1c2a8ce79e238e939b2cf9bbac..232c06d18003a24f17920198fdf8cc25995e2273 100644
--- a/package.json
+++ b/package.json
@@ -31,7 +31,6 @@
     "prepare:husky": "husky install",
     "prepare:generate": "run-s generate:*",
     "prepare:re2": "node tools/check-re2.mjs",
-    "prepare:tools": "tsc -p tools",
     "prestart": "run-s generate:* ",
     "pretest": "run-s generate:* ",
     "prettier": "prettier --check \"**/*.{ts,js,mjs,json,md}\"",
@@ -144,10 +143,10 @@
     "dequal": "2.0.2",
     "detect-indent": "6.1.0",
     "email-addresses": "4.0.0",
-    "extract-zip": "2.0.1",
     "emoji-regex": "9.2.2",
     "emojibase": "5.2.0",
     "emojibase-regex": "5.1.3",
+    "extract-zip": "2.0.1",
     "fast-safe-stringify": "2.0.8",
     "find-up": "5.0.0",
     "fs-extra": "10.0.0",
@@ -244,6 +243,7 @@
     "eslint": "7.32.0",
     "eslint-config-airbnb-typescript": "12.3.1",
     "eslint-config-prettier": "8.3.0",
+    "eslint-formatter-gha": "1.2.0",
     "eslint-plugin-import": "2.24.1",
     "eslint-plugin-jest": "24.4.0",
     "eslint-plugin-promise": "5.1.0",
@@ -252,6 +252,7 @@
     "husky": "7.0.1",
     "jest": "27.0.6",
     "jest-extended": "0.11.5",
+    "jest-github-actions-reporter": "1.0.3",
     "jest-junit": "12.2.0",
     "jest-mock-extended": "1.0.18",
     "jest-silent-reporter": "0.5.0",
diff --git a/tools/eslint-gh-reporter.ts b/tools/eslint-gh-reporter.ts
deleted file mode 100644
index 4d9ab6452770a578779d59569c8263bb134ab764..0000000000000000000000000000000000000000
--- a/tools/eslint-gh-reporter.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { relative } from 'path';
-import { error } from '@actions/core';
-import { issueCommand } from '@actions/core/lib/command';
-import { CLIEngine, Linter } from 'eslint';
-import stripAnsi from 'strip-ansi';
-
-const ROOT = process.cwd();
-
-type Level = 'debug' | 'warning' | 'error';
-
-function getCmd(severity: Linter.Severity): Level {
-  switch (severity) {
-    case 2:
-      return 'error';
-    case 1:
-      return 'warning';
-    default:
-      return 'debug';
-  }
-}
-
-function getPath(path: string): string {
-  return relative(ROOT, path).replace(/\\/g, '/');
-}
-
-const formatter: CLIEngine.Formatter = (results) => {
-  try {
-    for (const { filePath, messages } of results) {
-      const file = getPath(filePath);
-      for (const { severity, line, column, ruleId, message } of messages) {
-        const cmd = getCmd(severity);
-        const pos = { line: line.toString(), col: column.toString() };
-        issueCommand(
-          cmd,
-          { file, ...pos },
-          stripAnsi(`[${ruleId}] ${message}`)
-        );
-      }
-    }
-  } catch (e) {
-    error(`Unexpected error: ${(e as Error).toString()}`);
-  }
-  return '';
-};
-
-export = formatter;
diff --git a/tools/jest-gh-reporter.ts b/tools/jest-gh-reporter.ts
deleted file mode 100644
index a6c154690b34b317506a1d979d3dab660defdec3..0000000000000000000000000000000000000000
--- a/tools/jest-gh-reporter.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import { relative } from 'path';
-import { error } from '@actions/core';
-import { issueCommand } from '@actions/core/lib/command';
-import { AggregatedResult, BaseReporter, Context } from '@jest/reporters';
-import { AssertionResult, TestResult } from '@jest/test-result';
-import stripAnsi from 'strip-ansi';
-import { getEnv } from './utils';
-
-const ROOT = process.cwd();
-
-type Level = 'debug' | 'warning' | 'error';
-
-function getCmd(test: AssertionResult): Level {
-  switch (test.status) {
-    case 'failed':
-      return 'error';
-    case 'pending':
-    case 'todo':
-      return 'warning';
-    default:
-      return 'debug';
-  }
-}
-
-function getPath(suite: TestResult): string {
-  return relative(ROOT, suite.testFilePath).replace(/\\/g, '/');
-}
-
-const ignoreStates = new Set(['passed', 'pending']);
-const lineRe = /\.spec\.ts:(?<line>\d+):(?<col>\d+)\)/;
-
-function getPos(msg: string): Record<string, string> {
-  const pos = lineRe.exec(msg);
-  if (!pos || !pos.groups) {
-    return {};
-  }
-
-  const line = pos.groups.line;
-  const col = pos.groups.col;
-
-  return {
-    line,
-    col,
-  };
-}
-
-class GitHubReporter extends BaseReporter {
-  // eslint-disable-next-line class-methods-use-this
-  override onRunComplete(
-    _contexts: Set<Context>,
-    testResult: AggregatedResult
-  ): void {
-    try {
-      if (getEnv('GITHUB_ACTIONS') !== 'true') {
-        return;
-      }
-
-      for (const suite of testResult.testResults.filter((s) => !s.skipped)) {
-        const file = getPath(suite);
-        for (const test of suite.testResults.filter(
-          (t) => !ignoreStates.has(t.status)
-        )) {
-          const message =
-            stripAnsi(test.failureMessages?.join('\n ')) ||
-            `test status: ${test.status}`;
-          const pos = getPos(message);
-          const cmd = getCmd(test);
-
-          issueCommand(cmd, { file, ...pos }, message);
-        }
-      }
-    } catch (e) {
-      error(`Unexpected error: ${(e as Error).toString()}`);
-    }
-  }
-}
-
-export = GitHubReporter;
diff --git a/tools/package.json b/tools/package.json
deleted file mode 100644
index 72f4771090c27050e9175875bbbef3ea552de1c2..0000000000000000000000000000000000000000
--- a/tools/package.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
-  "private": true,
-  "type": "module",
-  "dependencies": {
-    "@actions/core": "1.5.0",
-    "@jest/reporters": "27.0.6",
-    "@jest/test-result": "27.0.6",
-    "commander": "7.2.0",
-    "eslint": "7.32.0",
-    "fs-extra": "10.0.0",
-    "got": "11.8.2",
-    "lodash": "4.17.21",
-    "shelljs": "0.8.4",
-    "strip-ansi": "6.0.0",
-    "upath": "2.0.1"
-  },
-  "devDependencies": {
-    "@types/graceful-fs": "4.1.5"
-  }
-}
diff --git a/tools/tsconfig.json b/tools/tsconfig.json
deleted file mode 100644
index a233462919b21766db0fc831a799c61bb1937e17..0000000000000000000000000000000000000000
--- a/tools/tsconfig.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
-  "compilerOptions": {
-    "baseUrl": ".",
-    "outDir": "../tmp/tools",
-    "strict": true,
-    "target": "es2018",
-    "module": "commonjs",
-    "allowSyntheticDefaultImports": true,
-    "esModuleInterop": true,
-    "resolveJsonModule": false,
-    "lib": ["es2018"],
-    "types": ["node"],
-    "incremental": true
-  }
-}
diff --git a/yarn.lock b/yarn.lock
index 426ae5bcb6d5ff743653a250131eab576c8619f7..4555f38e46c48a9e53e90f56574b09f66fd444b7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,7 +2,7 @@
 # yarn lockfile v1
 
 
-"@actions/core@1.5.0":
+"@actions/core@1.5.0", "@actions/core@^1.2.0", "@actions/core@^1.2.6":
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.5.0.tgz#885b864700001a1b9a6fba247833a036e75ad9d3"
   integrity sha512-eDOLH1Nq9zh+PJlYLqEMkS/jLQxhksPNmUGNBHfa4G+tQmnIhzpctxmchETtVGyBOvXgOVVpYuE40+eS4cUnwQ==
@@ -3937,6 +3937,13 @@ eslint-config-prettier@8.3.0:
   resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz#f7471b20b6fe8a9a9254cc684454202886a2dd7a"
   integrity sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==
 
+eslint-formatter-gha@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/eslint-formatter-gha/-/eslint-formatter-gha-1.2.0.tgz#ce52de9f9b17c65dc138482c0990fd2a158548be"
+  integrity sha512-teAorPqDzHzzYNCQCptdoYsnc+CZFXYcdB4v3dTi4Yqe8V64aShsbvtvPtYuJnKBoN6AwPzZ9P4D7R0hdQ5bcA==
+  dependencies:
+    "@actions/core" "^1.2.6"
+
 eslint-import-resolver-node@^0.3.6:
   version "0.3.6"
   resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd"
@@ -5619,6 +5626,13 @@ jest-get-type@^27.0.6:
   resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.0.6.tgz#0eb5c7f755854279ce9b68a9f1a4122f69047cfe"
   integrity sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg==
 
+jest-github-actions-reporter@1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/jest-github-actions-reporter/-/jest-github-actions-reporter-1.0.3.tgz#6aa2b3a6599352e1043bbe42628a7f73a1ce48c2"
+  integrity sha512-IwLAKLSWLN8ZVfcfEEv6rfeWb78wKDeOhvOmH9KKXayKsKLSCwceopBcB+KUtwxfB5wYnT8Y9s2eZ+WdhA5yng==
+  dependencies:
+    "@actions/core" "^1.2.0"
+
 jest-haste-map@^27.0.6:
   version "27.0.6"
   resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.0.6.tgz#4683a4e68f6ecaa74231679dca237279562c8dc7"