fix: expand tilde file paths (#756)

Signed-off-by: Rui Chen <rui@chenrui.dev>
This commit is contained in:
Rui Chen
2026-03-15 00:09:15 -04:00
committed by GitHub
parent 45211baa90
commit 37f7a20824
5 changed files with 82 additions and 31 deletions

View File

@@ -186,7 +186,7 @@ The following are optional as `step.with` keys
| `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 | 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. | | `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. Escape glob metacharacters when you need to match a literal filename that contains them, such as `[` or `]`. On Windows, both `\` and `/` separators are accepted. | | `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. |
| `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`. `refs/tags/<name>` values are normalized to `<name>`. | | `tag_name` | String | Name of a tag. defaults to `github.ref_name`. `refs/tags/<name>` values are normalized to `<name>`. |

View File

@@ -1,6 +1,8 @@
import { import {
alignAssetName, alignAssetName,
expandHomePattern,
isTag, isTag,
normalizeFilePattern,
normalizeGlobPattern, normalizeGlobPattern,
normalizeTagName, normalizeTagName,
parseConfig, parseConfig,
@@ -541,6 +543,36 @@ describe('util', () => {
}); });
}); });
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');

View File

@@ -25,7 +25,7 @@ inputs:
description: "Upload artifacts sequentially in the provided order. This does not control the final display order GitHub uses for release assets." 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. Escape glob metacharacters when matching literal filenames that contain them. On Windows, both \\ and / path separators are accepted." 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."
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)"

50
dist/index.js vendored

File diff suppressed because one or more lines are too long

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 { homedir } from 'os';
import * as pathLib from 'path'; import * as pathLib from 'path';
export interface Config { export interface Config {
@@ -135,11 +136,29 @@ export const normalizeGlobPattern = (
return pattern; 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(normalizeGlobPattern(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();
@@ -153,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(normalizeGlobPattern(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;