Refactored asset matching code.
This commit is contained in:
1985
dist/cli.js
vendored
1985
dist/cli.js
vendored
File diff suppressed because it is too large
Load Diff
47
package-lock.json
generated
47
package-lock.json
generated
@@ -1,16 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "setup-github-release",
|
"name": "install-github-release",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "setup-github-release",
|
"name": "install-github-release",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.11.0",
|
"@actions/core": "^1.11.0",
|
||||||
"@actions/tool-cache": "^2.0.2"
|
"@actions/tool-cache": "^2.0.2",
|
||||||
|
"minimatch": "^10.2.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"check-github-token": "dist/check-token.js",
|
||||||
|
"install-github-release": "dist/cli.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^25.0.0",
|
"@types/node": "^25.0.0",
|
||||||
@@ -530,6 +535,27 @@
|
|||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/balanced-match": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/brace-expansion": {
|
||||||
|
"version": "5.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||||
|
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.27.2",
|
"version": "0.27.2",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
|
||||||
@@ -572,6 +598,21 @@
|
|||||||
"@esbuild/win32-x64": "0.27.2"
|
"@esbuild/win32-x64": "0.27.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minimatch": {
|
||||||
|
"version": "10.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||||
|
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^5.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.11.0",
|
"@actions/core": "^1.11.0",
|
||||||
"@actions/tool-cache": "^2.0.2"
|
"@actions/tool-cache": "^2.0.2",
|
||||||
|
"minimatch": "^10.2.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^25.0.0",
|
"@types/node": "^25.0.0",
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ Options:
|
|||||||
-a, --app-name <name> Application name (optional, for output messages)
|
-a, --app-name <name> Application name (optional, for output messages)
|
||||||
-f, --file-name <name> Asset file name or regex pattern (prefixed with ~)
|
-f, --file-name <name> Asset file name or regex pattern (prefixed with ~)
|
||||||
-b, --binary-name <name> Binary name (supports source:destination form)
|
-b, --binary-name <name> Binary name (supports source:destination form)
|
||||||
-t, --file-type <type> archive|package|zip|gzip|gz|tar|tar.gz|tgz|deb|pkg|rpm
|
-t, --file-type <type> Known: archive|package|linux|macos|targz
|
||||||
|
Or custom: ~<regex> (end-of-string match) or extension (e.g. zip, .tar.gz)
|
||||||
-p, --install-path <path> Custom installation directory
|
-p, --install-path <path> Custom installation directory
|
||||||
-o, --output-directory <path>
|
-o, --output-directory <path>
|
||||||
Only download selected asset to the specified directory
|
Only download selected asset to the specified directory
|
||||||
@@ -118,9 +119,8 @@ function parseCliArgs(argv: string[]): CliOptions {
|
|||||||
break;
|
break;
|
||||||
case '-t':
|
case '-t':
|
||||||
case '--file-type': {
|
case '--file-type': {
|
||||||
const fileType = ensureOptionValue(argv, i, arg).toLowerCase();
|
const fileType = ensureOptionValue(argv, i, arg);
|
||||||
const knownType = /^(archive|package|zip|gzip|gz|tar|tar\.gz|tgz|deb|pkg|rpm)$/i;
|
if (!fileType.trim()) {
|
||||||
if (!knownType.test(fileType)) {
|
|
||||||
throw new Error(`Unknown asset type: ${fileType}`);
|
throw new Error(`Unknown asset type: ${fileType}`);
|
||||||
}
|
}
|
||||||
opts.fileType = fileType;
|
opts.fileType = fileType;
|
||||||
|
|||||||
@@ -1,116 +1,76 @@
|
|||||||
import { PlatformInfo } from './platform';
|
import { PlatformInfo } from './platform';
|
||||||
|
import { minimatch } from 'minimatch';
|
||||||
|
|
||||||
function normalizeCustomExtensionPattern(fileType: string): string {
|
type ReleaseAsset = { name: string; browser_download_url: string };
|
||||||
let pattern = fileType;
|
|
||||||
|
|
||||||
if (!pattern.endsWith('$')) {
|
const knownFileTypes: Record<string, string> = {
|
||||||
pattern += '$';
|
archive: '*.{zip,tar.gz,tgz}',
|
||||||
}
|
package: '*.{deb,pkg,rpm}',
|
||||||
|
linux: '*.{deb,rpm}',
|
||||||
|
macos: '*.pkg',
|
||||||
|
targz: '*.{tgz,tar.gz}',
|
||||||
|
};
|
||||||
|
|
||||||
if (!pattern.startsWith('\\.')) {
|
function filterByRegex(assets: ReleaseAsset[], pattern: string): ReleaseAsset[] {
|
||||||
pattern = `\\.${pattern}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pattern;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getExtPattern(fileType: string | undefined, system: string): string {
|
|
||||||
const normalizedType = (fileType || '').toLowerCase();
|
|
||||||
|
|
||||||
if (!normalizedType) {
|
|
||||||
if (system === 'linux') {
|
|
||||||
return '\\.(deb|rpm|zip|tar\\.gz|tgz)$';
|
|
||||||
}
|
|
||||||
if (system === 'darwin' || system === 'macos' || system === 'mac' || system === 'osx') {
|
|
||||||
return '\\.(pkg|zip|tar\\.gz|tgz)$';
|
|
||||||
}
|
|
||||||
return '\\.(zip|tar\\.gz|tgz)$';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (normalizedType === 'archive') {
|
|
||||||
return '\\.(zip|tar\\.gz|tgz)$';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (normalizedType === 'package') {
|
|
||||||
return '\\.(deb|pkg|rpm)$';
|
|
||||||
}
|
|
||||||
|
|
||||||
const shorthandTypePatterns: Record<string, string> = {
|
|
||||||
zip: '\\.(zip)$',
|
|
||||||
gzip: '\\.(tar\\.gz|tgz)$',
|
|
||||||
gz: '\\.(tar\\.gz|tgz)$',
|
|
||||||
tar: '\\.(tar)$',
|
|
||||||
'tar.gz': '\\.(tar\\.gz)$',
|
|
||||||
tgz: '\\.(tgz)$',
|
|
||||||
deb: '\\.(deb)$',
|
|
||||||
pkg: '\\.(pkg)$',
|
|
||||||
rpm: '\\.(rpm)$'
|
|
||||||
};
|
|
||||||
|
|
||||||
if (shorthandTypePatterns[normalizedType]) {
|
|
||||||
return shorthandTypePatterns[normalizedType];
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalizeCustomExtensionPattern(fileType || '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function matchFilenameString(re: string, pi: PlatformInfo, extRe: string): string {
|
|
||||||
const hasSystem = re.includes('{{SYSTEM}}');
|
|
||||||
const hasArch = re.includes('{{ARCH}}');
|
|
||||||
const hasExt = re.includes('{{EXT_PATTERN}}');
|
|
||||||
const hasEnd = re.endsWith('$');
|
|
||||||
|
|
||||||
const finalRe = (!hasSystem && !hasArch && !hasExt && !hasEnd)
|
|
||||||
? `${re}.*{{SYSTEM}}[_-]{{ARCH}}.*{{EXT_PATTERN}}$`
|
|
||||||
: (hasSystem && hasArch && !hasExt && !hasEnd)
|
|
||||||
? `${re}.*{{EXT_PATTERN}}$`
|
|
||||||
: re;
|
|
||||||
|
|
||||||
return finalRe
|
|
||||||
.replace(/{{SYSTEM}}/g, pi.systemPattern)
|
|
||||||
.replace(/{{ARCH}}/g, pi.archPattern)
|
|
||||||
.replace(/{{EXT_PATTERN}}/g, extRe);
|
|
||||||
}
|
|
||||||
|
|
||||||
function matchSingleAssetByRegex(assets: any[], pattern: string, noMatchError: string, multipleMatchErrorPrefix: string): any {
|
|
||||||
const regex = new RegExp(pattern, 'i');
|
const regex = new RegExp(pattern, 'i');
|
||||||
const matchingAssets = assets.filter((a: any) => regex.test(a.name));
|
return assets.filter((asset) => regex.test(asset.name));
|
||||||
if (matchingAssets.length === 0) {
|
|
||||||
throw new Error(noMatchError);
|
|
||||||
}
|
|
||||||
if (matchingAssets.length > 1) {
|
|
||||||
throw new Error(`${multipleMatchErrorPrefix}: ${matchingAssets.map((a: any) => a.name).join(', ')}`);
|
|
||||||
}
|
|
||||||
return matchingAssets[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMatchingAsset(assets: any[], platform: PlatformInfo, fileName?: string, fileType?: string): any {
|
function replacePlatformPlaceholders(pattern: string, platform: PlatformInfo): string {
|
||||||
const extPattern = getExtPattern(fileType, platform.system);
|
return pattern
|
||||||
|
.replace(/{{SYSTEM}}/g, platform.systemPattern)
|
||||||
|
.replace(/{{ARCH}}/g, platform.archPattern);
|
||||||
|
}
|
||||||
|
|
||||||
if (!fileName || fileName.startsWith('~')) {
|
export function getMatchingAsset(assets: ReleaseAsset[], platform: PlatformInfo, fileName?: string, fileType?: string): ReleaseAsset {
|
||||||
// Rule 1 + Rule 3: Regex-based matching rules
|
// Filename provided as literal string (no ~): exact match.
|
||||||
const pattern = !fileName
|
if (fileName && !fileName.startsWith('~')) {
|
||||||
? `${platform.systemPattern}[_-]${platform.archPattern}.*${extPattern}`
|
const exactMatches = assets.filter((asset) => asset.name === fileName);
|
||||||
: matchFilenameString(fileName.substring(1), platform, extPattern);
|
if (exactMatches.length !== 1) {
|
||||||
const noMatchError = !fileName
|
throw new Error(`Expected exactly one asset to match the provided filename, matched: ${exactMatches.length}`);
|
||||||
? `No assets matched the default criteria: ${pattern}`
|
|
||||||
: `No assets matched the regex: ${pattern}`;
|
|
||||||
const multipleMatchErrorPrefix = !fileName
|
|
||||||
? 'Multiple assets matched the default criteria'
|
|
||||||
: 'Multiple assets matched the criteria';
|
|
||||||
|
|
||||||
return matchSingleAssetByRegex(
|
|
||||||
assets,
|
|
||||||
pattern,
|
|
||||||
noMatchError,
|
|
||||||
multipleMatchErrorPrefix
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Rule 2: Literal matching rule
|
|
||||||
const asset = assets.find((a: any) => a.name === fileName);
|
|
||||||
if (!asset) {
|
|
||||||
throw new Error(`No asset found matching the exact name: ${fileName}`);
|
|
||||||
}
|
}
|
||||||
return asset;
|
return exactMatches[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filetype filtering stage (or passthrough when not provided).
|
||||||
|
let fileTypeFilteredAssets: ReleaseAsset[] = assets;
|
||||||
|
if (fileType) {
|
||||||
|
if (Object.hasOwn(knownFileTypes, fileType)) {
|
||||||
|
// 2. Known fileType key: use predefined glob.
|
||||||
|
const fileTypeGlob = knownFileTypes[fileType];
|
||||||
|
fileTypeFilteredAssets = assets.filter((asset) => minimatch(asset.name, fileTypeGlob, { nocase: true }));
|
||||||
|
} else if (fileType.startsWith('~')) {
|
||||||
|
// 3. Custom regex fileType: match regex at end of string.
|
||||||
|
const fileTypeRegex = `${fileType.substring(1)}$`;
|
||||||
|
fileTypeFilteredAssets = filterByRegex(assets, fileTypeRegex);
|
||||||
|
} else {
|
||||||
|
// 4. Custom extension fileType: treat as plain extension glob.
|
||||||
|
const extension = fileType.replace(/^\./, '');
|
||||||
|
const fileTypeGlob = `*.${extension}`;
|
||||||
|
fileTypeFilteredAssets = assets.filter((asset) => minimatch(asset.name, fileTypeGlob, { nocase: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Filename provided with ~: platform placeholder expansion and regex filtering.
|
||||||
|
if (fileName && fileName.startsWith('~')) {
|
||||||
|
const fileNamePattern = replacePlatformPlaceholders(fileName.substring(1), platform);
|
||||||
|
const fileNameFilteredAssets = filterByRegex(fileTypeFilteredAssets, fileNamePattern);
|
||||||
|
if (fileNameFilteredAssets.length !== 1) {
|
||||||
|
throw new Error(`Expected exactly one asset to match the filename regex, matched: ${fileNameFilteredAssets.length}`);
|
||||||
|
}
|
||||||
|
return fileNameFilteredAssets[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. No filename: use default {{SYSTEM}}-{{ARCH}} regex.
|
||||||
|
const defaultPattern = replacePlatformPlaceholders('{{SYSTEM}}[_-]{{ARCH}}', platform);
|
||||||
|
const defaultFilteredAssets = filterByRegex(fileTypeFilteredAssets, defaultPattern);
|
||||||
|
|
||||||
|
// 6. Zero or multiple matches are errors.
|
||||||
|
if (defaultFilteredAssets.length !== 1) {
|
||||||
|
const errorMessage = defaultFilteredAssets.length === 0
|
||||||
|
? `No assets matched the default criteria: ${defaultPattern}`
|
||||||
|
: `Multiple assets matched the default criteria: ${defaultFilteredAssets.map((asset) => asset.name).join(', ')}`;
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
return defaultFilteredAssets[0];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user