mirror of
				https://github.com/softprops/action-gh-release.git
				synced 2025-11-04 05:39:24 +00:00 
			
		
		
		
	Compare commits
	
		
			44 Commits
		
	
	
		
			v2.2.2
			...
			dependabot
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					0d3324bd4e | ||
| 
						 | 
					0adea5aa98 | ||
| 
						 | 
					aa05f9d779 | ||
| 
						 | 
					bbaccb3a0c | ||
| 
						 | 
					50fda3f773 | ||
| 
						 | 
					5434409c2b | ||
| 
						 | 
					6da8fa9354 | ||
| 
						 | 
					f38efdea4c | ||
| 
						 | 
					cec1a1113b | ||
| 
						 | 
					aec2ec56f9 | ||
| 
						 | 
					4db716b167 | ||
| 
						 | 
					14820f2cee | ||
| 
						 | 
					62c96d0c4e | ||
| 
						 | 
					7dc9b8ac0f | ||
| 
						 | 
					0f0e0b98e9 | ||
| 
						 | 
					97d42c1b50 | ||
| 
						 | 
					19cd0bcd2b | ||
| 
						 | 
					5d1b0b1164 | ||
| 
						 | 
					f6021cf9a4 | ||
| 
						 | 
					6cbd405e2c | ||
| 
						 | 
					fbadcc90e8 | ||
| 
						 | 
					4a840061c4 | ||
| 
						 | 
					7191749478 | ||
| 
						 | 
					126b1e7093 | ||
| 
						 | 
					f82d31e53e | ||
| 
						 | 
					f2352b97da | ||
| 
						 | 
					f0b3259de2 | ||
| 
						 | 
					f37a2f9143 | ||
| 
						 | 
					db560141c6 | ||
| 
						 | 
					40521a2029 | ||
| 
						 | 
					c8d8e91662 | ||
| 
						 | 
					605f567f95 | ||
| 
						 | 
					5822334cb4 | ||
| 
						 | 
					72f2c25fcb | ||
| 
						 | 
					552dc5524b | ||
| 
						 | 
					f3cad8bcbf | ||
| 
						 | 
					07a2257003 | ||
| 
						 | 
					d5382d3e6f | ||
| 
						 | 
					a0e2122208 | ||
| 
						 | 
					8836085300 | ||
| 
						 | 
					86463358d8 | ||
| 
						 | 
					46b284799f | ||
| 
						 | 
					37fd9d0351 | ||
| 
						 | 
					39ba0b9d81 | 
							
								
								
									
										8
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							@@ -12,13 +12,9 @@ updates:
 | 
			
		||||
      - dependency-name: node-fetch
 | 
			
		||||
        versions:
 | 
			
		||||
          - ">=3.0.0"
 | 
			
		||||
      # ignore mime and @types/mime per https://github.com/softprops/action-gh-release/pull/475
 | 
			
		||||
      - dependency-name: mime
 | 
			
		||||
      - dependency-name: "@types/node"
 | 
			
		||||
        versions:
 | 
			
		||||
          - ">=4.0.0"
 | 
			
		||||
      - dependency-name: "@types/mime"
 | 
			
		||||
        versions:
 | 
			
		||||
          - ">=4.0.0"
 | 
			
		||||
          - ">=22.0.0"
 | 
			
		||||
    commit-message:
 | 
			
		||||
      prefix: "chore(deps)"
 | 
			
		||||
  - package-ecosystem: github-actions
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/main.yml
									
									
									
									
										vendored
									
									
								
							@@ -8,9 +8,9 @@ jobs:
 | 
			
		||||
  build:
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
 | 
			
		||||
      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
 | 
			
		||||
 | 
			
		||||
      - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
 | 
			
		||||
      - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".tool-versions"
 | 
			
		||||
          cache: "npm"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -2,3 +2,4 @@ __tests__/runner/*
 | 
			
		||||
# actions requires a node_modules dir https://github.com/actions/toolkit/blob/master/docs/javascript-action.md#publish-a-releasesv1-action
 | 
			
		||||
# but its recommended not to check these in https://github.com/actions/toolkit/blob/master/docs/action-versioning.md#recommendations
 | 
			
		||||
node_modules
 | 
			
		||||
coverage
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
# Build outputs
 | 
			
		||||
dist/
 | 
			
		||||
lib/
 | 
			
		||||
coverage/
 | 
			
		||||
 | 
			
		||||
# Dependencies
 | 
			
		||||
node_modules/
 | 
			
		||||
 | 
			
		||||
# Misc
 | 
			
		||||
.github/
 | 
			
		||||
*.log
 | 
			
		||||
.DS_Store
 | 
			
		||||
__tests__/release.txt
 | 
			
		||||
 | 
			
		||||
# Package files
 | 
			
		||||
package-lock.json
 | 
			
		||||
							
								
								
									
										11
									
								
								.prettierrc.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.prettierrc.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
/**
 | 
			
		||||
 * @type {import('prettier').Config}
 | 
			
		||||
 */
 | 
			
		||||
module.exports = {
 | 
			
		||||
  trailingComma: 'all',
 | 
			
		||||
  tabWidth: 2,
 | 
			
		||||
  semi: true,
 | 
			
		||||
  singleQuote: true,
 | 
			
		||||
  printWidth: 100,
 | 
			
		||||
  bracketSpacing: true,
 | 
			
		||||
};
 | 
			
		||||
@@ -1 +1 @@
 | 
			
		||||
nodejs 20.15.1
 | 
			
		||||
nodejs 24.2.0
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										58
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -1,3 +1,61 @@
 | 
			
		||||
## 2.4.1
 | 
			
		||||
 | 
			
		||||
## What's Changed
 | 
			
		||||
 | 
			
		||||
### Other Changes 🔄
 | 
			
		||||
 | 
			
		||||
* fix(util): support brace expansion globs containing commas in parseInputFiles by @Copilot in https://github.com/softprops/action-gh-release/pull/672
 | 
			
		||||
* fix: gracefully fallback to body when body_path cannot be read by @Copilot in https://github.com/softprops/action-gh-release/pull/671
 | 
			
		||||
 | 
			
		||||
## 2.4.0
 | 
			
		||||
 | 
			
		||||
## What's Changed
 | 
			
		||||
 | 
			
		||||
### Exciting New Features 🎉
 | 
			
		||||
 | 
			
		||||
* feat(action): respect working_directory for files globs by @stephenway in https://github.com/softprops/action-gh-release/pull/667
 | 
			
		||||
 | 
			
		||||
## 2.3.4
 | 
			
		||||
 | 
			
		||||
## What's Changed
 | 
			
		||||
 | 
			
		||||
### Bug fixes 🐛
 | 
			
		||||
 | 
			
		||||
* fix(action): handle 422 already_exists race condition by @stephenway in https://github.com/softprops/action-gh-release/pull/665
 | 
			
		||||
 | 
			
		||||
### Other Changes 🔄
 | 
			
		||||
 | 
			
		||||
- dependency updates
 | 
			
		||||
 | 
			
		||||
## 2.3.3
 | 
			
		||||
 | 
			
		||||
## What's Changed
 | 
			
		||||
 | 
			
		||||
### Exciting New Features 🎉
 | 
			
		||||
 | 
			
		||||
* feat: add input option `overwrite_files` by @asfernandes in https://github.com/softprops/action-gh-release/pull/343
 | 
			
		||||
 | 
			
		||||
### Other Changes 🔄
 | 
			
		||||
 | 
			
		||||
- dependency updates
 | 
			
		||||
 | 
			
		||||
## 2.3.2
 | 
			
		||||
 | 
			
		||||
* fix: revert fs `readableWebStream` change
 | 
			
		||||
 | 
			
		||||
## 2.3.1
 | 
			
		||||
 | 
			
		||||
### Bug fixes 🐛
 | 
			
		||||
 | 
			
		||||
* fix: fix file closing issue by @WailGree in https://github.com/softprops/action-gh-release/pull/629
 | 
			
		||||
 | 
			
		||||
## 2.3.0
 | 
			
		||||
 | 
			
		||||
* Migrate from jest to vitest
 | 
			
		||||
* Replace `mime` with `mime-types`
 | 
			
		||||
* Bump to use node 24
 | 
			
		||||
* Dependency updates
 | 
			
		||||
 | 
			
		||||
## 2.2.2
 | 
			
		||||
 | 
			
		||||
## What's Changed
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								README.md
									
									
									
									
									
								
							@@ -51,7 +51,7 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
        uses: actions/checkout@v5
 | 
			
		||||
      - name: Release
 | 
			
		||||
        uses: softprops/action-gh-release@v2
 | 
			
		||||
        if: github.ref_type == 'tag'
 | 
			
		||||
@@ -72,7 +72,7 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
        uses: actions/checkout@v5
 | 
			
		||||
      - name: Release
 | 
			
		||||
        uses: softprops/action-gh-release@v2
 | 
			
		||||
```
 | 
			
		||||
@@ -99,7 +99,7 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
        uses: actions/checkout@v5
 | 
			
		||||
      - name: Build
 | 
			
		||||
        run: echo ${{ github.sha }} > Release.txt
 | 
			
		||||
      - name: Test
 | 
			
		||||
@@ -123,7 +123,7 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
        uses: actions/checkout@v5
 | 
			
		||||
      - name: Build
 | 
			
		||||
        run: echo ${{ github.sha }} > Release.txt
 | 
			
		||||
      - name: Test
 | 
			
		||||
@@ -157,7 +157,7 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
        uses: actions/checkout@v5
 | 
			
		||||
      - name: Generate Changelog
 | 
			
		||||
        run: echo "# Good things have arrived" > ${{ github.workspace }}-CHANGELOG.txt
 | 
			
		||||
      - name: Release
 | 
			
		||||
@@ -185,6 +185,7 @@ The following are optional as `step.with` keys
 | 
			
		||||
| `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                                                                                                                                                                                                                                                                                                                                                                                |
 | 
			
		||||
| `overwrite_files`          | Boolean | Indicator of whether files should be overwritten when they already exist. Defaults to true                                                                                                                                                                                                                                                                                                                                                      |
 | 
			
		||||
| `name`                     | String  | Name of the release. defaults to tag name                                                                                                                                                                                                                                                                                                                                                                                                       |
 | 
			
		||||
| `tag_name`                 | String  | Name of a tag. defaults to `github.ref_name`                                                                                                                                                                                                                                                                                                                                                                                                         |
 | 
			
		||||
| `fail_on_unmatched_files`  | Boolean | Indicator of whether to fail if any of the `files` globs match nothing                                                                                                                                                                                                                                                                                                                                                                          |
 | 
			
		||||
 
 | 
			
		||||
@@ -1,61 +1,63 @@
 | 
			
		||||
import * as assert from "assert";
 | 
			
		||||
import {
 | 
			
		||||
  mimeOrDefault,
 | 
			
		||||
  asset,
 | 
			
		||||
  Releaser,
 | 
			
		||||
  Release,
 | 
			
		||||
  findTagFromReleases,
 | 
			
		||||
} from "../src/github";
 | 
			
		||||
  mimeOrDefault,
 | 
			
		||||
  release,
 | 
			
		||||
  Release,
 | 
			
		||||
  Releaser,
 | 
			
		||||
} from '../src/github';
 | 
			
		||||
 | 
			
		||||
describe("github", () => {
 | 
			
		||||
  describe("mimeOrDefault", () => {
 | 
			
		||||
    it("returns a specific mime for common path", async () => {
 | 
			
		||||
      assert.equal(mimeOrDefault("foo.tar.gz"), "application/gzip");
 | 
			
		||||
import { assert, describe, it } from 'vitest';
 | 
			
		||||
 | 
			
		||||
describe('github', () => {
 | 
			
		||||
  describe('mimeOrDefault', () => {
 | 
			
		||||
    it('returns a specific mime for common path', async () => {
 | 
			
		||||
      assert.equal(mimeOrDefault('foo.tar.gz'), 'application/gzip');
 | 
			
		||||
    });
 | 
			
		||||
    it("returns default mime for uncommon path", async () => {
 | 
			
		||||
      assert.equal(mimeOrDefault("foo.uncommon"), "application/octet-stream");
 | 
			
		||||
    it('returns default mime for uncommon path', async () => {
 | 
			
		||||
      assert.equal(mimeOrDefault('foo.uncommon'), 'application/octet-stream');
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("asset", () => {
 | 
			
		||||
    it("derives asset info from a path", async () => {
 | 
			
		||||
      const { name, mime, size } = asset("tests/data/foo/bar.txt");
 | 
			
		||||
      assert.equal(name, "bar.txt");
 | 
			
		||||
      assert.equal(mime, "text/plain");
 | 
			
		||||
  describe('asset', () => {
 | 
			
		||||
    it('derives asset info from a path', async () => {
 | 
			
		||||
      const { name, mime, size } = asset('tests/data/foo/bar.txt');
 | 
			
		||||
      assert.equal(name, 'bar.txt');
 | 
			
		||||
      assert.equal(mime, 'text/plain');
 | 
			
		||||
      assert.equal(size, 10);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("findTagFromReleases", () => {
 | 
			
		||||
    const owner = "owner";
 | 
			
		||||
    const repo = "repo";
 | 
			
		||||
  describe('findTagFromReleases', () => {
 | 
			
		||||
    const owner = 'owner';
 | 
			
		||||
    const repo = 'repo';
 | 
			
		||||
 | 
			
		||||
    const mockRelease: Release = {
 | 
			
		||||
      id: 1,
 | 
			
		||||
      upload_url: `https://api.github.com/repos/${owner}/${repo}/releases/1/assets`,
 | 
			
		||||
      html_url: `https://github.com/${owner}/${repo}/releases/tag/v1.0.0`,
 | 
			
		||||
      tag_name: "v1.0.0",
 | 
			
		||||
      name: "Test Release",
 | 
			
		||||
      body: "Test body",
 | 
			
		||||
      target_commitish: "main",
 | 
			
		||||
      tag_name: 'v1.0.0',
 | 
			
		||||
      name: 'Test Release',
 | 
			
		||||
      body: 'Test body',
 | 
			
		||||
      target_commitish: 'main',
 | 
			
		||||
      draft: false,
 | 
			
		||||
      prerelease: false,
 | 
			
		||||
      assets: [],
 | 
			
		||||
    } as const;
 | 
			
		||||
 | 
			
		||||
    const mockReleaser: Releaser = {
 | 
			
		||||
      getReleaseByTag: () => Promise.reject("Not implemented"),
 | 
			
		||||
      createRelease: () => Promise.reject("Not implemented"),
 | 
			
		||||
      updateRelease: () => Promise.reject("Not implemented"),
 | 
			
		||||
      getReleaseByTag: () => Promise.reject('Not implemented'),
 | 
			
		||||
      createRelease: () => Promise.reject('Not implemented'),
 | 
			
		||||
      updateRelease: () => Promise.reject('Not implemented'),
 | 
			
		||||
      allReleases: async function* () {
 | 
			
		||||
        yield { data: [mockRelease] };
 | 
			
		||||
      },
 | 
			
		||||
    } as const;
 | 
			
		||||
 | 
			
		||||
    describe("when the tag_name is not an empty string", () => {
 | 
			
		||||
      const targetTag = "v1.0.0";
 | 
			
		||||
    describe('when the tag_name is not an empty string', () => {
 | 
			
		||||
      const targetTag = 'v1.0.0';
 | 
			
		||||
 | 
			
		||||
      it("finds a matching release in first batch of results", async () => {
 | 
			
		||||
      it('finds a matching release in first batch of results', async () => {
 | 
			
		||||
        const targetRelease = {
 | 
			
		||||
          ...mockRelease,
 | 
			
		||||
          owner,
 | 
			
		||||
@@ -66,7 +68,7 @@ describe("github", () => {
 | 
			
		||||
          ...mockRelease,
 | 
			
		||||
          owner,
 | 
			
		||||
          repo,
 | 
			
		||||
          tag_name: "v1.0.1",
 | 
			
		||||
          tag_name: 'v1.0.1',
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const releaser = {
 | 
			
		||||
@@ -77,17 +79,12 @@ describe("github", () => {
 | 
			
		||||
          },
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const result = await findTagFromReleases(
 | 
			
		||||
          releaser,
 | 
			
		||||
          owner,
 | 
			
		||||
          repo,
 | 
			
		||||
          targetTag,
 | 
			
		||||
        );
 | 
			
		||||
        const result = await findTagFromReleases(releaser, owner, repo, targetTag);
 | 
			
		||||
 | 
			
		||||
        assert.deepStrictEqual(result, targetRelease);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("finds a matching release in second batch of results", async () => {
 | 
			
		||||
      it('finds a matching release in second batch of results', async () => {
 | 
			
		||||
        const targetRelease = {
 | 
			
		||||
          ...mockRelease,
 | 
			
		||||
          owner,
 | 
			
		||||
@@ -98,7 +95,7 @@ describe("github", () => {
 | 
			
		||||
          ...mockRelease,
 | 
			
		||||
          owner,
 | 
			
		||||
          repo,
 | 
			
		||||
          tag_name: "v1.0.1",
 | 
			
		||||
          tag_name: 'v1.0.1',
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const releaser = {
 | 
			
		||||
@@ -109,21 +106,16 @@ describe("github", () => {
 | 
			
		||||
          },
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const result = await findTagFromReleases(
 | 
			
		||||
          releaser,
 | 
			
		||||
          owner,
 | 
			
		||||
          repo,
 | 
			
		||||
          targetTag,
 | 
			
		||||
        );
 | 
			
		||||
        const result = await findTagFromReleases(releaser, owner, repo, targetTag);
 | 
			
		||||
        assert.deepStrictEqual(result, targetRelease);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("returns undefined when a release is not found in any batch", async () => {
 | 
			
		||||
      it('returns undefined when a release is not found in any batch', async () => {
 | 
			
		||||
        const otherRelease = {
 | 
			
		||||
          ...mockRelease,
 | 
			
		||||
          owner,
 | 
			
		||||
          repo,
 | 
			
		||||
          tag_name: "v1.0.1",
 | 
			
		||||
          tag_name: 'v1.0.1',
 | 
			
		||||
        };
 | 
			
		||||
        const releaser = {
 | 
			
		||||
          ...mockReleaser,
 | 
			
		||||
@@ -133,17 +125,12 @@ describe("github", () => {
 | 
			
		||||
          },
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const result = await findTagFromReleases(
 | 
			
		||||
          releaser,
 | 
			
		||||
          owner,
 | 
			
		||||
          repo,
 | 
			
		||||
          targetTag,
 | 
			
		||||
        );
 | 
			
		||||
        const result = await findTagFromReleases(releaser, owner, repo, targetTag);
 | 
			
		||||
 | 
			
		||||
        assert.strictEqual(result, undefined);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("returns undefined when no releases are returned", async () => {
 | 
			
		||||
      it('returns undefined when no releases are returned', async () => {
 | 
			
		||||
        const releaser = {
 | 
			
		||||
          ...mockReleaser,
 | 
			
		||||
          allReleases: async function* () {
 | 
			
		||||
@@ -151,21 +138,16 @@ describe("github", () => {
 | 
			
		||||
          },
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const result = await findTagFromReleases(
 | 
			
		||||
          releaser,
 | 
			
		||||
          owner,
 | 
			
		||||
          repo,
 | 
			
		||||
          targetTag,
 | 
			
		||||
        );
 | 
			
		||||
        const result = await findTagFromReleases(releaser, owner, repo, targetTag);
 | 
			
		||||
 | 
			
		||||
        assert.strictEqual(result, undefined);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("when the tag_name is an empty string", () => {
 | 
			
		||||
      const emptyTag = "";
 | 
			
		||||
    describe('when the tag_name is an empty string', () => {
 | 
			
		||||
      const emptyTag = '';
 | 
			
		||||
 | 
			
		||||
      it("finds a matching release in first batch of results", async () => {
 | 
			
		||||
      it('finds a matching release in first batch of results', async () => {
 | 
			
		||||
        const targetRelease = {
 | 
			
		||||
          ...mockRelease,
 | 
			
		||||
          owner,
 | 
			
		||||
@@ -176,7 +158,7 @@ describe("github", () => {
 | 
			
		||||
          ...mockRelease,
 | 
			
		||||
          owner,
 | 
			
		||||
          repo,
 | 
			
		||||
          tag_name: "v1.0.1",
 | 
			
		||||
          tag_name: 'v1.0.1',
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const releaser = {
 | 
			
		||||
@@ -187,17 +169,12 @@ describe("github", () => {
 | 
			
		||||
          },
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const result = await findTagFromReleases(
 | 
			
		||||
          releaser,
 | 
			
		||||
          owner,
 | 
			
		||||
          repo,
 | 
			
		||||
          emptyTag,
 | 
			
		||||
        );
 | 
			
		||||
        const result = await findTagFromReleases(releaser, owner, repo, emptyTag);
 | 
			
		||||
 | 
			
		||||
        assert.deepStrictEqual(result, targetRelease);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("finds a matching release in second batch of results", async () => {
 | 
			
		||||
      it('finds a matching release in second batch of results', async () => {
 | 
			
		||||
        const targetRelease = {
 | 
			
		||||
          ...mockRelease,
 | 
			
		||||
          owner,
 | 
			
		||||
@@ -208,7 +185,7 @@ describe("github", () => {
 | 
			
		||||
          ...mockRelease,
 | 
			
		||||
          owner,
 | 
			
		||||
          repo,
 | 
			
		||||
          tag_name: "v1.0.1",
 | 
			
		||||
          tag_name: 'v1.0.1',
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const releaser = {
 | 
			
		||||
@@ -219,21 +196,16 @@ describe("github", () => {
 | 
			
		||||
          },
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const result = await findTagFromReleases(
 | 
			
		||||
          releaser,
 | 
			
		||||
          owner,
 | 
			
		||||
          repo,
 | 
			
		||||
          emptyTag,
 | 
			
		||||
        );
 | 
			
		||||
        const result = await findTagFromReleases(releaser, owner, repo, emptyTag);
 | 
			
		||||
        assert.deepStrictEqual(result, targetRelease);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("returns undefined when a release is not found in any batch", async () => {
 | 
			
		||||
      it('returns undefined when a release is not found in any batch', async () => {
 | 
			
		||||
        const otherRelease = {
 | 
			
		||||
          ...mockRelease,
 | 
			
		||||
          owner,
 | 
			
		||||
          repo,
 | 
			
		||||
          tag_name: "v1.0.1",
 | 
			
		||||
          tag_name: 'v1.0.1',
 | 
			
		||||
        };
 | 
			
		||||
        const releaser = {
 | 
			
		||||
          ...mockReleaser,
 | 
			
		||||
@@ -243,17 +215,12 @@ describe("github", () => {
 | 
			
		||||
          },
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const result = await findTagFromReleases(
 | 
			
		||||
          releaser,
 | 
			
		||||
          owner,
 | 
			
		||||
          repo,
 | 
			
		||||
          emptyTag,
 | 
			
		||||
        );
 | 
			
		||||
        const result = await findTagFromReleases(releaser, owner, repo, emptyTag);
 | 
			
		||||
 | 
			
		||||
        assert.strictEqual(result, undefined);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("returns undefined when no releases are returned", async () => {
 | 
			
		||||
      it('returns undefined when no releases are returned', async () => {
 | 
			
		||||
        const releaser = {
 | 
			
		||||
          ...mockReleaser,
 | 
			
		||||
          allReleases: async function* () {
 | 
			
		||||
@@ -261,15 +228,81 @@ describe("github", () => {
 | 
			
		||||
          },
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const result = await findTagFromReleases(
 | 
			
		||||
          releaser,
 | 
			
		||||
          owner,
 | 
			
		||||
          repo,
 | 
			
		||||
          emptyTag,
 | 
			
		||||
        );
 | 
			
		||||
        const result = await findTagFromReleases(releaser, owner, repo, emptyTag);
 | 
			
		||||
 | 
			
		||||
        assert.strictEqual(result, undefined);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('error handling', () => {
 | 
			
		||||
    it('handles 422 already_exists error gracefully', async () => {
 | 
			
		||||
      const mockReleaser: Releaser = {
 | 
			
		||||
        getReleaseByTag: () => Promise.reject('Not implemented'),
 | 
			
		||||
        createRelease: () =>
 | 
			
		||||
          Promise.reject({
 | 
			
		||||
            status: 422,
 | 
			
		||||
            response: { data: { errors: [{ code: 'already_exists' }] } },
 | 
			
		||||
          }),
 | 
			
		||||
        updateRelease: () =>
 | 
			
		||||
          Promise.resolve({
 | 
			
		||||
            data: {
 | 
			
		||||
              id: 1,
 | 
			
		||||
              upload_url: 'test',
 | 
			
		||||
              html_url: 'test',
 | 
			
		||||
              tag_name: 'v1.0.0',
 | 
			
		||||
              name: 'test',
 | 
			
		||||
              body: 'test',
 | 
			
		||||
              target_commitish: 'main',
 | 
			
		||||
              draft: false,
 | 
			
		||||
              prerelease: false,
 | 
			
		||||
              assets: [],
 | 
			
		||||
            },
 | 
			
		||||
          }),
 | 
			
		||||
        allReleases: async function* () {
 | 
			
		||||
          yield {
 | 
			
		||||
            data: [
 | 
			
		||||
              {
 | 
			
		||||
                id: 1,
 | 
			
		||||
                upload_url: 'test',
 | 
			
		||||
                html_url: 'test',
 | 
			
		||||
                tag_name: 'v1.0.0',
 | 
			
		||||
                name: 'test',
 | 
			
		||||
                body: 'test',
 | 
			
		||||
                target_commitish: 'main',
 | 
			
		||||
                draft: false,
 | 
			
		||||
                prerelease: false,
 | 
			
		||||
                assets: [],
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          };
 | 
			
		||||
        },
 | 
			
		||||
      } as const;
 | 
			
		||||
 | 
			
		||||
      const config = {
 | 
			
		||||
        github_token: 'test-token',
 | 
			
		||||
        github_ref: 'refs/tags/v1.0.0',
 | 
			
		||||
        github_repository: 'owner/repo',
 | 
			
		||||
        input_tag_name: undefined,
 | 
			
		||||
        input_name: undefined,
 | 
			
		||||
        input_body: undefined,
 | 
			
		||||
        input_body_path: undefined,
 | 
			
		||||
        input_files: [],
 | 
			
		||||
        input_draft: undefined,
 | 
			
		||||
        input_prerelease: undefined,
 | 
			
		||||
        input_preserve_order: undefined,
 | 
			
		||||
        input_overwrite_files: undefined,
 | 
			
		||||
        input_fail_on_unmatched_files: false,
 | 
			
		||||
        input_target_commitish: undefined,
 | 
			
		||||
        input_discussion_category_name: undefined,
 | 
			
		||||
        input_generate_release_notes: false,
 | 
			
		||||
        input_append_body: false,
 | 
			
		||||
        input_make_latest: undefined,
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      const result = await release(config, mockReleaser, 1);
 | 
			
		||||
      assert.ok(result);
 | 
			
		||||
      assert.equal(result.id, 1);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,54 +1,72 @@
 | 
			
		||||
import {
 | 
			
		||||
  releaseBody,
 | 
			
		||||
  alignAssetName,
 | 
			
		||||
  isTag,
 | 
			
		||||
  paths,
 | 
			
		||||
  parseConfig,
 | 
			
		||||
  parseInputFiles,
 | 
			
		||||
  paths,
 | 
			
		||||
  releaseBody,
 | 
			
		||||
  unmatchedPatterns,
 | 
			
		||||
  uploadUrl,
 | 
			
		||||
  alignAssetName,
 | 
			
		||||
} from "../src/util";
 | 
			
		||||
import * as assert from "assert";
 | 
			
		||||
} from '../src/util';
 | 
			
		||||
 | 
			
		||||
describe("util", () => {
 | 
			
		||||
  describe("uploadUrl", () => {
 | 
			
		||||
    it("strips template", () => {
 | 
			
		||||
import { assert, describe, expect, it } from 'vitest';
 | 
			
		||||
 | 
			
		||||
describe('util', () => {
 | 
			
		||||
  describe('uploadUrl', () => {
 | 
			
		||||
    it('strips template', () => {
 | 
			
		||||
      assert.equal(
 | 
			
		||||
        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',
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  describe("parseInputFiles", () => {
 | 
			
		||||
    it("parses empty strings", () => {
 | 
			
		||||
      assert.deepStrictEqual(parseInputFiles(""), []);
 | 
			
		||||
  describe('parseInputFiles', () => {
 | 
			
		||||
    it('parses empty strings', () => {
 | 
			
		||||
      assert.deepStrictEqual(parseInputFiles(''), []);
 | 
			
		||||
    });
 | 
			
		||||
    it("parses comma-delimited strings", () => {
 | 
			
		||||
      assert.deepStrictEqual(parseInputFiles("foo,bar"), ["foo", "bar"]);
 | 
			
		||||
    it('parses comma-delimited strings', () => {
 | 
			
		||||
      assert.deepStrictEqual(parseInputFiles('foo,bar'), ['foo', 'bar']);
 | 
			
		||||
    });
 | 
			
		||||
    it("parses newline and comma-delimited (and then some)", () => {
 | 
			
		||||
      assert.deepStrictEqual(
 | 
			
		||||
        parseInputFiles("foo,bar\nbaz,boom,\n\ndoom,loom "),
 | 
			
		||||
        ["foo", "bar", "baz", "boom", "doom", "loom"],
 | 
			
		||||
      );
 | 
			
		||||
    it('parses newline and comma-delimited (and then some)', () => {
 | 
			
		||||
      assert.deepStrictEqual(parseInputFiles('foo,bar\nbaz,boom,\n\ndoom,loom '), [
 | 
			
		||||
        'foo',
 | 
			
		||||
        'bar',
 | 
			
		||||
        'baz',
 | 
			
		||||
        'boom',
 | 
			
		||||
        'doom',
 | 
			
		||||
        'loom',
 | 
			
		||||
      ]);
 | 
			
		||||
    });
 | 
			
		||||
    it('handles globs with brace groups containing commas', () => {
 | 
			
		||||
      assert.deepStrictEqual(parseInputFiles('./**/*.{exe,deb,tar.gz}\nfoo,bar'), [
 | 
			
		||||
        './**/*.{exe,deb,tar.gz}',
 | 
			
		||||
        'foo',
 | 
			
		||||
        'bar',
 | 
			
		||||
      ]);
 | 
			
		||||
    });
 | 
			
		||||
    it('handles single-line brace pattern correctly', () => {
 | 
			
		||||
      assert.deepStrictEqual(parseInputFiles('./**/*.{exe,deb,tar.gz}'), [
 | 
			
		||||
        './**/*.{exe,deb,tar.gz}',
 | 
			
		||||
      ]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  describe("releaseBody", () => {
 | 
			
		||||
    it("uses input body", () => {
 | 
			
		||||
  describe('releaseBody', () => {
 | 
			
		||||
    it('uses input body', () => {
 | 
			
		||||
      assert.equal(
 | 
			
		||||
        "foo",
 | 
			
		||||
        'foo',
 | 
			
		||||
        releaseBody({
 | 
			
		||||
          github_ref: "",
 | 
			
		||||
          github_repository: "",
 | 
			
		||||
          github_token: "",
 | 
			
		||||
          input_body: "foo",
 | 
			
		||||
          github_ref: '',
 | 
			
		||||
          github_repository: '',
 | 
			
		||||
          github_token: '',
 | 
			
		||||
          input_body: 'foo',
 | 
			
		||||
          input_body_path: undefined,
 | 
			
		||||
          input_draft: false,
 | 
			
		||||
          input_prerelease: false,
 | 
			
		||||
          input_preserve_order: undefined,
 | 
			
		||||
          input_files: [],
 | 
			
		||||
          input_overwrite_files: undefined,
 | 
			
		||||
          input_name: undefined,
 | 
			
		||||
          input_tag_name: undefined,
 | 
			
		||||
          input_target_commitish: undefined,
 | 
			
		||||
@@ -58,19 +76,20 @@ describe("util", () => {
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
    it("uses input body path", () => {
 | 
			
		||||
    it('uses input body path', () => {
 | 
			
		||||
      assert.equal(
 | 
			
		||||
        "bar",
 | 
			
		||||
        'bar',
 | 
			
		||||
        releaseBody({
 | 
			
		||||
          github_ref: "",
 | 
			
		||||
          github_repository: "",
 | 
			
		||||
          github_token: "",
 | 
			
		||||
          github_ref: '',
 | 
			
		||||
          github_repository: '',
 | 
			
		||||
          github_token: '',
 | 
			
		||||
          input_body: undefined,
 | 
			
		||||
          input_body_path: "__tests__/release.txt",
 | 
			
		||||
          input_body_path: '__tests__/release.txt',
 | 
			
		||||
          input_draft: false,
 | 
			
		||||
          input_prerelease: false,
 | 
			
		||||
          input_preserve_order: undefined,
 | 
			
		||||
          input_files: [],
 | 
			
		||||
          input_overwrite_files: undefined,
 | 
			
		||||
          input_name: undefined,
 | 
			
		||||
          input_tag_name: undefined,
 | 
			
		||||
          input_target_commitish: undefined,
 | 
			
		||||
@@ -80,19 +99,20 @@ describe("util", () => {
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
    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', () => {
 | 
			
		||||
      assert.equal(
 | 
			
		||||
        "bar",
 | 
			
		||||
        'bar',
 | 
			
		||||
        releaseBody({
 | 
			
		||||
          github_ref: "",
 | 
			
		||||
          github_repository: "",
 | 
			
		||||
          github_token: "",
 | 
			
		||||
          input_body: "foo",
 | 
			
		||||
          input_body_path: "__tests__/release.txt",
 | 
			
		||||
          github_ref: '',
 | 
			
		||||
          github_repository: '',
 | 
			
		||||
          github_token: '',
 | 
			
		||||
          input_body: 'foo',
 | 
			
		||||
          input_body_path: '__tests__/release.txt',
 | 
			
		||||
          input_draft: false,
 | 
			
		||||
          input_prerelease: false,
 | 
			
		||||
          input_preserve_order: undefined,
 | 
			
		||||
          input_files: [],
 | 
			
		||||
          input_overwrite_files: undefined,
 | 
			
		||||
          input_name: undefined,
 | 
			
		||||
          input_tag_name: undefined,
 | 
			
		||||
          input_target_commitish: undefined,
 | 
			
		||||
@@ -102,9 +122,55 @@ describe("util", () => {
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
    it('falls back to body when body_path is missing', () => {
 | 
			
		||||
      assert.equal(
 | 
			
		||||
        releaseBody({
 | 
			
		||||
          github_ref: '',
 | 
			
		||||
          github_repository: '',
 | 
			
		||||
          github_token: '',
 | 
			
		||||
          input_body: 'fallback-body',
 | 
			
		||||
          input_body_path: '__tests__/does-not-exist.txt',
 | 
			
		||||
          input_draft: false,
 | 
			
		||||
          input_prerelease: false,
 | 
			
		||||
          input_files: [],
 | 
			
		||||
          input_overwrite_files: undefined,
 | 
			
		||||
          input_preserve_order: undefined,
 | 
			
		||||
          input_name: undefined,
 | 
			
		||||
          input_tag_name: undefined,
 | 
			
		||||
          input_target_commitish: undefined,
 | 
			
		||||
          input_discussion_category_name: undefined,
 | 
			
		||||
          input_generate_release_notes: false,
 | 
			
		||||
          input_make_latest: undefined,
 | 
			
		||||
        }),
 | 
			
		||||
        'fallback-body',
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
    it('returns undefined when body_path is missing and body is not provided', () => {
 | 
			
		||||
      assert.equal(
 | 
			
		||||
        releaseBody({
 | 
			
		||||
          github_ref: '',
 | 
			
		||||
          github_repository: '',
 | 
			
		||||
          github_token: '',
 | 
			
		||||
          input_body: undefined,
 | 
			
		||||
          input_body_path: '__tests__/does-not-exist.txt',
 | 
			
		||||
          input_draft: false,
 | 
			
		||||
          input_prerelease: false,
 | 
			
		||||
          input_files: [],
 | 
			
		||||
          input_overwrite_files: undefined,
 | 
			
		||||
          input_preserve_order: undefined,
 | 
			
		||||
          input_name: undefined,
 | 
			
		||||
          input_tag_name: undefined,
 | 
			
		||||
          input_target_commitish: undefined,
 | 
			
		||||
          input_discussion_category_name: undefined,
 | 
			
		||||
          input_generate_release_notes: false,
 | 
			
		||||
          input_make_latest: undefined,
 | 
			
		||||
        }),
 | 
			
		||||
        undefined,
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  describe("parseConfig", () => {
 | 
			
		||||
    it("parses basic config", () => {
 | 
			
		||||
  describe('parseConfig', () => {
 | 
			
		||||
    it('parses basic config', () => {
 | 
			
		||||
      assert.deepStrictEqual(
 | 
			
		||||
        parseConfig({
 | 
			
		||||
          // note: inputs declared in actions.yml, even when declared not required,
 | 
			
		||||
@@ -113,13 +179,14 @@ describe("util", () => {
 | 
			
		||||
          // as an empty string !== undefined in terms of what we pass to the api
 | 
			
		||||
          // so we cover that in a test case here to ensure undefined values are actually
 | 
			
		||||
          // resolved as undefined and not empty strings
 | 
			
		||||
          INPUT_TARGET_COMMITISH: "",
 | 
			
		||||
          INPUT_DISCUSSION_CATEGORY_NAME: "",
 | 
			
		||||
          INPUT_TARGET_COMMITISH: '',
 | 
			
		||||
          INPUT_DISCUSSION_CATEGORY_NAME: '',
 | 
			
		||||
        }),
 | 
			
		||||
        {
 | 
			
		||||
          github_ref: "",
 | 
			
		||||
          github_repository: "",
 | 
			
		||||
          github_token: "",
 | 
			
		||||
          github_ref: '',
 | 
			
		||||
          github_repository: '',
 | 
			
		||||
          github_token: '',
 | 
			
		||||
          input_working_directory: undefined,
 | 
			
		||||
          input_append_body: false,
 | 
			
		||||
          input_body: undefined,
 | 
			
		||||
          input_body_path: undefined,
 | 
			
		||||
@@ -127,6 +194,7 @@ describe("util", () => {
 | 
			
		||||
          input_prerelease: undefined,
 | 
			
		||||
          input_preserve_order: undefined,
 | 
			
		||||
          input_files: [],
 | 
			
		||||
          input_overwrite_files: undefined,
 | 
			
		||||
          input_name: undefined,
 | 
			
		||||
          input_tag_name: undefined,
 | 
			
		||||
          input_fail_on_unmatched_files: false,
 | 
			
		||||
@@ -138,41 +206,44 @@ describe("util", () => {
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("parses basic config with commitish", () => {
 | 
			
		||||
    it('parses basic config with commitish', () => {
 | 
			
		||||
      assert.deepStrictEqual(
 | 
			
		||||
        parseConfig({
 | 
			
		||||
          INPUT_TARGET_COMMITISH: "affa18ef97bc9db20076945705aba8c516139abd",
 | 
			
		||||
          INPUT_TARGET_COMMITISH: 'affa18ef97bc9db20076945705aba8c516139abd',
 | 
			
		||||
        }),
 | 
			
		||||
        {
 | 
			
		||||
          github_ref: "",
 | 
			
		||||
          github_repository: "",
 | 
			
		||||
          github_token: "",
 | 
			
		||||
          github_ref: '',
 | 
			
		||||
          github_repository: '',
 | 
			
		||||
          github_token: '',
 | 
			
		||||
          input_working_directory: undefined,
 | 
			
		||||
          input_append_body: false,
 | 
			
		||||
          input_body: undefined,
 | 
			
		||||
          input_body_path: undefined,
 | 
			
		||||
          input_draft: undefined,
 | 
			
		||||
          input_prerelease: undefined,
 | 
			
		||||
          input_files: [],
 | 
			
		||||
          input_overwrite_files: undefined,
 | 
			
		||||
          input_preserve_order: undefined,
 | 
			
		||||
          input_name: undefined,
 | 
			
		||||
          input_tag_name: undefined,
 | 
			
		||||
          input_fail_on_unmatched_files: false,
 | 
			
		||||
          input_target_commitish: "affa18ef97bc9db20076945705aba8c516139abd",
 | 
			
		||||
          input_target_commitish: 'affa18ef97bc9db20076945705aba8c516139abd',
 | 
			
		||||
          input_discussion_category_name: undefined,
 | 
			
		||||
          input_generate_release_notes: false,
 | 
			
		||||
          input_make_latest: undefined,
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
    it("supports discussion category names", () => {
 | 
			
		||||
    it('supports discussion category names', () => {
 | 
			
		||||
      assert.deepStrictEqual(
 | 
			
		||||
        parseConfig({
 | 
			
		||||
          INPUT_DISCUSSION_CATEGORY_NAME: "releases",
 | 
			
		||||
          INPUT_DISCUSSION_CATEGORY_NAME: 'releases',
 | 
			
		||||
        }),
 | 
			
		||||
        {
 | 
			
		||||
          github_ref: "",
 | 
			
		||||
          github_repository: "",
 | 
			
		||||
          github_token: "",
 | 
			
		||||
          github_ref: '',
 | 
			
		||||
          github_repository: '',
 | 
			
		||||
          github_token: '',
 | 
			
		||||
          input_working_directory: undefined,
 | 
			
		||||
          input_append_body: false,
 | 
			
		||||
          input_body: undefined,
 | 
			
		||||
          input_body_path: undefined,
 | 
			
		||||
@@ -181,25 +252,27 @@ describe("util", () => {
 | 
			
		||||
          input_files: [],
 | 
			
		||||
          input_preserve_order: undefined,
 | 
			
		||||
          input_name: undefined,
 | 
			
		||||
          input_overwrite_files: undefined,
 | 
			
		||||
          input_tag_name: undefined,
 | 
			
		||||
          input_fail_on_unmatched_files: false,
 | 
			
		||||
          input_target_commitish: undefined,
 | 
			
		||||
          input_discussion_category_name: "releases",
 | 
			
		||||
          input_discussion_category_name: 'releases',
 | 
			
		||||
          input_generate_release_notes: false,
 | 
			
		||||
          input_make_latest: undefined,
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("supports generating release notes", () => {
 | 
			
		||||
    it('supports generating release notes', () => {
 | 
			
		||||
      assert.deepStrictEqual(
 | 
			
		||||
        parseConfig({
 | 
			
		||||
          INPUT_GENERATE_RELEASE_NOTES: "true",
 | 
			
		||||
          INPUT_GENERATE_RELEASE_NOTES: 'true',
 | 
			
		||||
        }),
 | 
			
		||||
        {
 | 
			
		||||
          github_ref: "",
 | 
			
		||||
          github_repository: "",
 | 
			
		||||
          github_token: "",
 | 
			
		||||
          github_ref: '',
 | 
			
		||||
          github_repository: '',
 | 
			
		||||
          github_token: '',
 | 
			
		||||
          input_working_directory: undefined,
 | 
			
		||||
          input_append_body: false,
 | 
			
		||||
          input_body: undefined,
 | 
			
		||||
          input_body_path: undefined,
 | 
			
		||||
@@ -207,6 +280,7 @@ describe("util", () => {
 | 
			
		||||
          input_prerelease: undefined,
 | 
			
		||||
          input_preserve_order: undefined,
 | 
			
		||||
          input_files: [],
 | 
			
		||||
          input_overwrite_files: undefined,
 | 
			
		||||
          input_name: undefined,
 | 
			
		||||
          input_tag_name: undefined,
 | 
			
		||||
          input_fail_on_unmatched_files: false,
 | 
			
		||||
@@ -218,19 +292,20 @@ describe("util", () => {
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("prefers GITHUB_TOKEN over token input for backwards compatibility", () => {
 | 
			
		||||
    it('prefers GITHUB_TOKEN over token input for backwards compatibility', () => {
 | 
			
		||||
      assert.deepStrictEqual(
 | 
			
		||||
        parseConfig({
 | 
			
		||||
          INPUT_DRAFT: "false",
 | 
			
		||||
          INPUT_PRERELEASE: "true",
 | 
			
		||||
          INPUT_PRESERVE_ORDER: "true",
 | 
			
		||||
          GITHUB_TOKEN: "env-token",
 | 
			
		||||
          INPUT_TOKEN: "input-token",
 | 
			
		||||
          INPUT_DRAFT: 'false',
 | 
			
		||||
          INPUT_PRERELEASE: 'true',
 | 
			
		||||
          INPUT_PRESERVE_ORDER: 'true',
 | 
			
		||||
          GITHUB_TOKEN: 'env-token',
 | 
			
		||||
          INPUT_TOKEN: 'input-token',
 | 
			
		||||
        }),
 | 
			
		||||
        {
 | 
			
		||||
          github_ref: "",
 | 
			
		||||
          github_repository: "",
 | 
			
		||||
          github_token: "env-token",
 | 
			
		||||
          github_ref: '',
 | 
			
		||||
          github_repository: '',
 | 
			
		||||
          github_token: 'env-token',
 | 
			
		||||
          input_working_directory: undefined,
 | 
			
		||||
          input_append_body: false,
 | 
			
		||||
          input_body: undefined,
 | 
			
		||||
          input_body_path: undefined,
 | 
			
		||||
@@ -238,6 +313,7 @@ describe("util", () => {
 | 
			
		||||
          input_prerelease: true,
 | 
			
		||||
          input_preserve_order: true,
 | 
			
		||||
          input_files: [],
 | 
			
		||||
          input_overwrite_files: undefined,
 | 
			
		||||
          input_name: undefined,
 | 
			
		||||
          input_tag_name: undefined,
 | 
			
		||||
          input_fail_on_unmatched_files: false,
 | 
			
		||||
@@ -248,17 +324,18 @@ describe("util", () => {
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
    it("uses input token as the source of GITHUB_TOKEN by default", () => {
 | 
			
		||||
    it('uses input token as the source of GITHUB_TOKEN by default', () => {
 | 
			
		||||
      assert.deepStrictEqual(
 | 
			
		||||
        parseConfig({
 | 
			
		||||
          INPUT_DRAFT: "false",
 | 
			
		||||
          INPUT_PRERELEASE: "true",
 | 
			
		||||
          INPUT_TOKEN: "input-token",
 | 
			
		||||
          INPUT_DRAFT: 'false',
 | 
			
		||||
          INPUT_PRERELEASE: 'true',
 | 
			
		||||
          INPUT_TOKEN: 'input-token',
 | 
			
		||||
        }),
 | 
			
		||||
        {
 | 
			
		||||
          github_ref: "",
 | 
			
		||||
          github_repository: "",
 | 
			
		||||
          github_token: "input-token",
 | 
			
		||||
          github_ref: '',
 | 
			
		||||
          github_repository: '',
 | 
			
		||||
          github_token: 'input-token',
 | 
			
		||||
          input_working_directory: undefined,
 | 
			
		||||
          input_append_body: false,
 | 
			
		||||
          input_body: undefined,
 | 
			
		||||
          input_body_path: undefined,
 | 
			
		||||
@@ -266,6 +343,7 @@ describe("util", () => {
 | 
			
		||||
          input_prerelease: true,
 | 
			
		||||
          input_preserve_order: undefined,
 | 
			
		||||
          input_files: [],
 | 
			
		||||
          input_overwrite_files: undefined,
 | 
			
		||||
          input_name: undefined,
 | 
			
		||||
          input_tag_name: undefined,
 | 
			
		||||
          input_fail_on_unmatched_files: false,
 | 
			
		||||
@@ -276,16 +354,17 @@ describe("util", () => {
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
    it("parses basic config with draft and prerelease", () => {
 | 
			
		||||
    it('parses basic config with draft and prerelease', () => {
 | 
			
		||||
      assert.deepStrictEqual(
 | 
			
		||||
        parseConfig({
 | 
			
		||||
          INPUT_DRAFT: "false",
 | 
			
		||||
          INPUT_PRERELEASE: "true",
 | 
			
		||||
          INPUT_DRAFT: 'false',
 | 
			
		||||
          INPUT_PRERELEASE: 'true',
 | 
			
		||||
        }),
 | 
			
		||||
        {
 | 
			
		||||
          github_ref: "",
 | 
			
		||||
          github_repository: "",
 | 
			
		||||
          github_token: "",
 | 
			
		||||
          github_ref: '',
 | 
			
		||||
          github_repository: '',
 | 
			
		||||
          github_token: '',
 | 
			
		||||
          input_working_directory: undefined,
 | 
			
		||||
          input_append_body: false,
 | 
			
		||||
          input_body: undefined,
 | 
			
		||||
          input_body_path: undefined,
 | 
			
		||||
@@ -293,6 +372,7 @@ describe("util", () => {
 | 
			
		||||
          input_prerelease: true,
 | 
			
		||||
          input_preserve_order: undefined,
 | 
			
		||||
          input_files: [],
 | 
			
		||||
          input_overwrite_files: undefined,
 | 
			
		||||
          input_name: undefined,
 | 
			
		||||
          input_tag_name: undefined,
 | 
			
		||||
          input_fail_on_unmatched_files: false,
 | 
			
		||||
@@ -303,15 +383,16 @@ describe("util", () => {
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
    it("parses basic config where make_latest is passed", () => {
 | 
			
		||||
    it('parses basic config where make_latest is passed', () => {
 | 
			
		||||
      assert.deepStrictEqual(
 | 
			
		||||
        parseConfig({
 | 
			
		||||
          INPUT_MAKE_LATEST: "false",
 | 
			
		||||
          INPUT_MAKE_LATEST: 'false',
 | 
			
		||||
        }),
 | 
			
		||||
        {
 | 
			
		||||
          github_ref: "",
 | 
			
		||||
          github_repository: "",
 | 
			
		||||
          github_token: "",
 | 
			
		||||
          github_ref: '',
 | 
			
		||||
          github_repository: '',
 | 
			
		||||
          github_token: '',
 | 
			
		||||
          input_working_directory: undefined,
 | 
			
		||||
          input_append_body: false,
 | 
			
		||||
          input_body: undefined,
 | 
			
		||||
          input_body_path: undefined,
 | 
			
		||||
@@ -320,24 +401,26 @@ describe("util", () => {
 | 
			
		||||
          input_preserve_order: undefined,
 | 
			
		||||
          input_files: [],
 | 
			
		||||
          input_name: undefined,
 | 
			
		||||
          input_overwrite_files: undefined,
 | 
			
		||||
          input_tag_name: undefined,
 | 
			
		||||
          input_fail_on_unmatched_files: false,
 | 
			
		||||
          input_target_commitish: undefined,
 | 
			
		||||
          input_discussion_category_name: undefined,
 | 
			
		||||
          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', () => {
 | 
			
		||||
      assert.deepStrictEqual(
 | 
			
		||||
        parseConfig({
 | 
			
		||||
          INPUT_APPEND_BODY: "true",
 | 
			
		||||
          INPUT_APPEND_BODY: 'true',
 | 
			
		||||
        }),
 | 
			
		||||
        {
 | 
			
		||||
          github_ref: "",
 | 
			
		||||
          github_repository: "",
 | 
			
		||||
          github_token: "",
 | 
			
		||||
          github_ref: '',
 | 
			
		||||
          github_repository: '',
 | 
			
		||||
          github_token: '',
 | 
			
		||||
          input_working_directory: undefined,
 | 
			
		||||
          input_append_body: true,
 | 
			
		||||
          input_body: undefined,
 | 
			
		||||
          input_body_path: undefined,
 | 
			
		||||
@@ -345,6 +428,7 @@ describe("util", () => {
 | 
			
		||||
          input_prerelease: undefined,
 | 
			
		||||
          input_preserve_order: undefined,
 | 
			
		||||
          input_files: [],
 | 
			
		||||
          input_overwrite_files: undefined,
 | 
			
		||||
          input_name: undefined,
 | 
			
		||||
          input_tag_name: undefined,
 | 
			
		||||
          input_fail_on_unmatched_files: false,
 | 
			
		||||
@@ -356,46 +440,86 @@ describe("util", () => {
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  describe("isTag", () => {
 | 
			
		||||
    it("returns true for tags", async () => {
 | 
			
		||||
      assert.equal(isTag("refs/tags/foo"), true);
 | 
			
		||||
  describe('isTag', () => {
 | 
			
		||||
    it('returns true for tags', async () => {
 | 
			
		||||
      assert.equal(isTag('refs/tags/foo'), true);
 | 
			
		||||
    });
 | 
			
		||||
    it("returns false for other kinds of refs", async () => {
 | 
			
		||||
      assert.equal(isTag("refs/heads/master"), false);
 | 
			
		||||
    it('returns false for other kinds of refs', async () => {
 | 
			
		||||
      assert.equal(isTag('refs/heads/master'), false);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("paths", () => {
 | 
			
		||||
    it("resolves files given a set of paths", async () => {
 | 
			
		||||
      assert.deepStrictEqual(
 | 
			
		||||
        paths(["tests/data/**/*", "tests/data/does/not/exist/*"]),
 | 
			
		||||
        ["tests/data/foo/bar.txt"],
 | 
			
		||||
      );
 | 
			
		||||
  describe('paths', () => {
 | 
			
		||||
    it('resolves files given a set of paths', async () => {
 | 
			
		||||
      assert.deepStrictEqual(paths(['tests/data/**/*', 'tests/data/does/not/exist/*']), [
 | 
			
		||||
        'tests/data/foo/bar.txt',
 | 
			
		||||
      ]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('resolves files relative to working_directory', async () => {
 | 
			
		||||
      assert.deepStrictEqual(paths(['data/**/*'], 'tests'), ['tests/data/foo/bar.txt']);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("unmatchedPatterns", () => {
 | 
			
		||||
  describe('unmatchedPatterns', () => {
 | 
			
		||||
    it("returns the patterns that don't match any files", async () => {
 | 
			
		||||
      assert.deepStrictEqual(
 | 
			
		||||
        unmatchedPatterns(["tests/data/**/*", "tests/data/does/not/exist/*"]),
 | 
			
		||||
        ["tests/data/does/not/exist/*"],
 | 
			
		||||
        unmatchedPatterns(['tests/data/**/*', 'tests/data/does/not/exist/*']),
 | 
			
		||||
        ['tests/data/does/not/exist/*'],
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('resolves unmatched relative to working_directory', async () => {
 | 
			
		||||
      assert.deepStrictEqual(unmatchedPatterns(['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");
 | 
			
		||||
  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('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");
 | 
			
		||||
    it('returns the same string if there are no spaces', () => {
 | 
			
		||||
      expect(alignAssetName('JohnDoe')).toBe('JohnDoe');
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('parseInputFiles edge cases', () => {
 | 
			
		||||
  it('handles multiple brace groups on same line', () => {
 | 
			
		||||
    assert.deepStrictEqual(parseInputFiles('./**/*.{exe,deb},./dist/**/*.{zip,tar.gz}'), [
 | 
			
		||||
      './**/*.{exe,deb}',
 | 
			
		||||
      './dist/**/*.{zip,tar.gz}',
 | 
			
		||||
    ]);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('handles nested braces', () => {
 | 
			
		||||
    assert.deepStrictEqual(parseInputFiles('path/{a,{b,c}}/file.txt'), ['path/{a,{b,c}}/file.txt']);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('handles empty comma-separated values', () => {
 | 
			
		||||
    assert.deepStrictEqual(parseInputFiles('foo,,bar'), ['foo', 'bar']);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('handles commas with spaces around braces', () => {
 | 
			
		||||
    assert.deepStrictEqual(parseInputFiles(' ./**/*.{exe,deb} , file.txt '), [
 | 
			
		||||
      './**/*.{exe,deb}',
 | 
			
		||||
      'file.txt',
 | 
			
		||||
    ]);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('handles mixed newlines and commas with braces', () => {
 | 
			
		||||
    assert.deepStrictEqual(parseInputFiles('file1.txt\n./**/*.{exe,deb},file2.txt\nfile3.txt'), [
 | 
			
		||||
      'file1.txt',
 | 
			
		||||
      './**/*.{exe,deb}',
 | 
			
		||||
      'file2.txt',
 | 
			
		||||
      'file3.txt',
 | 
			
		||||
    ]);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,13 @@ inputs:
 | 
			
		||||
  files:
 | 
			
		||||
    description: "Newline-delimited list of path globs for asset files to upload"
 | 
			
		||||
    required: false
 | 
			
		||||
  working_directory:
 | 
			
		||||
    description: "Base directory to resolve 'files' globs against (defaults to job working-directory)"
 | 
			
		||||
    required: false
 | 
			
		||||
  overwrite_files:
 | 
			
		||||
    description: "Overwrite existing files with the same name. Defaults to true"
 | 
			
		||||
    required: false
 | 
			
		||||
    default: 'true'
 | 
			
		||||
  fail_on_unmatched_files:
 | 
			
		||||
    description: "Fails if any of the `files` globs match nothing. Defaults to false"
 | 
			
		||||
    required: false
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								dist/index.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								dist/index.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -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;
 | 
			
		||||
							
								
								
									
										5224
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5224
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										37
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										37
									
								
								package.json
									
									
									
									
									
								
							@@ -1,13 +1,14 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "action-gh-release",
 | 
			
		||||
  "version": "2.2.2",
 | 
			
		||||
  "version": "2.4.1",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "description": "GitHub Action for creating GitHub Releases",
 | 
			
		||||
  "main": "lib/main.js",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "build": "ncc build src/main.ts --minify",
 | 
			
		||||
    "build": "ncc build src/main.ts --minify --target es2022",
 | 
			
		||||
    "build-debug": "ncc build src/main.ts --v8-cache --source-map",
 | 
			
		||||
    "test": "jest",
 | 
			
		||||
    "typecheck": "tsc --noEmit",
 | 
			
		||||
    "test": "vitest --coverage",
 | 
			
		||||
    "fmt": "prettier --write \"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"
 | 
			
		||||
@@ -22,24 +23,22 @@
 | 
			
		||||
  "author": "softprops",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@actions/core": "^1.11.1",
 | 
			
		||||
    "@actions/github": "^6.0.0",
 | 
			
		||||
    "@octokit/plugin-retry": "^7.2.1",
 | 
			
		||||
    "@octokit/plugin-throttling": "^10.0.0",
 | 
			
		||||
    "glob": "^11.0.1",
 | 
			
		||||
    "mime": "^3.0.0"
 | 
			
		||||
    "@actions/github": "^6.0.1",
 | 
			
		||||
    "@octokit/plugin-retry": "^8.0.3",
 | 
			
		||||
    "@octokit/plugin-throttling": "^11.0.3",
 | 
			
		||||
    "glob": "^11.0.3",
 | 
			
		||||
    "mime-types": "^3.0.1"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@types/glob": "^8.1.0",
 | 
			
		||||
    "@types/jest": "^29.5.14",
 | 
			
		||||
    "@types/mime": "^3.0.1",
 | 
			
		||||
    "@types/node": "^22.14.1",
 | 
			
		||||
    "@vercel/ncc": "^0.38.3",
 | 
			
		||||
    "jest": "^29.3.1",
 | 
			
		||||
    "jest-circus": "^29.3.1",
 | 
			
		||||
    "prettier": "3.5.3",
 | 
			
		||||
    "ts-jest": "^29.3.2",
 | 
			
		||||
    "@types/glob": "^9.0.0",
 | 
			
		||||
    "@types/mime-types": "^3.0.1",
 | 
			
		||||
    "@types/node": "^20.19.24",
 | 
			
		||||
    "@vercel/ncc": "^0.38.4",
 | 
			
		||||
    "@vitest/coverage-v8": "^4.0.6",
 | 
			
		||||
    "prettier": "3.6.2",
 | 
			
		||||
    "ts-node": "^10.9.2",
 | 
			
		||||
    "typescript": "^5.8.3",
 | 
			
		||||
    "typescript-formatter": "^7.2.2"
 | 
			
		||||
    "typescript": "^5.9.3",
 | 
			
		||||
    "typescript-formatter": "^7.2.2",
 | 
			
		||||
    "vitest": "^4.0.4"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										133
									
								
								src/github.ts
									
									
									
									
									
								
							
							
						
						
									
										133
									
								
								src/github.ts
									
									
									
									
									
								
							@@ -1,9 +1,9 @@
 | 
			
		||||
import { GitHub } from "@actions/github/lib/utils";
 | 
			
		||||
import { Config, isTag, releaseBody, alignAssetName } from "./util";
 | 
			
		||||
import { statSync } from "fs";
 | 
			
		||||
import { open } from "fs/promises";
 | 
			
		||||
import { getType } from "mime";
 | 
			
		||||
import { basename } from "path";
 | 
			
		||||
import { GitHub } from '@actions/github/lib/utils';
 | 
			
		||||
import { statSync } from 'fs';
 | 
			
		||||
import { open } from 'fs/promises';
 | 
			
		||||
import { lookup } from 'mime-types';
 | 
			
		||||
import { basename } from 'path';
 | 
			
		||||
import { alignAssetName, Config, isTag, releaseBody } from './util';
 | 
			
		||||
 | 
			
		||||
type GitHub = InstanceType<typeof GitHub>;
 | 
			
		||||
 | 
			
		||||
@@ -27,11 +27,7 @@ export interface Release {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Releaser {
 | 
			
		||||
  getReleaseByTag(params: {
 | 
			
		||||
    owner: string;
 | 
			
		||||
    repo: string;
 | 
			
		||||
    tag: string;
 | 
			
		||||
  }): Promise<{ data: Release }>;
 | 
			
		||||
  getReleaseByTag(params: { owner: string; repo: string; tag: string }): Promise<{ data: Release }>;
 | 
			
		||||
 | 
			
		||||
  createRelease(params: {
 | 
			
		||||
    owner: string;
 | 
			
		||||
@@ -44,7 +40,7 @@ export interface Releaser {
 | 
			
		||||
    target_commitish: string | undefined;
 | 
			
		||||
    discussion_category_name: string | undefined;
 | 
			
		||||
    generate_release_notes: boolean | undefined;
 | 
			
		||||
    make_latest: "true" | "false" | "legacy" | undefined;
 | 
			
		||||
    make_latest: 'true' | 'false' | 'legacy' | undefined;
 | 
			
		||||
  }): Promise<{ data: Release }>;
 | 
			
		||||
 | 
			
		||||
  updateRelease(params: {
 | 
			
		||||
@@ -59,13 +55,10 @@ export interface Releaser {
 | 
			
		||||
    prerelease: boolean | undefined;
 | 
			
		||||
    discussion_category_name: string | undefined;
 | 
			
		||||
    generate_release_notes: boolean | undefined;
 | 
			
		||||
    make_latest: "true" | "false" | "legacy" | undefined;
 | 
			
		||||
    make_latest: 'true' | 'false' | 'legacy' | undefined;
 | 
			
		||||
  }): Promise<{ data: Release }>;
 | 
			
		||||
 | 
			
		||||
  allReleases(params: {
 | 
			
		||||
    owner: string;
 | 
			
		||||
    repo: string;
 | 
			
		||||
  }): AsyncIterableIterator<{ data: Release[] }>;
 | 
			
		||||
  allReleases(params: { owner: string; repo: string }): AsyncIterableIterator<{ data: Release[] }>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class GitHubReleaser implements Releaser {
 | 
			
		||||
@@ -93,11 +86,11 @@ export class GitHubReleaser implements Releaser {
 | 
			
		||||
    target_commitish: string | undefined;
 | 
			
		||||
    discussion_category_name: string | undefined;
 | 
			
		||||
    generate_release_notes: boolean | undefined;
 | 
			
		||||
    make_latest: "true" | "false" | "legacy" | undefined;
 | 
			
		||||
    make_latest: 'true' | 'false' | 'legacy' | undefined;
 | 
			
		||||
  }): Promise<{ data: Release }> {
 | 
			
		||||
    if (
 | 
			
		||||
      typeof params.make_latest === "string" &&
 | 
			
		||||
      !["true", "false", "legacy"].includes(params.make_latest)
 | 
			
		||||
      typeof params.make_latest === 'string' &&
 | 
			
		||||
      !['true', 'false', 'legacy'].includes(params.make_latest)
 | 
			
		||||
    ) {
 | 
			
		||||
      params.make_latest = undefined;
 | 
			
		||||
    }
 | 
			
		||||
@@ -117,11 +110,11 @@ export class GitHubReleaser implements Releaser {
 | 
			
		||||
    prerelease: boolean | undefined;
 | 
			
		||||
    discussion_category_name: string | undefined;
 | 
			
		||||
    generate_release_notes: boolean | undefined;
 | 
			
		||||
    make_latest: "true" | "false" | "legacy" | undefined;
 | 
			
		||||
    make_latest: 'true' | 'false' | 'legacy' | undefined;
 | 
			
		||||
  }): Promise<{ data: Release }> {
 | 
			
		||||
    if (
 | 
			
		||||
      typeof params.make_latest === "string" &&
 | 
			
		||||
      !["true", "false", "legacy"].includes(params.make_latest)
 | 
			
		||||
      typeof params.make_latest === 'string' &&
 | 
			
		||||
      !['true', 'false', 'legacy'].includes(params.make_latest)
 | 
			
		||||
    ) {
 | 
			
		||||
      params.make_latest = undefined;
 | 
			
		||||
    }
 | 
			
		||||
@@ -129,10 +122,7 @@ export class GitHubReleaser implements Releaser {
 | 
			
		||||
    return this.github.rest.repos.updateRelease(params);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  allReleases(params: {
 | 
			
		||||
    owner: string;
 | 
			
		||||
    repo: string;
 | 
			
		||||
  }): AsyncIterableIterator<{ data: Release[] }> {
 | 
			
		||||
  allReleases(params: { owner: string; repo: string }): AsyncIterableIterator<{ data: Release[] }> {
 | 
			
		||||
    const updatedParams = { per_page: 100, ...params };
 | 
			
		||||
    return this.github.paginate.iterator(
 | 
			
		||||
      this.github.rest.repos.listReleases.endpoint.merge(updatedParams),
 | 
			
		||||
@@ -149,7 +139,7 @@ export const asset = (path: string): ReleaseAsset => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const mimeOrDefault = (path: string): string => {
 | 
			
		||||
  return getType(path) || "application/octet-stream";
 | 
			
		||||
  return lookup(path) || 'application/octet-stream';
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const upload = async (
 | 
			
		||||
@@ -159,7 +149,7 @@ export const upload = async (
 | 
			
		||||
  path: string,
 | 
			
		||||
  currentAssets: Array<{ id: number; name: string }>,
 | 
			
		||||
): Promise<any> => {
 | 
			
		||||
  const [owner, repo] = config.github_repository.split("/");
 | 
			
		||||
  const [owner, repo] = config.github_repository.split('/');
 | 
			
		||||
  const { name, mime, size } = asset(path);
 | 
			
		||||
  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.
 | 
			
		||||
@@ -168,27 +158,32 @@ export const upload = async (
 | 
			
		||||
    ({ name: currentName }) => currentName == alignAssetName(name),
 | 
			
		||||
  );
 | 
			
		||||
  if (currentAsset) {
 | 
			
		||||
    console.log(`♻️ Deleting previously uploaded asset ${name}...`);
 | 
			
		||||
    await github.rest.repos.deleteReleaseAsset({
 | 
			
		||||
      asset_id: currentAsset.id || 1,
 | 
			
		||||
      owner,
 | 
			
		||||
      repo,
 | 
			
		||||
    });
 | 
			
		||||
    if (config.input_overwrite_files === false) {
 | 
			
		||||
      console.log(`Asset ${name} already exists and overwrite_files is false...`);
 | 
			
		||||
      return null;
 | 
			
		||||
    } else {
 | 
			
		||||
      console.log(`♻️ Deleting previously uploaded asset ${name}...`);
 | 
			
		||||
      await github.rest.repos.deleteReleaseAsset({
 | 
			
		||||
        asset_id: currentAsset.id || 1,
 | 
			
		||||
        owner,
 | 
			
		||||
        repo,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  console.log(`⬆️ Uploading ${name}...`);
 | 
			
		||||
  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({
 | 
			
		||||
      method: "POST",
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      url: endpoint.toString(),
 | 
			
		||||
      headers: {
 | 
			
		||||
        "content-length": `${size}`,
 | 
			
		||||
        "content-type": mime,
 | 
			
		||||
        'content-length': `${size}`,
 | 
			
		||||
        'content-type': mime,
 | 
			
		||||
        authorization: `token ${config.github_token}`,
 | 
			
		||||
      },
 | 
			
		||||
      data: fh.readableWebStream({ type: "bytes" }),
 | 
			
		||||
      data: fh.readableWebStream({ type: 'bytes' }),
 | 
			
		||||
    });
 | 
			
		||||
    const json = resp.data;
 | 
			
		||||
    if (resp.status !== 201) {
 | 
			
		||||
@@ -212,25 +207,18 @@ export const release = async (
 | 
			
		||||
): Promise<Release> => {
 | 
			
		||||
  if (maxRetries <= 0) {
 | 
			
		||||
    console.log(`❌ Too many retries. Aborting...`);
 | 
			
		||||
    throw new Error("Too many retries.");
 | 
			
		||||
    throw new Error('Too many retries.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const [owner, repo] = config.github_repository.split("/");
 | 
			
		||||
  const [owner, repo] = config.github_repository.split('/');
 | 
			
		||||
  const tag =
 | 
			
		||||
    config.input_tag_name ||
 | 
			
		||||
    (isTag(config.github_ref)
 | 
			
		||||
      ? config.github_ref.replace("refs/tags/", "")
 | 
			
		||||
      : "");
 | 
			
		||||
    (isTag(config.github_ref) ? config.github_ref.replace('refs/tags/', '') : '');
 | 
			
		||||
 | 
			
		||||
  const discussion_category_name = config.input_discussion_category_name;
 | 
			
		||||
  const generate_release_notes = config.input_generate_release_notes;
 | 
			
		||||
  try {
 | 
			
		||||
    const _release: Release | undefined = await findTagFromReleases(
 | 
			
		||||
      releaser,
 | 
			
		||||
      owner,
 | 
			
		||||
      repo,
 | 
			
		||||
      tag,
 | 
			
		||||
    );
 | 
			
		||||
    const _release: Release | undefined = await findTagFromReleases(releaser, owner, repo, tag);
 | 
			
		||||
 | 
			
		||||
    if (_release === undefined) {
 | 
			
		||||
      return await createRelease(
 | 
			
		||||
@@ -246,9 +234,7 @@ export const release = async (
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let existingRelease: Release = _release!;
 | 
			
		||||
    console.log(
 | 
			
		||||
      `Found release ${existingRelease.name} (with id=${existingRelease.id})`,
 | 
			
		||||
    );
 | 
			
		||||
    console.log(`Found release ${existingRelease.name} (with id=${existingRelease.id})`);
 | 
			
		||||
 | 
			
		||||
    const release_id = existingRelease.id;
 | 
			
		||||
    let target_commitish: string;
 | 
			
		||||
@@ -270,23 +256,18 @@ export const release = async (
 | 
			
		||||
    // 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
 | 
			
		||||
    // no one wants
 | 
			
		||||
    const workflowBody = releaseBody(config) || "";
 | 
			
		||||
    const existingReleaseBody = existingRelease.body || "";
 | 
			
		||||
    const workflowBody = releaseBody(config) || '';
 | 
			
		||||
    const existingReleaseBody = existingRelease.body || '';
 | 
			
		||||
    let body: string;
 | 
			
		||||
    if (config.input_append_body && workflowBody && existingReleaseBody) {
 | 
			
		||||
      body = existingReleaseBody + "\n" + workflowBody;
 | 
			
		||||
      body = existingReleaseBody + '\n' + workflowBody;
 | 
			
		||||
    } else {
 | 
			
		||||
      body = workflowBody || existingReleaseBody;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const draft =
 | 
			
		||||
      config.input_draft !== undefined
 | 
			
		||||
        ? config.input_draft
 | 
			
		||||
        : existingRelease.draft;
 | 
			
		||||
    const draft = config.input_draft !== undefined ? config.input_draft : existingRelease.draft;
 | 
			
		||||
    const prerelease =
 | 
			
		||||
      config.input_prerelease !== undefined
 | 
			
		||||
        ? config.input_prerelease
 | 
			
		||||
        : existingRelease.prerelease;
 | 
			
		||||
      config.input_prerelease !== undefined ? config.input_prerelease : existingRelease.prerelease;
 | 
			
		||||
 | 
			
		||||
    const make_latest = config.input_make_latest;
 | 
			
		||||
 | 
			
		||||
@@ -370,13 +351,11 @@ async function createRelease(
 | 
			
		||||
  const prerelease = config.input_prerelease;
 | 
			
		||||
  const target_commitish = config.input_target_commitish;
 | 
			
		||||
  const make_latest = config.input_make_latest;
 | 
			
		||||
  let commitMessage: string = "";
 | 
			
		||||
  let commitMessage: string = '';
 | 
			
		||||
  if (target_commitish) {
 | 
			
		||||
    commitMessage = ` using commit "${target_commitish}"`;
 | 
			
		||||
  }
 | 
			
		||||
  console.log(
 | 
			
		||||
    `👩🏭 Creating new GitHub release for tag ${tag_name}${commitMessage}...`,
 | 
			
		||||
  );
 | 
			
		||||
  console.log(`👩🏭 Creating new GitHub release for tag ${tag_name}${commitMessage}...`);
 | 
			
		||||
  try {
 | 
			
		||||
    let release = await releaser.createRelease({
 | 
			
		||||
      owner,
 | 
			
		||||
@@ -400,17 +379,27 @@ async function createRelease(
 | 
			
		||||
    switch (error.status) {
 | 
			
		||||
      case 403:
 | 
			
		||||
        console.log(
 | 
			
		||||
          "Skip retry — your GitHub token/PAT does not have the required permission to create a release",
 | 
			
		||||
          'Skip retry — your GitHub token/PAT does not have the required permission to create a release',
 | 
			
		||||
        );
 | 
			
		||||
        throw error;
 | 
			
		||||
 | 
			
		||||
      case 404:
 | 
			
		||||
        console.log("Skip retry - discussion category mismatch");
 | 
			
		||||
        console.log('Skip retry - discussion category mismatch');
 | 
			
		||||
        throw error;
 | 
			
		||||
 | 
			
		||||
      case 422:
 | 
			
		||||
        console.log("Skip retry - validation failed");
 | 
			
		||||
        throw error;
 | 
			
		||||
        // Check if this is a race condition with "already_exists" error
 | 
			
		||||
        const errorData = error.response?.data;
 | 
			
		||||
        if (errorData?.errors?.[0]?.code === 'already_exists') {
 | 
			
		||||
          console.log(
 | 
			
		||||
            '⚠️ Release already exists (race condition detected), retrying to find and update existing release...',
 | 
			
		||||
          );
 | 
			
		||||
          // Don't throw - allow retry to find existing release
 | 
			
		||||
        } else {
 | 
			
		||||
          console.log('Skip retry - validation failed');
 | 
			
		||||
          throw error;
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.log(`retrying... (${maxRetries - 1} retries remaining)`);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										72
									
								
								src/main.ts
									
									
									
									
									
								
							
							
						
						
									
										72
									
								
								src/main.ts
									
									
									
									
									
								
							@@ -1,28 +1,18 @@
 | 
			
		||||
import {
 | 
			
		||||
  paths,
 | 
			
		||||
  parseConfig,
 | 
			
		||||
  isTag,
 | 
			
		||||
  unmatchedPatterns,
 | 
			
		||||
  uploadUrl,
 | 
			
		||||
} from "./util";
 | 
			
		||||
import { release, upload, GitHubReleaser } from "./github";
 | 
			
		||||
import { getOctokit } from "@actions/github";
 | 
			
		||||
import { setFailed, setOutput } from "@actions/core";
 | 
			
		||||
import { setFailed, setOutput } from '@actions/core';
 | 
			
		||||
import { getOctokit } from '@actions/github';
 | 
			
		||||
import { GitHubReleaser, release, upload } from './github';
 | 
			
		||||
import { isTag, parseConfig, paths, unmatchedPatterns, uploadUrl } from './util';
 | 
			
		||||
 | 
			
		||||
import { env } from "process";
 | 
			
		||||
import { env } from 'process';
 | 
			
		||||
 | 
			
		||||
async function run() {
 | 
			
		||||
  try {
 | 
			
		||||
    const config = parseConfig(env);
 | 
			
		||||
    if (
 | 
			
		||||
      !config.input_tag_name &&
 | 
			
		||||
      !isTag(config.github_ref) &&
 | 
			
		||||
      !config.input_draft
 | 
			
		||||
    ) {
 | 
			
		||||
    if (!config.input_tag_name && !isTag(config.github_ref) && !config.input_draft) {
 | 
			
		||||
      throw new Error(`⚠️ GitHub Releases requires a tag`);
 | 
			
		||||
    }
 | 
			
		||||
    if (config.input_files) {
 | 
			
		||||
      const patterns = unmatchedPatterns(config.input_files);
 | 
			
		||||
      const patterns = unmatchedPatterns(config.input_files, config.input_working_directory);
 | 
			
		||||
      patterns.forEach((pattern) => {
 | 
			
		||||
        if (config.input_fail_on_unmatched_files) {
 | 
			
		||||
          throw new Error(`⚠️  Pattern '${pattern}' does not match any files.`);
 | 
			
		||||
@@ -44,9 +34,7 @@ async function run() {
 | 
			
		||||
      //new oktokit(
 | 
			
		||||
      throttle: {
 | 
			
		||||
        onRateLimit: (retryAfter, options) => {
 | 
			
		||||
          console.warn(
 | 
			
		||||
            `Request quota exhausted for request ${options.method} ${options.url}`,
 | 
			
		||||
          );
 | 
			
		||||
          console.warn(`Request quota exhausted for request ${options.method} ${options.url}`);
 | 
			
		||||
          if (options.request.retryCount === 0) {
 | 
			
		||||
            // only retries once
 | 
			
		||||
            console.log(`Retrying after ${retryAfter} seconds!`);
 | 
			
		||||
@@ -55,56 +43,48 @@ async function run() {
 | 
			
		||||
        },
 | 
			
		||||
        onAbuseLimit: (retryAfter, options) => {
 | 
			
		||||
          // does not retry, only logs a warning
 | 
			
		||||
          console.warn(
 | 
			
		||||
            `Abuse detected for request ${options.method} ${options.url}`,
 | 
			
		||||
          );
 | 
			
		||||
          console.warn(`Abuse detected for request ${options.method} ${options.url}`);
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    //);
 | 
			
		||||
    const rel = await release(config, new GitHubReleaser(gh));
 | 
			
		||||
    if (config.input_files && config.input_files.length > 0) {
 | 
			
		||||
      const files = paths(config.input_files);
 | 
			
		||||
      const files = paths(config.input_files, config.input_working_directory);
 | 
			
		||||
      if (files.length == 0) {
 | 
			
		||||
        if (config.input_fail_on_unmatched_files) {
 | 
			
		||||
          throw new Error(
 | 
			
		||||
            `⚠️ ${config.input_files} does not include a 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.`,
 | 
			
		||||
          );
 | 
			
		||||
          console.warn(`🤔 ${config.input_files} does not include a valid file.`);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      const currentAssets = rel.assets;
 | 
			
		||||
 | 
			
		||||
      const uploadFile = async (path) => {
 | 
			
		||||
        const json = await upload(
 | 
			
		||||
          config,
 | 
			
		||||
          gh,
 | 
			
		||||
          uploadUrl(rel.upload_url),
 | 
			
		||||
          path,
 | 
			
		||||
          currentAssets,
 | 
			
		||||
        );
 | 
			
		||||
        delete json.uploader;
 | 
			
		||||
        const json = await upload(config, gh, uploadUrl(rel.upload_url), path, currentAssets);
 | 
			
		||||
        if (json) {
 | 
			
		||||
          delete json.uploader;
 | 
			
		||||
        }
 | 
			
		||||
        return json;
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      let assets;
 | 
			
		||||
      let results: (any | null)[];
 | 
			
		||||
      if (!config.input_preserve_order) {
 | 
			
		||||
        assets = await Promise.all(files.map(uploadFile));
 | 
			
		||||
        results = await Promise.all(files.map(uploadFile));
 | 
			
		||||
      } else {
 | 
			
		||||
        assets = [];
 | 
			
		||||
        results = [];
 | 
			
		||||
        for (const path of files) {
 | 
			
		||||
          assets.push(await uploadFile(path));
 | 
			
		||||
          results.push(await uploadFile(path));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      setOutput("assets", assets);
 | 
			
		||||
 | 
			
		||||
      const assets = results.filter(Boolean);
 | 
			
		||||
      setOutput('assets', assets);
 | 
			
		||||
    }
 | 
			
		||||
    console.log(`🎉 Release ready at ${rel.html_url}`);
 | 
			
		||||
    setOutput("url", rel.html_url);
 | 
			
		||||
    setOutput("id", rel.id.toString());
 | 
			
		||||
    setOutput("upload_url", rel.upload_url);
 | 
			
		||||
    setOutput('url', rel.html_url);
 | 
			
		||||
    setOutput('id', rel.id.toString());
 | 
			
		||||
    setOutput('upload_url', rel.upload_url);
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    setFailed(error.message);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										138
									
								
								src/util.ts
									
									
									
									
									
								
							
							
						
						
									
										138
									
								
								src/util.ts
									
									
									
									
									
								
							@@ -1,5 +1,6 @@
 | 
			
		||||
import * as glob from "glob";
 | 
			
		||||
import { statSync, readFileSync } from "fs";
 | 
			
		||||
import * as glob from 'glob';
 | 
			
		||||
import { statSync, readFileSync } from 'fs';
 | 
			
		||||
import * as pathLib from 'path';
 | 
			
		||||
 | 
			
		||||
export interface Config {
 | 
			
		||||
  github_token: string;
 | 
			
		||||
@@ -12,6 +13,8 @@ export interface Config {
 | 
			
		||||
  input_body?: string;
 | 
			
		||||
  input_body_path?: string;
 | 
			
		||||
  input_files?: string[];
 | 
			
		||||
  input_working_directory?: string;
 | 
			
		||||
  input_overwrite_files?: boolean;
 | 
			
		||||
  input_draft?: boolean;
 | 
			
		||||
  input_preserve_order?: boolean;
 | 
			
		||||
  input_prerelease?: boolean;
 | 
			
		||||
@@ -20,11 +23,11 @@ export interface Config {
 | 
			
		||||
  input_discussion_category_name?: string;
 | 
			
		||||
  input_generate_release_notes?: boolean;
 | 
			
		||||
  input_append_body?: boolean;
 | 
			
		||||
  input_make_latest: "true" | "false" | "legacy" | undefined;
 | 
			
		||||
  input_make_latest: 'true' | 'false' | 'legacy' | undefined;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const uploadUrl = (url: string): string => {
 | 
			
		||||
  const templateMarkerPos = url.indexOf("{");
 | 
			
		||||
  const templateMarkerPos = url.indexOf('{');
 | 
			
		||||
  if (templateMarkerPos > -1) {
 | 
			
		||||
    return url.substring(0, templateMarkerPos);
 | 
			
		||||
  }
 | 
			
		||||
@@ -32,84 +35,123 @@ export const uploadUrl = (url: string): string => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const releaseBody = (config: Config): string | undefined => {
 | 
			
		||||
  return (
 | 
			
		||||
    (config.input_body_path &&
 | 
			
		||||
      readFileSync(config.input_body_path).toString("utf8")) ||
 | 
			
		||||
    config.input_body
 | 
			
		||||
  );
 | 
			
		||||
  if (config.input_body_path) {
 | 
			
		||||
    try {
 | 
			
		||||
      const contents = readFileSync(config.input_body_path, 'utf8');
 | 
			
		||||
      return contents;
 | 
			
		||||
    } catch (err: any) {
 | 
			
		||||
      console.warn(
 | 
			
		||||
        `⚠️ Failed to read body_path "${config.input_body_path}" (${err?.code ?? 'ERR'}). Falling back to 'body' input.`,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return config.input_body;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type Env = { [key: string]: string | undefined };
 | 
			
		||||
 | 
			
		||||
const smartSplit = (input: string): string[] => {
 | 
			
		||||
  const result: string[] = [];
 | 
			
		||||
  let current = '';
 | 
			
		||||
  let braceDepth = 0;
 | 
			
		||||
 | 
			
		||||
  for (const ch of input) {
 | 
			
		||||
    if (ch === '{') {
 | 
			
		||||
      braceDepth++;
 | 
			
		||||
    }
 | 
			
		||||
    if (ch === '}') {
 | 
			
		||||
      braceDepth--;
 | 
			
		||||
    }
 | 
			
		||||
    if (ch === ',' && braceDepth === 0) {
 | 
			
		||||
      if (current.trim()) {
 | 
			
		||||
        result.push(current.trim());
 | 
			
		||||
      }
 | 
			
		||||
      current = '';
 | 
			
		||||
    } else {
 | 
			
		||||
      current += ch;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (current.trim()) {
 | 
			
		||||
    result.push(current.trim());
 | 
			
		||||
  }
 | 
			
		||||
  return result;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const parseInputFiles = (files: string): string[] => {
 | 
			
		||||
  return files.split(/\r?\n/).reduce<string[]>(
 | 
			
		||||
    (acc, line) =>
 | 
			
		||||
      acc
 | 
			
		||||
        .concat(line.split(","))
 | 
			
		||||
        .filter((pat) => pat)
 | 
			
		||||
        .map((pat) => pat.trim()),
 | 
			
		||||
    [],
 | 
			
		||||
  );
 | 
			
		||||
  return files
 | 
			
		||||
    .split(/\r?\n/)
 | 
			
		||||
    .flatMap((line) => smartSplit(line))
 | 
			
		||||
    .filter((pat) => pat.trim() !== '');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const parseConfig = (env: Env): Config => {
 | 
			
		||||
  return {
 | 
			
		||||
    github_token: env.GITHUB_TOKEN || env.INPUT_TOKEN || "",
 | 
			
		||||
    github_ref: env.GITHUB_REF || "",
 | 
			
		||||
    github_repository: env.INPUT_REPOSITORY || env.GITHUB_REPOSITORY || "",
 | 
			
		||||
    github_token: env.GITHUB_TOKEN || env.INPUT_TOKEN || '',
 | 
			
		||||
    github_ref: env.GITHUB_REF || '',
 | 
			
		||||
    github_repository: env.INPUT_REPOSITORY || env.GITHUB_REPOSITORY || '',
 | 
			
		||||
    input_name: env.INPUT_NAME,
 | 
			
		||||
    input_tag_name: env.INPUT_TAG_NAME?.trim(),
 | 
			
		||||
    input_body: env.INPUT_BODY,
 | 
			
		||||
    input_body_path: env.INPUT_BODY_PATH,
 | 
			
		||||
    input_files: parseInputFiles(env.INPUT_FILES || ""),
 | 
			
		||||
    input_draft: env.INPUT_DRAFT ? env.INPUT_DRAFT === "true" : undefined,
 | 
			
		||||
    input_preserve_order: env.INPUT_PRESERVE_ORDER
 | 
			
		||||
      ? env.INPUT_PRESERVE_ORDER == "true"
 | 
			
		||||
    input_files: parseInputFiles(env.INPUT_FILES || ''),
 | 
			
		||||
    input_working_directory: env.INPUT_WORKING_DIRECTORY || undefined,
 | 
			
		||||
    input_overwrite_files: env.INPUT_OVERWRITE_FILES
 | 
			
		||||
      ? env.INPUT_OVERWRITE_FILES == 'true'
 | 
			
		||||
      : undefined,
 | 
			
		||||
    input_prerelease: env.INPUT_PRERELEASE
 | 
			
		||||
      ? env.INPUT_PRERELEASE == "true"
 | 
			
		||||
      : undefined,
 | 
			
		||||
    input_fail_on_unmatched_files: env.INPUT_FAIL_ON_UNMATCHED_FILES == "true",
 | 
			
		||||
    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 ? env.INPUT_PRERELEASE == 'true' : undefined,
 | 
			
		||||
    input_fail_on_unmatched_files: env.INPUT_FAIL_ON_UNMATCHED_FILES == 'true',
 | 
			
		||||
    input_target_commitish: env.INPUT_TARGET_COMMITISH || undefined,
 | 
			
		||||
    input_discussion_category_name:
 | 
			
		||||
      env.INPUT_DISCUSSION_CATEGORY_NAME || undefined,
 | 
			
		||||
    input_generate_release_notes: env.INPUT_GENERATE_RELEASE_NOTES == "true",
 | 
			
		||||
    input_append_body: env.INPUT_APPEND_BODY == "true",
 | 
			
		||||
    input_discussion_category_name: env.INPUT_DISCUSSION_CATEGORY_NAME || undefined,
 | 
			
		||||
    input_generate_release_notes: env.INPUT_GENERATE_RELEASE_NOTES == 'true',
 | 
			
		||||
    input_append_body: env.INPUT_APPEND_BODY == 'true',
 | 
			
		||||
    input_make_latest: parseMakeLatest(env.INPUT_MAKE_LATEST),
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const parseMakeLatest = (
 | 
			
		||||
  value: string | undefined,
 | 
			
		||||
): "true" | "false" | "legacy" | undefined => {
 | 
			
		||||
  if (value === "true" || value === "false" || value === "legacy") {
 | 
			
		||||
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[], cwd?: string): string[] => {
 | 
			
		||||
  return patterns.reduce((acc: string[], pattern: string): string[] => {
 | 
			
		||||
    return acc.concat(
 | 
			
		||||
      glob.sync(pattern).filter((path) => statSync(path).isFile()),
 | 
			
		||||
    );
 | 
			
		||||
    const matches = glob.sync(pattern, { cwd, dot: true, absolute: false });
 | 
			
		||||
    const resolved = matches
 | 
			
		||||
      .map((p) => (cwd ? pathLib.join(cwd, p) : p))
 | 
			
		||||
      .filter((p) => {
 | 
			
		||||
        try {
 | 
			
		||||
          return statSync(p).isFile();
 | 
			
		||||
        } catch {
 | 
			
		||||
          return false;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    return acc.concat(resolved);
 | 
			
		||||
  }, []);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const unmatchedPatterns = (patterns: string[]): string[] => {
 | 
			
		||||
export const unmatchedPatterns = (patterns: string[], cwd?: string): string[] => {
 | 
			
		||||
  return patterns.reduce((acc: string[], pattern: string): string[] => {
 | 
			
		||||
    return acc.concat(
 | 
			
		||||
      glob.sync(pattern).filter((path) => statSync(path).isFile()).length == 0
 | 
			
		||||
        ? [pattern]
 | 
			
		||||
        : [],
 | 
			
		||||
    );
 | 
			
		||||
    const matches = glob.sync(pattern, { cwd, dot: true, absolute: false });
 | 
			
		||||
    const files = matches.filter((p) => {
 | 
			
		||||
      try {
 | 
			
		||||
        const full = cwd ? pathLib.join(cwd, p) : p;
 | 
			
		||||
        return statSync(full).isFile();
 | 
			
		||||
      } catch {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    return acc.concat(files.length == 0 ? [pattern] : []);
 | 
			
		||||
  }, []);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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, ".");
 | 
			
		||||
  return assetName.replace(/ /g, '.');
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,8 @@
 | 
			
		||||
    "useUnknownInCatchVariables": false,
 | 
			
		||||
    /* Basic Options */
 | 
			
		||||
    // "incremental": true,                   /* Enable incremental compilation */
 | 
			
		||||
    "target": "es6",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
 | 
			
		||||
    "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
 | 
			
		||||
    "target": "es2022",
 | 
			
		||||
    "module": "NodeNext",
 | 
			
		||||
    // "allowJs": true,                       /* Allow javascript files to be compiled. */
 | 
			
		||||
    // "checkJs": true,                       /* Report errors in .js files. */
 | 
			
		||||
    // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
 | 
			
		||||
@@ -25,6 +25,7 @@
 | 
			
		||||
    /* Strict Type-Checking Options */
 | 
			
		||||
    "strict": true,                           /* Enable all strict type-checking options. */
 | 
			
		||||
    "noImplicitAny": false,                 /* Raise error on expressions and declarations with an implied 'any' type. */
 | 
			
		||||
    "skipLibCheck": true,
 | 
			
		||||
    // "strictNullChecks": true,              /* Enable strict null checks. */
 | 
			
		||||
    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
 | 
			
		||||
    // "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
 | 
			
		||||
@@ -44,7 +45,7 @@
 | 
			
		||||
    // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
 | 
			
		||||
    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
 | 
			
		||||
    // "typeRoots": [],                       /* List of folders to include type definitions from. */
 | 
			
		||||
    // "types": [],                           /* Type declaration files to be included in compilation. */
 | 
			
		||||
    "types": ["vitest/globals"],
 | 
			
		||||
    // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
 | 
			
		||||
    "esModuleInterop": true                   /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
 | 
			
		||||
    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */
 | 
			
		||||
@@ -60,5 +61,5 @@
 | 
			
		||||
    // "experimentalDecorators": true,        /* Enables experimental support for ES7 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", "vitest.config.ts"]
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								vitest.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								vitest.config.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
import { defineConfig } from 'vitest/config';
 | 
			
		||||
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  test: {
 | 
			
		||||
    environment: 'node',
 | 
			
		||||
    coverage: {
 | 
			
		||||
      reporter: ['text', 'lcov'],
 | 
			
		||||
    },
 | 
			
		||||
    include: ['__tests__/**/*.ts'],
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
		Reference in New Issue
	
	Block a user