Compare commits

..

5 commits

Author SHA1 Message Date
Copilot
f38efdea4c
fix: gracefully fallback to body when body_path cannot be read (#671)
Some checks failed
main / build (push) Has been cancelled
* Initial plan

* fix: gracefully fallback to body when body_path cannot be read

Co-authored-by: chenrui333 <1580956+chenrui333@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: chenrui333 <1580956+chenrui333@users.noreply.github.com>
2025-10-06 23:50:32 -04:00
Copilot
cec1a1113b
fix(util): support brace expansion globs containing commas in parseInputFiles (#672)
* Initial plan

* fix(util): support brace expansion globs containing commas in parseInputFiles

Co-authored-by: chenrui333 <1580956+chenrui333@users.noreply.github.com>

* test(util): add comprehensive edge case coverage for brace expansion parsing

Co-authored-by: chenrui333 <1580956+chenrui333@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: chenrui333 <1580956+chenrui333@users.noreply.github.com>
2025-10-06 23:50:00 -04:00
Rui Chen
aec2ec56f9
release 2.4.0
Signed-off-by: Rui Chen <rui@chenrui.dev>
2025-10-06 22:53:32 -04:00
Stephen Way
4db716b167
feat: respect working_directory for files globs; add input and tests (#667) 2025-10-06 22:51:45 -04:00
dependabot[bot]
14820f2cee
chore(deps): bump the npm group with 2 updates (#668)
Bumps the npm group with 2 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [typescript](https://github.com/microsoft/TypeScript).


Updates `@types/node` from 20.19.18 to 20.19.19
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `typescript` from 5.9.2 to 5.9.3
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.2...v5.9.3)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 20.19.19
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: typescript
  dependency-version: 5.9.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-06 22:48:36 -04:00
8 changed files with 205 additions and 34 deletions

View file

@ -1,3 +1,11 @@
## 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 ## 2.3.4
## What's Changed ## What's Changed

View file

@ -39,6 +39,18 @@ describe('util', () => {
'loom', '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', () => { describe('releaseBody', () => {
it('uses input body', () => { it('uses input body', () => {
@ -110,6 +122,52 @@ 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', () => { describe('parseConfig', () => {
it('parses basic config', () => { it('parses basic config', () => {
@ -128,6 +186,7 @@ describe('util', () => {
github_ref: '', github_ref: '',
github_repository: '', github_repository: '',
github_token: '', github_token: '',
input_working_directory: undefined,
input_append_body: false, input_append_body: false,
input_body: undefined, input_body: undefined,
input_body_path: undefined, input_body_path: undefined,
@ -156,6 +215,7 @@ describe('util', () => {
github_ref: '', github_ref: '',
github_repository: '', github_repository: '',
github_token: '', github_token: '',
input_working_directory: undefined,
input_append_body: false, input_append_body: false,
input_body: undefined, input_body: undefined,
input_body_path: undefined, input_body_path: undefined,
@ -183,6 +243,7 @@ describe('util', () => {
github_ref: '', github_ref: '',
github_repository: '', github_repository: '',
github_token: '', github_token: '',
input_working_directory: undefined,
input_append_body: false, input_append_body: false,
input_body: undefined, input_body: undefined,
input_body_path: undefined, input_body_path: undefined,
@ -211,6 +272,7 @@ describe('util', () => {
github_ref: '', github_ref: '',
github_repository: '', github_repository: '',
github_token: '', github_token: '',
input_working_directory: undefined,
input_append_body: false, input_append_body: false,
input_body: undefined, input_body: undefined,
input_body_path: undefined, input_body_path: undefined,
@ -243,6 +305,7 @@ describe('util', () => {
github_ref: '', github_ref: '',
github_repository: '', github_repository: '',
github_token: 'env-token', github_token: 'env-token',
input_working_directory: undefined,
input_append_body: false, input_append_body: false,
input_body: undefined, input_body: undefined,
input_body_path: undefined, input_body_path: undefined,
@ -272,6 +335,7 @@ describe('util', () => {
github_ref: '', github_ref: '',
github_repository: '', github_repository: '',
github_token: 'input-token', github_token: 'input-token',
input_working_directory: undefined,
input_append_body: false, input_append_body: false,
input_body: undefined, input_body: undefined,
input_body_path: undefined, input_body_path: undefined,
@ -300,6 +364,7 @@ describe('util', () => {
github_ref: '', github_ref: '',
github_repository: '', github_repository: '',
github_token: '', github_token: '',
input_working_directory: undefined,
input_append_body: false, input_append_body: false,
input_body: undefined, input_body: undefined,
input_body_path: undefined, input_body_path: undefined,
@ -327,6 +392,7 @@ describe('util', () => {
github_ref: '', github_ref: '',
github_repository: '', github_repository: '',
github_token: '', github_token: '',
input_working_directory: undefined,
input_append_body: false, input_append_body: false,
input_body: undefined, input_body: undefined,
input_body_path: undefined, input_body_path: undefined,
@ -354,6 +420,7 @@ describe('util', () => {
github_ref: '', github_ref: '',
github_repository: '', github_repository: '',
github_token: '', github_token: '',
input_working_directory: undefined,
input_append_body: true, input_append_body: true,
input_body: undefined, input_body: undefined,
input_body_path: undefined, input_body_path: undefined,
@ -388,6 +455,10 @@ describe('util', () => {
'tests/data/foo/bar.txt', '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', () => {
@ -397,6 +468,12 @@ describe('util', () => {
['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', () => { describe('replaceSpacesWithDots', () => {
@ -413,3 +490,36 @@ describe('util', () => {
}); });
}); });
}); });
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',
]);
});
});

View file

@ -27,6 +27,9 @@ inputs:
files: files:
description: "Newline-delimited list of path globs for asset files to upload" description: "Newline-delimited list of path globs for asset files to upload"
required: false required: false
working_directory:
description: "Base directory to resolve 'files' globs against (defaults to job working-directory)"
required: false
overwrite_files: overwrite_files:
description: "Overwrite existing files with the same name. Defaults to true" description: "Overwrite existing files with the same name. Defaults to true"
required: false required: false

2
dist/index.js vendored

File diff suppressed because one or more lines are too long

20
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "action-gh-release", "name": "action-gh-release",
"version": "2.3.4", "version": "2.4.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "action-gh-release", "name": "action-gh-release",
"version": "2.3.4", "version": "2.4.0",
"dependencies": { "dependencies": {
"@actions/core": "^1.11.1", "@actions/core": "^1.11.1",
"@actions/github": "^6.0.1", "@actions/github": "^6.0.1",
@ -18,12 +18,12 @@
"devDependencies": { "devDependencies": {
"@types/glob": "^9.0.0", "@types/glob": "^9.0.0",
"@types/mime-types": "^3.0.1", "@types/mime-types": "^3.0.1",
"@types/node": "^20.19.18", "@types/node": "^20.19.19",
"@vercel/ncc": "^0.38.4", "@vercel/ncc": "^0.38.4",
"@vitest/coverage-v8": "^3.2.4", "@vitest/coverage-v8": "^3.2.4",
"prettier": "3.6.2", "prettier": "3.6.2",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.9.2", "typescript": "^5.9.3",
"typescript-formatter": "^7.2.2", "typescript-formatter": "^7.2.2",
"vitest": "^3.1.4" "vitest": "^3.1.4"
} }
@ -1482,9 +1482,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.19.18", "version": "20.19.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.18.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.19.tgz",
"integrity": "sha512-KeYVbfnbsBCyKG8e3gmUqAfyZNcoj/qpEbHRkQkfZdKOBrU7QQ+BsTdfqLSWX9/m1ytYreMhpKvp+EZi3UFYAg==", "integrity": "sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -2817,9 +2817,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.9.2", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {

View file

@ -1,6 +1,6 @@
{ {
"name": "action-gh-release", "name": "action-gh-release",
"version": "2.3.4", "version": "2.4.0",
"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",
@ -32,12 +32,12 @@
"devDependencies": { "devDependencies": {
"@types/glob": "^9.0.0", "@types/glob": "^9.0.0",
"@types/mime-types": "^3.0.1", "@types/mime-types": "^3.0.1",
"@types/node": "^20.19.18", "@types/node": "^20.19.19",
"@vercel/ncc": "^0.38.4", "@vercel/ncc": "^0.38.4",
"@vitest/coverage-v8": "^3.2.4", "@vitest/coverage-v8": "^3.2.4",
"prettier": "3.6.2", "prettier": "3.6.2",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.9.2", "typescript": "^5.9.3",
"typescript-formatter": "^7.2.2", "typescript-formatter": "^7.2.2",
"vitest": "^3.1.4" "vitest": "^3.1.4"
} }

View file

@ -12,7 +12,7 @@ async function run() {
throw new Error(`⚠️ GitHub Releases requires a tag`); throw new Error(`⚠️ GitHub Releases requires a tag`);
} }
if (config.input_files) { if (config.input_files) {
const patterns = unmatchedPatterns(config.input_files); const patterns = unmatchedPatterns(config.input_files, config.input_working_directory);
patterns.forEach((pattern) => { patterns.forEach((pattern) => {
if (config.input_fail_on_unmatched_files) { if (config.input_fail_on_unmatched_files) {
throw new Error(`⚠️ Pattern '${pattern}' does not match any files.`); throw new Error(`⚠️ Pattern '${pattern}' does not match any files.`);
@ -50,7 +50,7 @@ async function run() {
//); //);
const rel = await release(config, new GitHubReleaser(gh)); const rel = await release(config, new GitHubReleaser(gh));
if (config.input_files && config.input_files.length > 0) { if (config.input_files && config.input_files.length > 0) {
const files = paths(config.input_files); const files = paths(config.input_files, config.input_working_directory);
if (files.length == 0) { if (files.length == 0) {
if (config.input_fail_on_unmatched_files) { 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.`);

View file

@ -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 * as pathLib from 'path';
export interface Config { export interface Config {
github_token: string; github_token: string;
@ -12,6 +13,7 @@ export interface Config {
input_body?: string; input_body?: string;
input_body_path?: string; input_body_path?: string;
input_files?: string[]; input_files?: string[];
input_working_directory?: string;
input_overwrite_files?: boolean; input_overwrite_files?: boolean;
input_draft?: boolean; input_draft?: boolean;
input_preserve_order?: boolean; input_preserve_order?: boolean;
@ -33,23 +35,53 @@ export const uploadUrl = (url: string): string => {
}; };
export const releaseBody = (config: Config): string | undefined => { export const releaseBody = (config: Config): string | undefined => {
return ( if (config.input_body_path) {
(config.input_body_path && readFileSync(config.input_body_path).toString('utf8')) || try {
config.input_body 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 }; 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[] => { export const parseInputFiles = (files: string): string[] => {
return files.split(/\r?\n/).reduce<string[]>( return files
(acc, line) => .split(/\r?\n/)
acc .flatMap((line) => smartSplit(line))
.concat(line.split(',')) .filter((pat) => pat.trim() !== '');
.filter((pat) => pat)
.map((pat) => pat.trim()),
[],
);
}; };
export const parseConfig = (env: Env): Config => { export const parseConfig = (env: Env): Config => {
@ -62,6 +94,7 @@ export const parseConfig = (env: Env): Config => {
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 || ''),
input_working_directory: env.INPUT_WORKING_DIRECTORY || undefined,
input_overwrite_files: env.INPUT_OVERWRITE_FILES input_overwrite_files: env.INPUT_OVERWRITE_FILES
? env.INPUT_OVERWRITE_FILES == 'true' ? env.INPUT_OVERWRITE_FILES == 'true'
: undefined, : undefined,
@ -84,17 +117,34 @@ const parseMakeLatest = (value: string | undefined): 'true' | 'false' | 'legacy'
return undefined; 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 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 patterns.reduce((acc: string[], pattern: string): string[] => {
return acc.concat( const matches = glob.sync(pattern, { cwd, dot: true, absolute: false });
glob.sync(pattern).filter((path) => statSync(path).isFile()).length == 0 ? [pattern] : [], 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] : []);
}, []); }, []);
}; };