mirror of
https://github.com/softprops/action-gh-release.git
synced 2026-03-17 02:28:55 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1853d73993 | ||
|
|
e8dbf3cc4a | ||
|
|
37f7a20824 | ||
|
|
45211baa90 | ||
|
|
21ae1a1eb2 | ||
|
|
26c9a934b1 | ||
|
|
abb4370aef | ||
|
|
ff689a6881 | ||
|
|
0a28836784 | ||
|
|
bafaa2d7ac | ||
|
|
b36466e122 |
25
CHANGELOG.md
25
CHANGELOG.md
@@ -1,3 +1,28 @@
|
|||||||
|
## 2.5.3
|
||||||
|
|
||||||
|
`2.5.3` is a patch release focused on the remaining path-handling and release-selection bugs uncovered after `2.5.2`.
|
||||||
|
It fixes `#639`, `#571`, `#280`, `#614`, `#311`, `#403`, and `#368`.
|
||||||
|
It also adds documentation clarifications for `#541`, `#645`, `#542`, `#393`, and `#411`,
|
||||||
|
where the current behavior is either usage-sensitive or constrained by GitHub platform limits rather than an action-side runtime bug.
|
||||||
|
|
||||||
|
If you still hit an issue after upgrading, please open a report with the bug template and include a minimal repro or sanitized workflow snippet where possible.
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
|
||||||
|
### Bug fixes 🐛
|
||||||
|
|
||||||
|
* fix: prefer token input over GITHUB_TOKEN by @chenrui333 in https://github.com/softprops/action-gh-release/pull/751
|
||||||
|
* fix: clean up duplicate drafts after canonicalization by @chenrui333 in https://github.com/softprops/action-gh-release/pull/753
|
||||||
|
* fix: support Windows-style file globs by @chenrui333 in https://github.com/softprops/action-gh-release/pull/754
|
||||||
|
* fix: normalize refs-tag inputs by @chenrui333 in https://github.com/softprops/action-gh-release/pull/755
|
||||||
|
* fix: expand tilde file paths by @chenrui333 in https://github.com/softprops/action-gh-release/pull/756
|
||||||
|
|
||||||
|
### Other Changes 🔄
|
||||||
|
|
||||||
|
* docs: clarify token precedence by @chenrui333 in https://github.com/softprops/action-gh-release/pull/752
|
||||||
|
* docs: clarify GitHub release limits by @chenrui333 in https://github.com/softprops/action-gh-release/pull/758
|
||||||
|
* documentation clarifications for empty-token handling, `preserve_order`, and special-character asset filename behavior
|
||||||
|
|
||||||
## 2.5.2
|
## 2.5.2
|
||||||
|
|
||||||
`2.5.2` is a patch release focused on the remaining release-creation and prerelease regressions in the `2.5.x` bug-fix cycle.
|
`2.5.2` is a patch release focused on the remaining release-creation and prerelease regressions in the `2.5.x` bug-fix cycle.
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -139,7 +139,7 @@ jobs:
|
|||||||
|
|
||||||
> **⚠️ Note:** Notice the `|` in the yaml syntax above ☝️. That lets you effectively declare a multi-line yaml string. You can learn more about multi-line yaml syntax [here](https://yaml-multiline.info)
|
> **⚠️ Note:** Notice the `|` in the yaml syntax above ☝️. That lets you effectively declare a multi-line yaml string. You can learn more about multi-line yaml syntax [here](https://yaml-multiline.info)
|
||||||
|
|
||||||
> **⚠️ Note for Windows:** Paths must use `/` as a separator, not `\`, as `\` is used to escape characters with special meaning in the pattern; for example, instead of specifying `D:\Foo.txt`, you must specify `D:/Foo.txt`. If you're using PowerShell, you can do this with `$Path = $Path -replace '\\','/'`
|
> **⚠️ Note for Windows:** Both `\` and `/` path separators are accepted in `files` globs. If you need to match a literal glob metacharacter such as `[` or `]`, keep escaping the metacharacter itself in the pattern.
|
||||||
|
|
||||||
### 📝 External release notes
|
### 📝 External release notes
|
||||||
|
|
||||||
@@ -167,7 +167,9 @@ jobs:
|
|||||||
body_path: ${{ github.workspace }}-CHANGELOG.txt
|
body_path: ${{ github.workspace }}-CHANGELOG.txt
|
||||||
repository: my_gh_org/my_gh_repo
|
repository: my_gh_org/my_gh_repo
|
||||||
# note you'll typically need to create a personal access token
|
# note you'll typically need to create a personal access token
|
||||||
# with permissions to create releases in the other repo
|
# with permissions to create releases in the other repo.
|
||||||
|
# A non-empty explicit token overrides GITHUB_TOKEN.
|
||||||
|
# Omit the input to use github.token; passing "" treats the token as unset.
|
||||||
token: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
|
token: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -183,15 +185,15 @@ The following are optional as `step.with` keys
|
|||||||
| `body_path` | String | Path to load text communicating notable changes in this release |
|
| `body_path` | String | Path to load text communicating notable changes in this release |
|
||||||
| `draft` | Boolean | Indicator of whether or not this release is a draft |
|
| `draft` | Boolean | Indicator of whether or not this release is a draft |
|
||||||
| `prerelease` | Boolean | Indicator of whether or not is a prerelease |
|
| `prerelease` | Boolean | Indicator of whether or not is a prerelease |
|
||||||
| `preserve_order` | Boolean | Indicator of whether order of files should be preserved when uploading assets |
|
| `preserve_order` | Boolean | Upload assets sequentially in the provided order. This controls the action's upload behavior, but it does not control the final asset ordering that GitHub may display on the release page or return from the Releases API. |
|
||||||
| `files` | String | Newline-delimited globs of paths to assets to upload for release |
|
| `files` | String | Newline-delimited globs of paths to assets to upload for release. Escape glob metacharacters when you need to match a literal filename that contains them, such as `[` or `]`. `~/...` expands to the runner home directory. On Windows, both `\` and `/` separators are accepted. GitHub may normalize raw asset filenames that contain special characters; the action restores the asset label when possible, but the final download name remains GitHub-controlled. |
|
||||||
| `overwrite_files` | Boolean | Indicator of whether files should be overwritten when they already exist. Defaults to true |
|
| `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 |
|
| `name` | String | Name of the release. defaults to tag name |
|
||||||
| `tag_name` | String | Name of a tag. defaults to `github.ref_name` |
|
| `tag_name` | String | Name of a tag. defaults to `github.ref_name`. `refs/tags/<name>` values are normalized to `<name>`. |
|
||||||
| `fail_on_unmatched_files` | Boolean | Indicator of whether to fail if any of the `files` globs match nothing |
|
| `fail_on_unmatched_files` | Boolean | Indicator of whether to fail if any of the `files` globs match nothing |
|
||||||
| `repository` | String | Name of a target repository in `<owner>/<repo>` format. Defaults to GITHUB_REPOSITORY env variable |
|
| `repository` | String | Name of a target repository in `<owner>/<repo>` format. Defaults to GITHUB_REPOSITORY env variable |
|
||||||
| `target_commitish` | String | Commitish value that determines where the Git tag is created from. Can be any branch or commit SHA. Defaults to repository default branch. |
|
| `target_commitish` | String | Commitish value that determines where the Git tag is created from. Can be any branch or commit SHA. Defaults to repository default branch. When creating a new tag for an older commit, `github.token` may not have permission to create the ref; use a PAT or another token with sufficient contents permissions if you hit `403 Resource not accessible by integration`. |
|
||||||
| `token` | String | Secret GitHub Personal Access Token. Defaults to `${{ github.token }}` |
|
| `token` | String | Authorized GitHub token or PAT. Defaults to `${{ github.token }}` when omitted. A non-empty explicit token overrides `GITHUB_TOKEN`. Passing `""` treats the token as explicitly unset, so omit the input entirely or use an expression such as `${{ inputs.token || github.token }}` when wrapping this action in a composite action. |
|
||||||
| `discussion_category_name` | String | If specified, a discussion of the specified category is created and linked to the release. The value must be a category that already exists in the repository. For more information, see ["Managing categories for discussions in your repository."](https://docs.github.com/en/discussions/managing-discussions-for-your-community/managing-categories-for-discussions-in-your-repository) |
|
| `discussion_category_name` | String | If specified, a discussion of the specified category is created and linked to the release. The value must be a category that already exists in the repository. For more information, see ["Managing categories for discussions in your repository."](https://docs.github.com/en/discussions/managing-discussions-for-your-community/managing-categories-for-discussions-in-your-repository) |
|
||||||
| `generate_release_notes` | Boolean | Whether to automatically generate the name and body for this release. If name is specified, the specified name will be used; otherwise, a name will be automatically generated. If body is specified, the body will be pre-pended to the automatically generated notes. See the [GitHub docs for this feature](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes) for more information |
|
| `generate_release_notes` | Boolean | Whether to automatically generate the name and body for this release. If name is specified, the specified name will be used; otherwise, a name will be automatically generated. If body is specified, the body will be pre-pended to the automatically generated notes. See the [GitHub docs for this feature](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes) for more information |
|
||||||
| `append_body` | Boolean | Append to existing body instead of overwriting it |
|
| `append_body` | Boolean | Append to existing body instead of overwriting it |
|
||||||
@@ -204,6 +206,14 @@ attempted first, then falling back on `body` if the path can not be read from.
|
|||||||
are not explicitly set and there is already an existing release for the tag, the
|
are not explicitly set and there is already an existing release for the tag, the
|
||||||
release will retain its original info.
|
release will retain its original info.
|
||||||
|
|
||||||
|
💡 `files` is glob-based, so literal filenames that contain glob metacharacters such as
|
||||||
|
`[` or `]` must be escaped in the pattern.
|
||||||
|
|
||||||
|
💡 GitHub may normalize or rewrite uploaded asset filenames that contain special or
|
||||||
|
non-ASCII characters. This action uploads the requested file, but it cannot force the
|
||||||
|
final asset name that GitHub stores or returns from the Releases API. In particular,
|
||||||
|
4-byte Unicode characters such as emoji cannot currently be restored via asset labels.
|
||||||
|
|
||||||
#### outputs
|
#### outputs
|
||||||
|
|
||||||
The following outputs can be accessed via `${{ steps.<step-id>.outputs }}` from this action
|
The following outputs can be accessed via `${{ steps.<step-id>.outputs }}` from this action
|
||||||
|
|||||||
@@ -498,6 +498,58 @@ describe('github', () => {
|
|||||||
assert.equal(result.created, false);
|
assert.equal(result.created, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('normalizes refs/tags-prefixed input_tag_name values before reusing an existing release', async () => {
|
||||||
|
const existingRelease: Release = {
|
||||||
|
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: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateReleaseSpy = vi.fn(async () => ({ data: existingRelease }));
|
||||||
|
const getReleaseByTagSpy = vi.fn(async () => ({ data: existingRelease }));
|
||||||
|
const result = await release(
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
input_tag_name: 'refs/tags/v1.0.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
getReleaseByTag: getReleaseByTagSpy,
|
||||||
|
createRelease: () => Promise.reject('Not implemented'),
|
||||||
|
updateRelease: updateReleaseSpy,
|
||||||
|
finalizeRelease: () => Promise.reject('Not implemented'),
|
||||||
|
allReleases: async function* () {
|
||||||
|
yield { data: [existingRelease] };
|
||||||
|
},
|
||||||
|
listReleaseAssets: () => Promise.reject('Not implemented'),
|
||||||
|
deleteReleaseAsset: () => Promise.reject('Not implemented'),
|
||||||
|
deleteRelease: () => Promise.reject('Not implemented'),
|
||||||
|
updateReleaseAsset: () => Promise.reject('Not implemented'),
|
||||||
|
uploadReleaseAsset: () => Promise.reject('Not implemented'),
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getReleaseByTagSpy).toHaveBeenCalledWith({
|
||||||
|
owner: 'owner',
|
||||||
|
repo: 'repo',
|
||||||
|
tag: 'v1.0.0',
|
||||||
|
});
|
||||||
|
expect(updateReleaseSpy).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
tag_name: 'v1.0.0',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
assert.equal(result.release.id, existingRelease.id);
|
||||||
|
assert.equal(result.created, false);
|
||||||
|
});
|
||||||
|
|
||||||
it('reuses a canonical release after concurrent create success and removes empty duplicates', async () => {
|
it('reuses a canonical release after concurrent create success and removes empty duplicates', async () => {
|
||||||
const canonicalRelease: Release = {
|
const canonicalRelease: Release = {
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -610,6 +662,66 @@ describe('github', () => {
|
|||||||
release_id: duplicateRelease.id,
|
release_id: duplicateRelease.id,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('deletes the just-created duplicate draft even if recent release listing misses it', async () => {
|
||||||
|
const canonicalRelease: Release = {
|
||||||
|
id: 1,
|
||||||
|
upload_url: 'canonical-upload',
|
||||||
|
html_url: 'canonical-html',
|
||||||
|
tag_name: 'v1.0.0',
|
||||||
|
name: 'canonical',
|
||||||
|
body: 'test',
|
||||||
|
target_commitish: 'main',
|
||||||
|
draft: true,
|
||||||
|
prerelease: false,
|
||||||
|
assets: [],
|
||||||
|
};
|
||||||
|
const duplicateRelease: Release = {
|
||||||
|
id: 2,
|
||||||
|
upload_url: 'duplicate-upload',
|
||||||
|
html_url: 'duplicate-html',
|
||||||
|
tag_name: 'v1.0.0',
|
||||||
|
name: 'duplicate',
|
||||||
|
body: 'test',
|
||||||
|
target_commitish: 'main',
|
||||||
|
draft: true,
|
||||||
|
prerelease: false,
|
||||||
|
assets: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
let lookupCount = 0;
|
||||||
|
const deleteReleaseSpy = vi.fn(async () => undefined);
|
||||||
|
const mockReleaser: Releaser = {
|
||||||
|
getReleaseByTag: () => {
|
||||||
|
lookupCount += 1;
|
||||||
|
if (lookupCount === 1) {
|
||||||
|
return Promise.reject({ status: 404 });
|
||||||
|
}
|
||||||
|
return Promise.resolve({ data: canonicalRelease });
|
||||||
|
},
|
||||||
|
createRelease: () => Promise.resolve({ data: duplicateRelease }),
|
||||||
|
updateRelease: () => Promise.reject('Not implemented'),
|
||||||
|
finalizeRelease: () => Promise.reject('Not implemented'),
|
||||||
|
allReleases: async function* () {
|
||||||
|
yield { data: [canonicalRelease] };
|
||||||
|
},
|
||||||
|
listReleaseAssets: () => Promise.reject('Not implemented'),
|
||||||
|
deleteReleaseAsset: () => Promise.reject('Not implemented'),
|
||||||
|
deleteRelease: deleteReleaseSpy,
|
||||||
|
updateReleaseAsset: () => Promise.reject('Not implemented'),
|
||||||
|
uploadReleaseAsset: () => Promise.reject('Not implemented'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await release(config, mockReleaser, 2);
|
||||||
|
|
||||||
|
assert.equal(result.release.id, canonicalRelease.id);
|
||||||
|
assert.equal(result.created, false);
|
||||||
|
expect(deleteReleaseSpy).toHaveBeenCalledWith({
|
||||||
|
owner: 'owner',
|
||||||
|
repo: 'repo',
|
||||||
|
release_id: duplicateRelease.id,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('upload', () => {
|
describe('upload', () => {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
alignAssetName,
|
alignAssetName,
|
||||||
|
expandHomePattern,
|
||||||
isTag,
|
isTag,
|
||||||
|
normalizeFilePattern,
|
||||||
|
normalizeGlobPattern,
|
||||||
|
normalizeTagName,
|
||||||
parseConfig,
|
parseConfig,
|
||||||
parseInputFiles,
|
parseInputFiles,
|
||||||
paths,
|
paths,
|
||||||
@@ -292,7 +296,7 @@ describe('util', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('prefers GITHUB_TOKEN over token input for backwards compatibility', () => {
|
it('prefers token input over GITHUB_TOKEN', () => {
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
parseConfig({
|
parseConfig({
|
||||||
INPUT_DRAFT: 'false',
|
INPUT_DRAFT: 'false',
|
||||||
@@ -304,7 +308,7 @@ describe('util', () => {
|
|||||||
{
|
{
|
||||||
github_ref: '',
|
github_ref: '',
|
||||||
github_repository: '',
|
github_repository: '',
|
||||||
github_token: 'env-token',
|
github_token: 'input-token',
|
||||||
input_working_directory: undefined,
|
input_working_directory: undefined,
|
||||||
input_append_body: false,
|
input_append_body: false,
|
||||||
input_body: undefined,
|
input_body: undefined,
|
||||||
@@ -324,6 +328,35 @@ describe('util', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
it('falls back to GITHUB_TOKEN when token input is empty', () => {
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
parseConfig({
|
||||||
|
GITHUB_TOKEN: 'env-token',
|
||||||
|
INPUT_TOKEN: ' ',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
github_ref: '',
|
||||||
|
github_repository: '',
|
||||||
|
github_token: 'env-token',
|
||||||
|
input_working_directory: undefined,
|
||||||
|
input_append_body: false,
|
||||||
|
input_body: undefined,
|
||||||
|
input_body_path: undefined,
|
||||||
|
input_draft: undefined,
|
||||||
|
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,
|
||||||
|
input_target_commitish: undefined,
|
||||||
|
input_discussion_category_name: undefined,
|
||||||
|
input_generate_release_notes: false,
|
||||||
|
input_make_latest: undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
it('uses input token as the source of GITHUB_TOKEN by default', () => {
|
it('uses input token as the source of GITHUB_TOKEN by default', () => {
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
parseConfig({
|
parseConfig({
|
||||||
@@ -439,6 +472,10 @@ describe('util', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('normalizes refs/tags-prefixed input_tag_name values', () => {
|
||||||
|
expect(parseConfig({ INPUT_TAG_NAME: 'refs/tags/v1.2.3' }).input_tag_name).toBe('v1.2.3');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('isTag', () => {
|
describe('isTag', () => {
|
||||||
it('returns true for tags', async () => {
|
it('returns true for tags', async () => {
|
||||||
@@ -449,6 +486,16 @@ describe('util', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('normalizeTagName', () => {
|
||||||
|
it('strips refs/tags/ from explicit tag names', () => {
|
||||||
|
assert.equal(normalizeTagName('refs/tags/v1.2.3'), 'v1.2.3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves plain tag names unchanged', () => {
|
||||||
|
assert.equal(normalizeTagName('v1.2.3'), 'v1.2.3');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('paths', () => {
|
describe('paths', () => {
|
||||||
it('resolves files given a set of paths', async () => {
|
it('resolves files given a set of paths', async () => {
|
||||||
assert.deepStrictEqual(paths(['tests/data/**/*', 'tests/data/does/not/exist/*']), [
|
assert.deepStrictEqual(paths(['tests/data/**/*', 'tests/data/does/not/exist/*']), [
|
||||||
@@ -476,6 +523,56 @@ describe('util', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('normalizeGlobPattern', () => {
|
||||||
|
it('preserves posix-style patterns on non-windows platforms', () => {
|
||||||
|
assert.equal(normalizeGlobPattern('./dist/**/*.tgz', 'linux'), './dist/**/*.tgz');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes relative windows-style glob patterns', () => {
|
||||||
|
assert.equal(
|
||||||
|
normalizeGlobPattern('.\\release-assets\\rssguard-*win7.exe', 'win32'),
|
||||||
|
'./release-assets/rssguard-*win7.exe',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes absolute windows-style glob patterns', () => {
|
||||||
|
assert.equal(
|
||||||
|
normalizeGlobPattern('D:\\a\\repo\\build\\packages\\*', 'win32'),
|
||||||
|
'D:/a/repo/build/packages/*',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('expandHomePattern', () => {
|
||||||
|
it('expands a bare tilde to the provided home directory', () => {
|
||||||
|
assert.equal(expandHomePattern('~', '/home/runner'), '/home/runner');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('expands posix-style tilde paths', () => {
|
||||||
|
assert.equal(expandHomePattern('~/release.txt', '/home/runner'), '/home/runner/release.txt');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves non-tilde paths unchanged', () => {
|
||||||
|
assert.equal(expandHomePattern('./release.txt', '/home/runner'), './release.txt');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('normalizeFilePattern', () => {
|
||||||
|
it('expands tilde paths before globbing', () => {
|
||||||
|
assert.equal(
|
||||||
|
normalizeFilePattern('~/release-assets/*.tgz', 'linux', '/home/runner'),
|
||||||
|
'/home/runner/release-assets/*.tgz',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('expands tilde paths and normalizes windows separators', () => {
|
||||||
|
assert.equal(
|
||||||
|
normalizeFilePattern('~\\release-assets\\*.zip', 'win32', 'C:\\Users\\runner'),
|
||||||
|
'C:/Users/runner/release-assets/*.zip',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('replaceSpacesWithDots', () => {
|
describe('replaceSpacesWithDots', () => {
|
||||||
it('replaces all spaces with dots', () => {
|
it('replaces all spaces with dots', () => {
|
||||||
expect(alignAssetName('John Doe.bla')).toBe('John.Doe.bla');
|
expect(alignAssetName('John Doe.bla')).toBe('John.Doe.bla');
|
||||||
|
|||||||
10
action.yml
10
action.yml
@@ -13,7 +13,7 @@ inputs:
|
|||||||
description: "Gives the release a custom name. Defaults to tag name"
|
description: "Gives the release a custom name. Defaults to tag name"
|
||||||
required: false
|
required: false
|
||||||
tag_name:
|
tag_name:
|
||||||
description: "Gives a tag name. Defaults to github.ref_name"
|
description: "Gives a tag name. Defaults to github.ref_name. refs/tags/<name> values are normalized to <name>."
|
||||||
required: false
|
required: false
|
||||||
draft:
|
draft:
|
||||||
description: "Creates a draft release. Defaults to false"
|
description: "Creates a draft release. Defaults to false"
|
||||||
@@ -22,10 +22,10 @@ inputs:
|
|||||||
description: "Identify the release as a prerelease. Defaults to false"
|
description: "Identify the release as a prerelease. Defaults to false"
|
||||||
required: false
|
required: false
|
||||||
preserve_order:
|
preserve_order:
|
||||||
description: "Preserver the order of the artifacts when uploading"
|
description: "Upload artifacts sequentially in the provided order. This does not control the final display order GitHub uses for release assets."
|
||||||
required: false
|
required: false
|
||||||
files:
|
files:
|
||||||
description: "Newline-delimited list of path globs for asset files to upload"
|
description: "Newline-delimited list of path globs for asset files to upload. Escape glob metacharacters when matching literal filenames that contain them. `~/...` expands to the runner home directory. On Windows, both \\ and / path separators are accepted. GitHub may normalize raw asset filenames that contain special characters; the action restores the asset label when possible, but the final download name remains GitHub-controlled."
|
||||||
required: false
|
required: false
|
||||||
working_directory:
|
working_directory:
|
||||||
description: "Base directory to resolve 'files' globs against (defaults to job working-directory)"
|
description: "Base directory to resolve 'files' globs against (defaults to job working-directory)"
|
||||||
@@ -41,11 +41,11 @@ inputs:
|
|||||||
description: "Repository to make releases against, in <owner>/<repo> format"
|
description: "Repository to make releases against, in <owner>/<repo> format"
|
||||||
required: false
|
required: false
|
||||||
token:
|
token:
|
||||||
description: "Authorized secret GitHub Personal Access Token. Defaults to github.token"
|
description: "Authorized GitHub token or PAT. Defaults to github.token when omitted. A non-empty explicit token overrides GITHUB_TOKEN. Passing an empty string treats the token as unset."
|
||||||
required: false
|
required: false
|
||||||
default: ${{ github.token }}
|
default: ${{ github.token }}
|
||||||
target_commitish:
|
target_commitish:
|
||||||
description: "Commitish value that determines where the Git tag is created from. Can be any branch or commit SHA."
|
description: "Commitish value that determines where the Git tag is created from. Can be any branch or commit SHA. When creating a new tag for an older commit, `github.token` may not have permission to create the ref; use a PAT or another token with sufficient contents permissions if you hit 403 `Resource not accessible by integration`."
|
||||||
required: false
|
required: false
|
||||||
discussion_category_name:
|
discussion_category_name:
|
||||||
description: "If specified, a discussion of the specified category is created and linked to the release. The value must be a category that already exists in the repository. If there is already a discussion linked to the release, this parameter is ignored."
|
description: "If specified, a discussion of the specified category is created and linked to the release. The value must be a category that already exists in the repository. If there is already a discussion linked to the release, this parameter is ignored."
|
||||||
|
|||||||
50
dist/index.js
vendored
50
dist/index.js
vendored
File diff suppressed because one or more lines are too long
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "action-gh-release",
|
"name": "action-gh-release",
|
||||||
"version": "2.5.2",
|
"version": "2.5.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "action-gh-release",
|
"name": "action-gh-release",
|
||||||
"version": "2.5.2",
|
"version": "2.5.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^3.0.0",
|
"@actions/core": "^3.0.0",
|
||||||
"@actions/github": "^9.0.0",
|
"@actions/github": "^9.0.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "action-gh-release",
|
"name": "action-gh-release",
|
||||||
"version": "2.5.2",
|
"version": "2.5.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "GitHub Action for creating GitHub Releases",
|
"description": "GitHub Action for creating GitHub Releases",
|
||||||
"main": "lib/main.js",
|
"main": "lib/main.js",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { statSync } from 'fs';
|
|||||||
import { open } from 'fs/promises';
|
import { open } from 'fs/promises';
|
||||||
import { lookup } from 'mime-types';
|
import { lookup } from 'mime-types';
|
||||||
import { basename } from 'path';
|
import { basename } from 'path';
|
||||||
import { alignAssetName, Config, isTag, releaseBody } from './util';
|
import { alignAssetName, Config, isTag, normalizeTagName, releaseBody } from './util';
|
||||||
|
|
||||||
type GitHub = InstanceType<typeof GitHub>;
|
type GitHub = InstanceType<typeof GitHub>;
|
||||||
|
|
||||||
@@ -420,7 +420,7 @@ export const release = async (
|
|||||||
|
|
||||||
const [owner, repo] = config.github_repository.split('/');
|
const [owner, repo] = config.github_repository.split('/');
|
||||||
const tag =
|
const tag =
|
||||||
config.input_tag_name ||
|
normalizeTagName(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 discussion_category_name = config.input_discussion_category_name;
|
||||||
@@ -707,9 +707,13 @@ async function cleanupDuplicateDraftReleases(
|
|||||||
repo: string,
|
repo: string,
|
||||||
tag: string,
|
tag: string,
|
||||||
canonicalReleaseId: number,
|
canonicalReleaseId: number,
|
||||||
recentReleases: Release[],
|
releases: Release[],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
for (const duplicate of recentReleases) {
|
const uniqueReleases = Array.from(
|
||||||
|
new Map(releases.map((release) => [release.id, release])).values(),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const duplicate of uniqueReleases) {
|
||||||
if (duplicate.id === canonicalReleaseId || !duplicate.draft || duplicate.assets.length > 0) {
|
if (duplicate.id === canonicalReleaseId || !duplicate.draft || duplicate.assets.length > 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -760,14 +764,10 @@ async function canonicalizeCreatedRelease(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await cleanupDuplicateDraftReleases(
|
await cleanupDuplicateDraftReleases(releaser, owner, repo, tag, canonicalRelease.id, [
|
||||||
releaser,
|
createdRelease,
|
||||||
owner,
|
...recentReleases,
|
||||||
repo,
|
]);
|
||||||
tag,
|
|
||||||
canonicalRelease.id,
|
|
||||||
recentReleases,
|
|
||||||
);
|
|
||||||
return canonicalRelease;
|
return canonicalRelease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
56
src/util.ts
56
src/util.ts
@@ -1,5 +1,6 @@
|
|||||||
import * as glob from 'glob';
|
import * as glob from 'glob';
|
||||||
import { statSync, readFileSync } from 'fs';
|
import { statSync, readFileSync } from 'fs';
|
||||||
|
import { homedir } from 'os';
|
||||||
import * as pathLib from 'path';
|
import * as pathLib from 'path';
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
@@ -84,13 +85,21 @@ export const parseInputFiles = (files: string): string[] => {
|
|||||||
.filter((pat) => pat.trim() !== '');
|
.filter((pat) => pat.trim() !== '');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const parseToken = (env: Env): string => {
|
||||||
|
const inputToken = env.INPUT_TOKEN?.trim();
|
||||||
|
if (inputToken) {
|
||||||
|
return inputToken;
|
||||||
|
}
|
||||||
|
return env.GITHUB_TOKEN?.trim() || '';
|
||||||
|
};
|
||||||
|
|
||||||
export const parseConfig = (env: Env): Config => {
|
export const parseConfig = (env: Env): Config => {
|
||||||
return {
|
return {
|
||||||
github_token: env.GITHUB_TOKEN || env.INPUT_TOKEN || '',
|
github_token: parseToken(env),
|
||||||
github_ref: env.GITHUB_REF || '',
|
github_ref: env.GITHUB_REF || '',
|
||||||
github_repository: env.INPUT_REPOSITORY || env.GITHUB_REPOSITORY || '',
|
github_repository: env.INPUT_REPOSITORY || env.GITHUB_REPOSITORY || '',
|
||||||
input_name: env.INPUT_NAME,
|
input_name: env.INPUT_NAME,
|
||||||
input_tag_name: env.INPUT_TAG_NAME?.trim(),
|
input_tag_name: normalizeTagName(env.INPUT_TAG_NAME?.trim()),
|
||||||
input_body: env.INPUT_BODY,
|
input_body: env.INPUT_BODY,
|
||||||
input_body_path: env.INPUT_BODY_PATH,
|
input_body_path: env.INPUT_BODY_PATH,
|
||||||
input_files: parseInputFiles(env.INPUT_FILES || ''),
|
input_files: parseInputFiles(env.INPUT_FILES || ''),
|
||||||
@@ -117,11 +126,39 @@ const parseMakeLatest = (value: string | undefined): 'true' | 'false' | 'legacy'
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const normalizeGlobPattern = (
|
||||||
|
pattern: string,
|
||||||
|
platform: NodeJS.Platform = process.platform,
|
||||||
|
): string => {
|
||||||
|
if (platform === 'win32') {
|
||||||
|
return pattern.replace(/\\/g, '/');
|
||||||
|
}
|
||||||
|
return pattern;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const expandHomePattern = (pattern: string, homeDirectory: string = homedir()): string => {
|
||||||
|
if (pattern === '~') {
|
||||||
|
return homeDirectory;
|
||||||
|
}
|
||||||
|
if (pattern.startsWith('~/') || pattern.startsWith('~\\')) {
|
||||||
|
return pathLib.join(homeDirectory, pattern.slice(2));
|
||||||
|
}
|
||||||
|
return pattern;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeFilePattern = (
|
||||||
|
pattern: string,
|
||||||
|
platform: NodeJS.Platform = process.platform,
|
||||||
|
homeDirectory: string = homedir(),
|
||||||
|
): string => {
|
||||||
|
return normalizeGlobPattern(expandHomePattern(pattern, homeDirectory), platform);
|
||||||
|
};
|
||||||
|
|
||||||
export const paths = (patterns: string[], cwd?: string): string[] => {
|
export const paths = (patterns: string[], cwd?: string): string[] => {
|
||||||
return patterns.reduce((acc: string[], pattern: string): string[] => {
|
return patterns.reduce((acc: string[], pattern: string): string[] => {
|
||||||
const matches = glob.sync(pattern, { cwd, dot: true, absolute: false });
|
const matches = glob.sync(normalizeFilePattern(pattern), { cwd, dot: true, absolute: false });
|
||||||
const resolved = matches
|
const resolved = matches
|
||||||
.map((p) => (cwd ? pathLib.join(cwd, p) : p))
|
.map((p) => (cwd && !pathLib.isAbsolute(p) ? pathLib.join(cwd, p) : p))
|
||||||
.filter((p) => {
|
.filter((p) => {
|
||||||
try {
|
try {
|
||||||
return statSync(p).isFile();
|
return statSync(p).isFile();
|
||||||
@@ -135,10 +172,10 @@ export const paths = (patterns: string[], cwd?: string): string[] => {
|
|||||||
|
|
||||||
export const unmatchedPatterns = (patterns: string[], cwd?: string): string[] => {
|
export const unmatchedPatterns = (patterns: string[], cwd?: string): string[] => {
|
||||||
return patterns.reduce((acc: string[], pattern: string): string[] => {
|
return patterns.reduce((acc: string[], pattern: string): string[] => {
|
||||||
const matches = glob.sync(pattern, { cwd, dot: true, absolute: false });
|
const matches = glob.sync(normalizeFilePattern(pattern), { cwd, dot: true, absolute: false });
|
||||||
const files = matches.filter((p) => {
|
const files = matches.filter((p) => {
|
||||||
try {
|
try {
|
||||||
const full = cwd ? pathLib.join(cwd, p) : p;
|
const full = cwd && !pathLib.isAbsolute(p) ? pathLib.join(cwd, p) : p;
|
||||||
return statSync(full).isFile();
|
return statSync(full).isFile();
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
@@ -152,6 +189,13 @@ export const isTag = (ref: string): boolean => {
|
|||||||
return ref.startsWith('refs/tags/');
|
return ref.startsWith('refs/tags/');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const normalizeTagName = (tag: string | undefined): string | undefined => {
|
||||||
|
if (!tag) {
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
return isTag(tag) ? tag.replace('refs/tags/', '') : tag;
|
||||||
|
};
|
||||||
|
|
||||||
export const alignAssetName = (assetName: string): string => {
|
export const alignAssetName = (assetName: string): string => {
|
||||||
return assetName.replace(/ /g, '.');
|
return assetName.replace(/ /g, '.');
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user