Compare commits

..

No commits in common. "master" and "v2.0.3" have entirely different histories.

19 changed files with 4652 additions and 2163 deletions

View File

@ -4,30 +4,11 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: weekly interval: weekly
groups:
npm:
patterns:
- "*"
ignore: ignore:
- dependency-name: node-fetch - dependency-name: node-fetch
versions: versions:
- ">=3.0.0" - ">=3.0.0"
# ignore mime and @types/mime per https://github.com/softprops/action-gh-release/pull/475
- dependency-name: mime
versions:
- ">=4.0.0"
- dependency-name: "@types/mime"
versions:
- ">=4.0.0"
commit-message:
prefix: "chore(deps)"
- package-ecosystem: github-actions - package-ecosystem: github-actions
directory: "/" directory: "/"
schedule: schedule:
interval: weekly interval: weekly
groups:
github-actions:
patterns:
- "*"
commit-message:
prefix: "chore(deps)"

22
.github/release.yml vendored
View File

@ -1,22 +0,0 @@
changelog:
exclude:
labels:
- ignore-for-release
- github-actions
authors:
- octocat
- renovate[bot]
categories:
- title: Breaking Changes 🛠
labels:
- breaking-change
- title: Exciting New Features 🎉
labels:
- enhancement
- feature
- title: Bug fixes 🐛
labels:
- bug
- title: Other Changes 🔄
labels:
- "*"

View File

@ -1,20 +1,14 @@
name: main name: Main
on: on: [pull_request, push]
push:
pull_request:
jobs: jobs:
build: build:
runs-on: ubuntu-24.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 # https://github.com/actions/checkout
- name: Checkout
- uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4 uses: actions/checkout@v4
with:
node-version-file: ".tool-versions"
cache: "npm"
- name: Install - name: Install
run: npm ci run: npm ci
- name: Build - name: Build

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
20.11.1

View File

@ -1 +0,0 @@
nodejs 20.15.1

View File

@ -1,91 +1,4 @@
## 2.2.1 ## 2.0.4 (unreleased)
## What's Changed
### Bug fixes 🐛
* fix: big file uploads by @xen0n in https://github.com/softprops/action-gh-release/pull/562
### Other Changes 🔄
* chore(deps): bump @types/node from 22.10.1 to 22.10.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/559
* chore(deps): bump @types/node from 22.10.2 to 22.10.5 by @dependabot in https://github.com/softprops/action-gh-release/pull/569
* chore: update error and warning messages for not matching files in files field by @ytimocin in https://github.com/softprops/action-gh-release/pull/568
## 2.2.0
## What's Changed
### Exciting New Features 🎉
* feat: read the release assets asynchronously by @xen0n in https://github.com/softprops/action-gh-release/pull/552
### Bug fixes 🐛
* fix(docs): clarify the default for tag_name by @alexeagle in https://github.com/softprops/action-gh-release/pull/544
### Other Changes 🔄
* chore(deps): bump typescript from 5.6.3 to 5.7.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/548
* chore(deps): bump @types/node from 22.9.0 to 22.9.4 by @dependabot in https://github.com/softprops/action-gh-release/pull/547
* chore(deps): bump cross-spawn from 7.0.3 to 7.0.6 by @dependabot in https://github.com/softprops/action-gh-release/pull/545
* chore(deps): bump @vercel/ncc from 0.38.2 to 0.38.3 by @dependabot in https://github.com/softprops/action-gh-release/pull/543
* chore(deps): bump prettier from 3.3.3 to 3.4.1 by @dependabot in https://github.com/softprops/action-gh-release/pull/550
* chore(deps): bump @types/node from 22.9.4 to 22.10.1 by @dependabot in https://github.com/softprops/action-gh-release/pull/551
* chore(deps): bump prettier from 3.4.1 to 3.4.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/554
## 2.1.0
## What's Changed
### Exciting New Features 🎉
* feat: add support for release assets with multiple spaces within the name by @dukhine in https://github.com/softprops/action-gh-release/pull/518
* feat: preserve upload order by @richarddd in https://github.com/softprops/action-gh-release/pull/500
### Other Changes 🔄
* chore(deps): bump @types/node from 22.8.2 to 22.8.7 by @dependabot in https://github.com/softprops/action-gh-release/pull/539
## 2.0.9
- maintenance release with updated dependencies
## 2.0.8
### Other Changes 🔄
* chore(deps): bump prettier from 2.8.0 to 3.3.3 by @dependabot in https://github.com/softprops/action-gh-release/pull/480
* chore(deps): bump @types/node from 20.14.9 to 20.14.11 by @dependabot in https://github.com/softprops/action-gh-release/pull/483
* chore(deps): bump @octokit/plugin-throttling from 9.3.0 to 9.3.1 by @dependabot in https://github.com/softprops/action-gh-release/pull/484
* chore(deps): bump glob from 10.4.2 to 11.0.0 by @dependabot in https://github.com/softprops/action-gh-release/pull/477
* refactor: write jest config in ts by @chenrui333 in https://github.com/softprops/action-gh-release/pull/485
* chore(deps): bump @actions/github from 5.1.1 to 6.0.0 by @dependabot in https://github.com/softprops/action-gh-release/pull/470
## 2.0.7
### Bug fixes 🐛
* Fix missing update release body by @FirelightFlagboy in https://github.com/softprops/action-gh-release/pull/365
### Other Changes 🔄
* Bump @octokit/plugin-retry from 4.0.3 to 7.1.1 by @dependabot in https://github.com/softprops/action-gh-release/pull/443
* Bump typescript from 4.9.5 to 5.5.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/467
* Bump @types/node from 20.14.6 to 20.14.8 by @dependabot in https://github.com/softprops/action-gh-release/pull/469
* Bump @types/node from 20.14.8 to 20.14.9 by @dependabot in https://github.com/softprops/action-gh-release/pull/473
* Bump typescript from 5.5.2 to 5.5.3 by @dependabot in https://github.com/softprops/action-gh-release/pull/472
* Bump ts-jest from 29.1.5 to 29.2.2 by @dependabot in https://github.com/softprops/action-gh-release/pull/479
* docs: document that existing releases are updated by @jvanbruegge in https://github.com/softprops/action-gh-release/pull/474
## 2.0.6
- maintenance release with updated dependencies
## 2.0.5
- Factor in file names with spaces when upserting files [#446](https://github.com/softprops/action-gh-release/pull/446) via [@MystiPanda](https://github.com/MystiPanda)
- Improvements to error handling [#449](https://github.com/softprops/action-gh-release/pull/449) via [@till](https://github.com/till)
## 2.0.4
- Minor follow up to [#417](https://github.com/softprops/action-gh-release/pull/417). [#425](https://github.com/softprops/action-gh-release/pull/425)
## 2.0.3 ## 2.0.3

View File

@ -21,16 +21,6 @@
<br /> <br />
- [🤸 Usage](#-usage)
- [🚥 Limit releases to pushes to tags](#-limit-releases-to-pushes-to-tags)
- [⬆️ Uploading release assets](#-uploading-release-assets)
- [📝 External release notes](#-external-release-notes)
- [💅 Customizing](#-customizing)
- [inputs](#inputs)
- [outputs](#outputs)
- [environment variables](#environment-variables)
- [Permissions](#permissions)
## 🤸 Usage ## 🤸 Usage
### 🚥 Limit releases to pushes to tags ### 🚥 Limit releases to pushes to tags
@ -54,7 +44,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
if: github.ref_type == 'tag' if: startsWith(github.ref, 'refs/tags/')
``` ```
You can also use push config tag filter You can also use push config tag filter
@ -85,7 +75,6 @@ GitHub release and all are optional.
A common case for GitHub releases is to upload your binary after its been validated and packaged. A common case for GitHub releases is to upload your binary after its been validated and packaged.
Use the `with.files` input to declare a newline-delimited list of glob expressions matching the files Use the `with.files` input to declare a newline-delimited list of glob expressions matching the files
you wish to upload to GitHub releases. If you'd like you can just list the files by name directly. you wish to upload to GitHub releases. If you'd like you can just list the files by name directly.
If a tag already has a GitHub release, the existing release will be updated with the release assets.
Below is an example of uploading a single asset named `Release.txt` Below is an example of uploading a single asset named `Release.txt`
@ -106,7 +95,7 @@ jobs:
run: cat Release.txt run: cat Release.txt
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
if: github.ref_type == 'tag' if: startsWith(github.ref, 'refs/tags/')
with: with:
files: Release.txt files: Release.txt
``` ```
@ -130,7 +119,7 @@ jobs:
run: cat Release.txt run: cat Release.txt
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
if: github.ref_type == 'tag' if: startsWith(github.ref, 'refs/tags/')
with: with:
files: | files: |
Release.txt Release.txt
@ -162,13 +151,14 @@ jobs:
run: echo "# Good things have arrived" > ${{ github.workspace }}-CHANGELOG.txt run: echo "# Good things have arrived" > ${{ github.workspace }}-CHANGELOG.txt
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
if: github.ref_type == 'tag' if: startsWith(github.ref, 'refs/tags/')
with: with:
body_path: ${{ github.workspace }}-CHANGELOG.txt body_path: ${{ github.workspace }}-CHANGELOG.txt
repository: my_gh_org/my_gh_repo
# note you'll typically need to create a personal access token # note you'll typically need to create a personal access token
# with permissions to create releases in the other repo # with permissions to create releases in the other repo
token: ${{ secrets.CUSTOM_GITHUB_TOKEN }} token: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
env:
GITHUB_REPOSITORY: my_gh_org/my_gh_repo
``` ```
### 💅 Customizing ### 💅 Customizing
@ -183,10 +173,9 @@ The following are optional as `step.with` keys
| `body_path` | String | Path to load text communicating notable changes in this release | | `body_path` | String | Path to load text communicating notable changes in this release |
| `draft` | Boolean | Indicator of whether or not this release is a draft | | `draft` | Boolean | Indicator of whether or not this release is a draft |
| `prerelease` | Boolean | Indicator of whether or not is a prerelease | | `prerelease` | Boolean | Indicator of whether or not is a prerelease |
| `preserve_order` | Boolean | Indicator of whether order of files should be preserved when uploading assets |
| `files` | String | Newline-delimited globs of paths to assets to upload for release | | `files` | String | Newline-delimited globs of paths to assets to upload for release |
| `name` | String | Name of the release. defaults to tag name | | `name` | String | Name of the release. defaults to tag name |
| `tag_name` | String | Name of a tag. defaults to `github.ref_name` | | `tag_name` | String | Name of a tag. defaults to `github.ref` |
| `fail_on_unmatched_files` | Boolean | Indicator of whether to fail if any of the `files` globs match nothing | | `fail_on_unmatched_files` | Boolean | Indicator of whether to fail if any of the `files` globs match nothing |
| `repository` | String | Name of a target repository in `<owner>/<repo>` format. Defaults to GITHUB_REPOSITORY env variable | | `repository` | String | Name of a target repository in `<owner>/<repo>` format. Defaults to GITHUB_REPOSITORY env variable |
| `target_commitish` | String | Commitish value that determines where the Git tag is created from. Can be any branch or commit SHA. Defaults to repository default branch. | | `target_commitish` | String | Commitish value that determines where the Git tag is created from. Can be any branch or commit SHA. Defaults to repository default branch. |

View File

@ -1,5 +1,6 @@
//import * as assert from "assert";
//const assert = require('assert');
import * as assert from "assert"; import * as assert from "assert";
import { text } from "stream/consumers";
import { mimeOrDefault, asset } from "../src/github"; import { mimeOrDefault, asset } from "../src/github";
describe("github", () => { describe("github", () => {
@ -14,10 +15,11 @@ describe("github", () => {
describe("asset", () => { describe("asset", () => {
it("derives asset info from a path", async () => { it("derives asset info from a path", async () => {
const { name, mime, size } = asset("tests/data/foo/bar.txt"); const { name, mime, size, data } = asset("tests/data/foo/bar.txt");
assert.equal(name, "bar.txt"); assert.equal(name, "bar.txt");
assert.equal(mime, "text/plain"); assert.equal(mime, "text/plain");
assert.equal(size, 10); assert.equal(size, 10);
assert.equal(data.toString(), "release me");
}); });
}); });
}); });

View File

@ -6,7 +6,6 @@ import {
parseInputFiles, parseInputFiles,
unmatchedPatterns, unmatchedPatterns,
uploadUrl, uploadUrl,
alignAssetName,
} from "../src/util"; } from "../src/util";
import * as assert from "assert"; import * as assert from "assert";
@ -15,9 +14,9 @@ describe("util", () => {
it("strips template", () => { it("strips template", () => {
assert.equal( assert.equal(
uploadUrl( uploadUrl(
"https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets{?name,label}", "https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets{?name,label}"
), ),
"https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets", "https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets"
); );
}); });
}); });
@ -31,7 +30,7 @@ describe("util", () => {
it("parses newline and comma-delimited (and then some)", () => { it("parses newline and comma-delimited (and then some)", () => {
assert.deepStrictEqual( assert.deepStrictEqual(
parseInputFiles("foo,bar\nbaz,boom,\n\ndoom,loom "), parseInputFiles("foo,bar\nbaz,boom,\n\ndoom,loom "),
["foo", "bar", "baz", "boom", "doom", "loom"], ["foo", "bar", "baz", "boom", "doom", "loom"]
); );
}); });
}); });
@ -47,7 +46,6 @@ describe("util", () => {
input_body_path: undefined, input_body_path: undefined,
input_draft: false, input_draft: false,
input_prerelease: false, input_prerelease: false,
input_preserve_order: undefined,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
@ -55,7 +53,7 @@ describe("util", () => {
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false,
input_make_latest: undefined, input_make_latest: undefined,
}), })
); );
}); });
it("uses input body path", () => { it("uses input body path", () => {
@ -69,7 +67,6 @@ describe("util", () => {
input_body_path: "__tests__/release.txt", input_body_path: "__tests__/release.txt",
input_draft: false, input_draft: false,
input_prerelease: false, input_prerelease: false,
input_preserve_order: undefined,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
@ -77,7 +74,7 @@ describe("util", () => {
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false,
input_make_latest: undefined, input_make_latest: undefined,
}), })
); );
}); });
it("defaults to body path when both body and body path are provided", () => { it("defaults to body path when both body and body path are provided", () => {
@ -91,7 +88,6 @@ describe("util", () => {
input_body_path: "__tests__/release.txt", input_body_path: "__tests__/release.txt",
input_draft: false, input_draft: false,
input_prerelease: false, input_prerelease: false,
input_preserve_order: undefined,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
@ -99,7 +95,7 @@ describe("util", () => {
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false,
input_make_latest: undefined, input_make_latest: undefined,
}), })
); );
}); });
}); });
@ -125,7 +121,6 @@ describe("util", () => {
input_body_path: undefined, input_body_path: undefined,
input_draft: undefined, input_draft: undefined,
input_prerelease: undefined, input_prerelease: undefined,
input_preserve_order: undefined,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
@ -134,7 +129,7 @@ describe("util", () => {
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false,
input_make_latest: undefined, input_make_latest: undefined,
}, }
); );
}); });
@ -153,7 +148,6 @@ describe("util", () => {
input_draft: undefined, input_draft: undefined,
input_prerelease: undefined, input_prerelease: undefined,
input_files: [], input_files: [],
input_preserve_order: undefined,
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
input_fail_on_unmatched_files: false, input_fail_on_unmatched_files: false,
@ -161,7 +155,7 @@ describe("util", () => {
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false,
input_make_latest: undefined, input_make_latest: undefined,
}, }
); );
}); });
it("supports discussion category names", () => { it("supports discussion category names", () => {
@ -179,7 +173,6 @@ describe("util", () => {
input_draft: undefined, input_draft: undefined,
input_prerelease: undefined, input_prerelease: undefined,
input_files: [], input_files: [],
input_preserve_order: undefined,
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
input_fail_on_unmatched_files: false, input_fail_on_unmatched_files: false,
@ -187,7 +180,7 @@ describe("util", () => {
input_discussion_category_name: "releases", input_discussion_category_name: "releases",
input_generate_release_notes: false, input_generate_release_notes: false,
input_make_latest: undefined, input_make_latest: undefined,
}, }
); );
}); });
@ -205,7 +198,6 @@ describe("util", () => {
input_body_path: undefined, input_body_path: undefined,
input_draft: undefined, input_draft: undefined,
input_prerelease: undefined, input_prerelease: undefined,
input_preserve_order: undefined,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
@ -214,7 +206,7 @@ describe("util", () => {
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: true, input_generate_release_notes: true,
input_make_latest: undefined, input_make_latest: undefined,
}, }
); );
}); });
@ -223,7 +215,6 @@ describe("util", () => {
parseConfig({ parseConfig({
INPUT_DRAFT: "false", INPUT_DRAFT: "false",
INPUT_PRERELEASE: "true", INPUT_PRERELEASE: "true",
INPUT_PRESERVE_ORDER: "true",
GITHUB_TOKEN: "env-token", GITHUB_TOKEN: "env-token",
INPUT_TOKEN: "input-token", INPUT_TOKEN: "input-token",
}), }),
@ -236,7 +227,6 @@ describe("util", () => {
input_body_path: undefined, input_body_path: undefined,
input_draft: false, input_draft: false,
input_prerelease: true, input_prerelease: true,
input_preserve_order: true,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
@ -245,7 +235,7 @@ describe("util", () => {
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false,
input_make_latest: undefined, input_make_latest: undefined,
}, }
); );
}); });
it("uses input token as the source of GITHUB_TOKEN by default", () => { it("uses input token as the source of GITHUB_TOKEN by default", () => {
@ -264,7 +254,6 @@ describe("util", () => {
input_body_path: undefined, input_body_path: undefined,
input_draft: false, input_draft: false,
input_prerelease: true, input_prerelease: true,
input_preserve_order: undefined,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
@ -273,7 +262,7 @@ describe("util", () => {
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false,
input_make_latest: undefined, input_make_latest: undefined,
}, }
); );
}); });
it("parses basic config with draft and prerelease", () => { it("parses basic config with draft and prerelease", () => {
@ -291,7 +280,6 @@ describe("util", () => {
input_body_path: undefined, input_body_path: undefined,
input_draft: false, input_draft: false,
input_prerelease: true, input_prerelease: true,
input_preserve_order: undefined,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
@ -300,7 +288,7 @@ describe("util", () => {
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false,
input_make_latest: undefined, input_make_latest: undefined,
}, }
); );
}); });
it("parses basic config where make_latest is passed", () => { it("parses basic config where make_latest is passed", () => {
@ -317,7 +305,6 @@ describe("util", () => {
input_body_path: undefined, input_body_path: undefined,
input_draft: undefined, input_draft: undefined,
input_prerelease: undefined, input_prerelease: undefined,
input_preserve_order: undefined,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
@ -326,7 +313,7 @@ describe("util", () => {
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false,
input_make_latest: "false", input_make_latest: "false",
}, }
); );
}); });
it("parses basic config with append_body", () => { it("parses basic config with append_body", () => {
@ -343,7 +330,6 @@ describe("util", () => {
input_body_path: undefined, input_body_path: undefined,
input_draft: undefined, input_draft: undefined,
input_prerelease: undefined, input_prerelease: undefined,
input_preserve_order: undefined,
input_files: [], input_files: [],
input_name: undefined, input_name: undefined,
input_tag_name: undefined, input_tag_name: undefined,
@ -352,7 +338,7 @@ describe("util", () => {
input_discussion_category_name: undefined, input_discussion_category_name: undefined,
input_generate_release_notes: false, input_generate_release_notes: false,
input_make_latest: undefined, input_make_latest: undefined,
}, }
); );
}); });
}); });
@ -369,7 +355,7 @@ describe("util", () => {
it("resolves files given a set of paths", async () => { it("resolves files given a set of paths", async () => {
assert.deepStrictEqual( assert.deepStrictEqual(
paths(["tests/data/**/*", "tests/data/does/not/exist/*"]), paths(["tests/data/**/*", "tests/data/does/not/exist/*"]),
["tests/data/foo/bar.txt"], ["tests/data/foo/bar.txt"]
); );
}); });
}); });
@ -378,24 +364,8 @@ describe("util", () => {
it("returns the patterns that don't match any files", async () => { it("returns the patterns that don't match any files", async () => {
assert.deepStrictEqual( assert.deepStrictEqual(
unmatchedPatterns(["tests/data/**/*", "tests/data/does/not/exist/*"]), unmatchedPatterns(["tests/data/**/*", "tests/data/does/not/exist/*"]),
["tests/data/does/not/exist/*"], ["tests/data/does/not/exist/*"]
); );
}); });
}); });
describe("replaceSpacesWithDots", () => {
it("replaces all spaces with dots", () => {
expect(alignAssetName("John Doe.bla")).toBe("John.Doe.bla");
});
it("handles names with multiple spaces", () => {
expect(alignAssetName("John William Doe.bla")).toBe(
"John.William.Doe.bla",
);
});
it("returns the same string if there are no spaces", () => {
expect(alignAssetName("JohnDoe")).toBe("JohnDoe");
});
});
}); });

View File

@ -13,7 +13,7 @@ inputs:
description: "Gives the release a custom name. Defaults to tag name" description: "Gives the release a custom name. Defaults to tag name"
required: false required: false
tag_name: tag_name:
description: "Gives a tag name. Defaults to github.ref_name" description: "Gives a tag name. Defaults to github.GITHUB_REF"
required: false required: false
draft: draft:
description: "Creates a draft release. Defaults to false" description: "Creates a draft release. Defaults to false"
@ -21,9 +21,6 @@ inputs:
prerelease: prerelease:
description: "Identify the release as a prerelease. Defaults to false" description: "Identify the release as a prerelease. Defaults to false"
required: false required: false
preserve_order:
description: "Preserver the order of the artifacts when uploading"
required: false
files: files:
description: "Newline-delimited list of path globs for asset files to upload" description: "Newline-delimited list of path globs for asset files to upload"
required: false required: false
@ -53,7 +50,7 @@ inputs:
description: "Specifies whether this release should be set as the latest release for the repository. Drafts and prereleases cannot be set as latest. Can be `true`, `false`, or `legacy`. Uses GitHub api default if not provided" description: "Specifies whether this release should be set as the latest release for the repository. Drafts and prereleases cannot be set as latest. Can be `true`, `false`, or `legacy`. Uses GitHub api default if not provided"
required: false required: false
env: env:
GITHUB_TOKEN: "As provided by Github Actions" "GITHUB_TOKEN": "As provided by Github Actions"
outputs: outputs:
url: url:
description: "URL to the Release HTML Page" description: "URL to the Release HTML Page"

10
dist/index.js vendored

File diff suppressed because one or more lines are too long

11
jest.config.js Normal file
View File

@ -0,0 +1,11 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
testRunner: 'jest-circus/runner',
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true
}

View File

@ -1,16 +0,0 @@
import type { JestConfigWithTsJest } from 'ts-jest';
const config: JestConfigWithTsJest = {
preset: 'ts-jest/presets/default-esm',
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
testRunner: 'jest-circus/runner',
transform: {
'^.+\\.ts$': 'ts-jest',
},
verbose: true,
};
export default config;

6232
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,14 @@
{ {
"name": "action-gh-release", "name": "action-gh-release",
"version": "2.2.1", "version": "2.0.3",
"private": true, "private": true,
"description": "GitHub Action for creating GitHub Releases", "description": "GitHub Action for creating GitHub Releases",
"main": "lib/main.js", "main": "lib/main.js",
"scripts": { "scripts": {
"build": "ncc build src/main.ts --minify", "build": "ncc build src/main.ts --minify",
"build-debug": "ncc build src/main.ts --v8-cache --source-map",
"test": "jest", "test": "jest",
"fmt": "prettier --write \"src/**/*.ts\" \"__tests__/**/*.ts\"", "fmt": "prettier --write \"src/**/*.ts\" \"__tests__/**/*.ts\"",
"fmtcheck": "prettier --check \"src/**/*.ts\" \"__tests__/**/*.ts\"", "fmtcheck": "prettier --check \"src/**/*.ts\" \"__tests__/**/*.ts\""
"updatetag": "git tag -d v2 && git push origin :v2 && git tag -a v2 -m '' && git push origin v2"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -21,25 +19,24 @@
], ],
"author": "softprops", "author": "softprops",
"dependencies": { "dependencies": {
"@actions/core": "^1.11.1", "@actions/core": "^1.10.0",
"@actions/github": "^6.0.0", "@actions/github": "^5.1.1",
"@octokit/plugin-retry": "^7.2.0", "@octokit/plugin-retry": "^4.0.3",
"@octokit/plugin-throttling": "^9.6.1", "@octokit/plugin-throttling": "^4.3.2",
"glob": "^11.0.1", "glob": "^8.0.3",
"mime": "^3.0.0" "mime": "^3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/glob": "^8.1.0", "@types/glob": "^8.0.0",
"@types/jest": "^29.5.14", "@types/jest": "^29.2.3",
"@types/mime": "^3.0.1", "@types/mime": "^3.0.1",
"@types/node": "^22.14.0", "@types/node": "^18.11.9",
"@vercel/ncc": "^0.38.3", "@vercel/ncc": "^0.34.0",
"jest": "^29.3.1", "jest": "^29.3.1",
"jest-circus": "^29.3.1", "jest-circus": "^29.3.1",
"prettier": "3.5.3", "prettier": "2.8.0",
"ts-jest": "^29.3.1", "ts-jest": "^29.0.3",
"ts-node": "^10.9.2", "typescript": "^4.9.3",
"typescript": "^5.8.3",
"typescript-formatter": "^7.2.2" "typescript-formatter": "^7.2.2"
} }
} }

View File

@ -1,7 +1,6 @@
import { GitHub } from "@actions/github/lib/utils"; import { GitHub } from "@actions/github/lib/utils";
import { Config, isTag, releaseBody, alignAssetName } from "./util"; import { Config, isTag, releaseBody } from "./util";
import { statSync } from "fs"; import { statSync, readFileSync } from "fs";
import { open } from "fs/promises";
import { getType } from "mime"; import { getType } from "mime";
import { basename } from "path"; import { basename } from "path";
@ -11,6 +10,7 @@ export interface ReleaseAsset {
name: string; name: string;
mime: string; mime: string;
size: number; size: number;
data: Buffer;
} }
export interface Release { export interface Release {
@ -44,7 +44,7 @@ export interface Releaser {
target_commitish: string | undefined; target_commitish: string | undefined;
discussion_category_name: string | undefined; discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined; generate_release_notes: boolean | undefined;
make_latest: "true" | "false" | "legacy" | undefined; make_latest: string | undefined;
}): Promise<{ data: Release }>; }): Promise<{ data: Release }>;
updateRelease(params: { updateRelease(params: {
@ -59,7 +59,7 @@ export interface Releaser {
prerelease: boolean | undefined; prerelease: boolean | undefined;
discussion_category_name: string | undefined; discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined; generate_release_notes: boolean | undefined;
make_latest: "true" | "false" | "legacy" | undefined; make_latest: string | undefined;
}): Promise<{ data: Release }>; }): Promise<{ data: Release }>;
allReleases(params: { allReleases(params: {
@ -93,15 +93,8 @@ export class GitHubReleaser implements Releaser {
target_commitish: string | undefined; target_commitish: string | undefined;
discussion_category_name: string | undefined; discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined; generate_release_notes: boolean | undefined;
make_latest: "true" | "false" | "legacy" | undefined; make_latest: string | undefined;
}): Promise<{ data: Release }> { }): Promise<{ data: Release }> {
if (
typeof params.make_latest === "string" &&
!["true", "false", "legacy"].includes(params.make_latest)
) {
params.make_latest = undefined;
}
return this.github.rest.repos.createRelease(params); return this.github.rest.repos.createRelease(params);
} }
@ -117,15 +110,8 @@ export class GitHubReleaser implements Releaser {
prerelease: boolean | undefined; prerelease: boolean | undefined;
discussion_category_name: string | undefined; discussion_category_name: string | undefined;
generate_release_notes: boolean | undefined; generate_release_notes: boolean | undefined;
make_latest: "true" | "false" | "legacy" | undefined; make_latest: string | undefined;
}): Promise<{ data: Release }> { }): Promise<{ data: Release }> {
if (
typeof params.make_latest === "string" &&
!["true", "false", "legacy"].includes(params.make_latest)
) {
params.make_latest = undefined;
}
return this.github.rest.repos.updateRelease(params); return this.github.rest.repos.updateRelease(params);
} }
@ -135,7 +121,7 @@ export class GitHubReleaser implements Releaser {
}): AsyncIterableIterator<{ data: Release[] }> { }): AsyncIterableIterator<{ data: Release[] }> {
const updatedParams = { per_page: 100, ...params }; const updatedParams = { per_page: 100, ...params };
return this.github.paginate.iterator( return this.github.paginate.iterator(
this.github.rest.repos.listReleases.endpoint.merge(updatedParams), this.github.rest.repos.listReleases.endpoint.merge(updatedParams)
); );
} }
} }
@ -145,6 +131,7 @@ export const asset = (path: string): ReleaseAsset => {
name: basename(path), name: basename(path),
mime: mimeOrDefault(path), mime: mimeOrDefault(path),
size: statSync(path).size, size: statSync(path).size,
data: readFileSync(path),
}; };
}; };
@ -157,15 +144,12 @@ export const upload = async (
github: GitHub, github: GitHub,
url: string, url: string,
path: string, path: string,
currentAssets: Array<{ id: number; name: string }>, currentAssets: Array<{ id: number; name: string }>
): Promise<any> => { ): Promise<any> => {
const [owner, repo] = config.github_repository.split("/"); const [owner, repo] = config.github_repository.split("/");
const { name, mime, size } = asset(path); const { name, size, mime, data: body } = asset(path);
const currentAsset = currentAssets.find( const currentAsset = currentAssets.find(
// note: GitHub renames asset filenames that have special characters, non-alphanumeric characters, and leading or trailing periods. The "List release assets" endpoint lists the renamed filenames. ({ name: currentName }) => currentName == name
// due to this renaming we need to be mindful when we compare the file name we're uploading with a name github may already have rewritten for logical comparison
// see https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#upload-a-release-asset
({ name: currentName }) => currentName == alignAssetName(name),
); );
if (currentAsset) { if (currentAsset) {
console.log(`♻️ Deleting previously uploaded asset ${name}...`); console.log(`♻️ Deleting previously uploaded asset ${name}...`);
@ -178,8 +162,6 @@ export const upload = async (
console.log(`⬆️ Uploading ${name}...`); console.log(`⬆️ Uploading ${name}...`);
const endpoint = new URL(url); const endpoint = new URL(url);
endpoint.searchParams.append("name", name); endpoint.searchParams.append("name", name);
const fh = await open(path);
try {
const resp = await github.request({ const resp = await github.request({
method: "POST", method: "POST",
url: endpoint.toString(), url: endpoint.toString(),
@ -188,27 +170,23 @@ export const upload = async (
"content-type": mime, "content-type": mime,
authorization: `token ${config.github_token}`, authorization: `token ${config.github_token}`,
}, },
data: fh.readableWebStream({ type: "bytes" }), data: body,
}); });
const json = resp.data; const json = resp.data;
if (resp.status !== 201) { if (resp.status !== 201) {
throw new Error( throw new Error(
`Failed to upload release asset ${name}. received status code ${ `Failed to upload release asset ${name}. received status code ${
resp.status resp.status
}\n${json.message}\n${JSON.stringify(json.errors)}`, }\n${json.message}\n${JSON.stringify(json.errors)}`
); );
} }
console.log(`✅ Uploaded ${name}`);
return json; return json;
} finally {
await fh.close();
}
}; };
export const release = async ( export const release = async (
config: Config, config: Config,
releaser: Releaser, releaser: Releaser,
maxRetries: number = 3, maxRetries: number = 3
): Promise<Release> => { ): Promise<Release> => {
if (maxRetries <= 0) { if (maxRetries <= 0) {
console.log(`❌ Too many retries. Aborting...`); console.log(`❌ Too many retries. Aborting...`);
@ -225,58 +203,47 @@ export const release = async (
const discussion_category_name = config.input_discussion_category_name; const discussion_category_name = config.input_discussion_category_name;
const generate_release_notes = config.input_generate_release_notes; const generate_release_notes = config.input_generate_release_notes;
try { try {
// you can't get an existing draft by tag // you can't get a an existing draft by tag
// so we must find one in the list of all releases // so we must find one in the list of all releases
let _release: Release | undefined = undefined; if (config.input_draft) {
for await (const response of releaser.allReleases({ for await (const response of releaser.allReleases({
owner, owner,
repo, repo,
})) { })) {
_release = response.data.find((release) => release.tag_name === tag); let release = response.data.find((release) => release.tag_name === tag);
if (_release !== undefined) { if (release) {
break; return release;
} }
} }
if (_release === undefined) { }
return await createRelease( let existingRelease = await releaser.getReleaseByTag({
tag,
config,
releaser,
owner, owner,
repo, repo,
discussion_category_name, tag,
generate_release_notes, });
maxRetries,
);
}
let existingRelease: Release = _release!; const release_id = existingRelease.data.id;
console.log(
`Found release ${existingRelease.name} (with id=${existingRelease.id})`,
);
const release_id = existingRelease.id;
let target_commitish: string; let target_commitish: string;
if ( if (
config.input_target_commitish && config.input_target_commitish &&
config.input_target_commitish !== existingRelease.target_commitish config.input_target_commitish !== existingRelease.data.target_commitish
) { ) {
console.log( console.log(
`Updating commit from "${existingRelease.target_commitish}" to "${config.input_target_commitish}"`, `Updating commit from "${existingRelease.data.target_commitish}" to "${config.input_target_commitish}"`
); );
target_commitish = config.input_target_commitish; target_commitish = config.input_target_commitish;
} else { } else {
target_commitish = existingRelease.target_commitish; target_commitish = existingRelease.data.target_commitish;
} }
const tag_name = tag; const tag_name = tag;
const name = config.input_name || existingRelease.name || tag; const name = config.input_name || existingRelease.data.name || tag;
// revisit: support a new body-concat-strategy input for accumulating // revisit: support a new body-concat-strategy input for accumulating
// body parts as a release gets updated. some users will likely want this while // body parts as a release gets updated. some users will likely want this while
// others won't previously this was duplicating content for most which // others won't previously this was duplicating content for most which
// no one wants // no one wants
const workflowBody = releaseBody(config) || ""; const workflowBody = releaseBody(config) || "";
const existingReleaseBody = existingRelease.body || ""; const existingReleaseBody = existingRelease.data.body || "";
let body: string; let body: string;
if (config.input_append_body && workflowBody && existingReleaseBody) { if (config.input_append_body && workflowBody && existingReleaseBody) {
body = existingReleaseBody + "\n" + workflowBody; body = existingReleaseBody + "\n" + workflowBody;
@ -287,11 +254,11 @@ export const release = async (
const draft = const draft =
config.input_draft !== undefined config.input_draft !== undefined
? config.input_draft ? config.input_draft
: existingRelease.draft; : existingRelease.data.draft;
const prerelease = const prerelease =
config.input_prerelease !== undefined config.input_prerelease !== undefined
? config.input_prerelease ? config.input_prerelease
: existingRelease.prerelease; : existingRelease.data.prerelease;
const make_latest = config.input_make_latest; const make_latest = config.input_make_latest;
@ -311,36 +278,7 @@ export const release = async (
}); });
return release.data; return release.data;
} catch (error) { } catch (error) {
if (error.status !== 404) { if (error.status === 404) {
console.log(
`⚠️ Unexpected error fetching GitHub release for tag ${config.github_ref}: ${error}`,
);
throw error;
}
return await createRelease(
tag,
config,
releaser,
owner,
repo,
discussion_category_name,
generate_release_notes,
maxRetries,
);
}
};
async function createRelease(
tag: string,
config: Config,
releaser: Releaser,
owner: string,
repo: string,
discussion_category_name: string | undefined,
generate_release_notes: boolean | undefined,
maxRetries: number,
) {
const tag_name = tag; const tag_name = tag;
const name = config.input_name || tag; const name = config.input_name || tag;
const body = releaseBody(config); const body = releaseBody(config);
@ -353,7 +291,7 @@ async function createRelease(
commitMessage = ` using commit "${target_commitish}"`; commitMessage = ` using commit "${target_commitish}"`;
} }
console.log( console.log(
`👩‍🏭 Creating new GitHub release for tag ${tag_name}${commitMessage}...`, `👩‍🏭 Creating new GitHub release for tag ${tag_name}${commitMessage}...`
); );
try { try {
let release = await releaser.createRelease({ let release = await releaser.createRelease({
@ -371,27 +309,21 @@ async function createRelease(
}); });
return release.data; return release.data;
} catch (error) { } catch (error) {
// presume a race with competing matrix runs // presume a race with competing metrix runs
console.log(`⚠️ GitHub release failed with status: ${error.status}`);
console.log(`${JSON.stringify(error.response.data)}`);
switch (error.status) {
case 403:
console.log( console.log(
"Skip retry — your GitHub token/PAT does not have the required permission to create a release", `⚠️ GitHub release failed with status: ${
error.status
}\n${JSON.stringify(error.response.data.errors)}\nretrying... (${
maxRetries - 1
} retries remaining)`
); );
throw error;
case 404:
console.log("Skip retry - discussion category mismatch");
throw error;
case 422:
console.log("Skip retry - validation failed");
throw error;
}
console.log(`retrying... (${maxRetries - 1} retries remaining)`);
return release(config, releaser, maxRetries - 1); return release(config, releaser, maxRetries - 1);
} }
} else {
console.log(
`⚠️ Unexpected error fetching GitHub release for tag ${config.github_ref}: ${error}`
);
throw error;
} }
}
};

View File

@ -8,6 +8,7 @@ import {
import { release, upload, GitHubReleaser } from "./github"; import { release, upload, GitHubReleaser } from "./github";
import { getOctokit } from "@actions/github"; import { getOctokit } from "@actions/github";
import { setFailed, setOutput } from "@actions/core"; import { setFailed, setOutput } from "@actions/core";
import { GitHub, getOctokitOptions } from "@actions/github/lib/utils";
import { env } from "process"; import { env } from "process";
@ -45,7 +46,7 @@ async function run() {
throttle: { throttle: {
onRateLimit: (retryAfter, options) => { onRateLimit: (retryAfter, options) => {
console.warn( console.warn(
`Request quota exhausted for request ${options.method} ${options.url}`, `Request quota exhausted for request ${options.method} ${options.url}`
); );
if (options.request.retryCount === 0) { if (options.request.retryCount === 0) {
// only retries once // only retries once
@ -56,7 +57,7 @@ async function run() {
onAbuseLimit: (retryAfter, options) => { onAbuseLimit: (retryAfter, options) => {
// does not retry, only logs a warning // does not retry, only logs a warning
console.warn( console.warn(
`Abuse detected for request ${options.method} ${options.url}`, `Abuse detected for request ${options.method} ${options.url}`
); );
}, },
}, },
@ -66,39 +67,24 @@ async function run() {
if (config.input_files && config.input_files.length > 0) { if (config.input_files && config.input_files.length > 0) {
const files = paths(config.input_files); const files = paths(config.input_files);
if (files.length == 0) { if (files.length == 0) {
if (config.input_fail_on_unmatched_files) { throw new Error(`⚠️ ${config.input_files} not include valid file.`);
throw new Error(
`⚠️ ${config.input_files} does not include a valid file.`,
);
} else {
console.warn(
`🤔 ${config.input_files} does not include a valid file.`,
);
}
} }
const currentAssets = rel.assets; const currentAssets = rel.assets;
const assets = await Promise.all(
const uploadFile = async (path) => { files.map(async (path) => {
const json = await upload( const json = await upload(
config, config,
gh, gh,
uploadUrl(rel.upload_url), uploadUrl(rel.upload_url),
path, path,
currentAssets, currentAssets
); );
delete json.uploader; delete json.uploader;
return json; return json;
}; })
).catch((error) => {
let assets; throw error;
if (!config.input_preserve_order) { });
assets = await Promise.all(files.map(uploadFile));
} else {
assets = [];
for (const path of files) {
assets.push(await uploadFile(path));
}
}
setOutput("assets", assets); setOutput("assets", assets);
} }
console.log(`🎉 Release ready at ${rel.html_url}`); console.log(`🎉 Release ready at ${rel.html_url}`);

View File

@ -13,14 +13,13 @@ export interface Config {
input_body_path?: string; input_body_path?: string;
input_files?: string[]; input_files?: string[];
input_draft?: boolean; input_draft?: boolean;
input_preserve_order?: boolean;
input_prerelease?: boolean; input_prerelease?: boolean;
input_fail_on_unmatched_files?: boolean; input_fail_on_unmatched_files?: boolean;
input_target_commitish?: string; input_target_commitish?: string;
input_discussion_category_name?: string; input_discussion_category_name?: string;
input_generate_release_notes?: boolean; input_generate_release_notes?: boolean;
input_append_body?: boolean; input_append_body?: boolean;
input_make_latest: "true" | "false" | "legacy" | undefined; input_make_latest: string | undefined;
} }
export const uploadUrl = (url: string): string => { export const uploadUrl = (url: string): string => {
@ -48,7 +47,7 @@ export const parseInputFiles = (files: string): string[] => {
.concat(line.split(",")) .concat(line.split(","))
.filter((pat) => pat) .filter((pat) => pat)
.map((pat) => pat.trim()), .map((pat) => pat.trim()),
[], []
); );
}; };
@ -63,9 +62,6 @@ export const parseConfig = (env: Env): Config => {
input_body_path: env.INPUT_BODY_PATH, input_body_path: env.INPUT_BODY_PATH,
input_files: parseInputFiles(env.INPUT_FILES || ""), input_files: parseInputFiles(env.INPUT_FILES || ""),
input_draft: env.INPUT_DRAFT ? env.INPUT_DRAFT === "true" : undefined, input_draft: env.INPUT_DRAFT ? env.INPUT_DRAFT === "true" : undefined,
input_preserve_order: env.INPUT_PRESERVE_ORDER
? env.INPUT_PRESERVE_ORDER == "true"
: undefined,
input_prerelease: env.INPUT_PRERELEASE input_prerelease: env.INPUT_PRERELEASE
? env.INPUT_PRERELEASE == "true" ? env.INPUT_PRERELEASE == "true"
: undefined, : undefined,
@ -75,23 +71,16 @@ export const parseConfig = (env: Env): Config => {
env.INPUT_DISCUSSION_CATEGORY_NAME || undefined, env.INPUT_DISCUSSION_CATEGORY_NAME || undefined,
input_generate_release_notes: env.INPUT_GENERATE_RELEASE_NOTES == "true", input_generate_release_notes: env.INPUT_GENERATE_RELEASE_NOTES == "true",
input_append_body: env.INPUT_APPEND_BODY == "true", input_append_body: env.INPUT_APPEND_BODY == "true",
input_make_latest: parseMakeLatest(env.INPUT_MAKE_LATEST), input_make_latest: env.INPUT_MAKE_LATEST
? env.INPUT_MAKE_LATEST
: undefined,
}; };
}; };
const parseMakeLatest = (
value: string | undefined,
): "true" | "false" | "legacy" | undefined => {
if (value === "true" || value === "false" || value === "legacy") {
return value;
}
return undefined;
};
export const paths = (patterns: string[]): string[] => { export const paths = (patterns: string[]): string[] => {
return patterns.reduce((acc: string[], pattern: string): string[] => { return patterns.reduce((acc: string[], pattern: string): string[] => {
return acc.concat( return acc.concat(
glob.sync(pattern).filter((path) => statSync(path).isFile()), glob.sync(pattern).filter((path) => statSync(path).isFile())
); );
}, []); }, []);
}; };
@ -101,7 +90,7 @@ export const unmatchedPatterns = (patterns: string[]): string[] => {
return acc.concat( return acc.concat(
glob.sync(pattern).filter((path) => statSync(path).isFile()).length == 0 glob.sync(pattern).filter((path) => statSync(path).isFile()).length == 0
? [pattern] ? [pattern]
: [], : []
); );
}, []); }, []);
}; };
@ -109,7 +98,3 @@ export const unmatchedPatterns = (patterns: string[]): string[] => {
export const isTag = (ref: string): boolean => { export const isTag = (ref: string): boolean => {
return ref.startsWith("refs/tags/"); return ref.startsWith("refs/tags/");
}; };
export const alignAssetName = (assetName: string): string => {
return assetName.replace(/ /g, ".");
};

View File

@ -60,5 +60,5 @@
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
}, },
"exclude": ["node_modules", "**/*.test.ts", "jest.config.ts"] "exclude": ["node_modules", "**/*.test.ts"]
} }