Skip to content

Commit 88d0deb

Browse files
authored
Merge pull request #108 from Araxeus/add-releaseRegex-config-option
2 parents 3b6bcbd + 86ddadc commit 88d0deb

File tree

5 files changed

+141
-21
lines changed

5 files changed

+141
-21
lines changed

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Whether you're a web developer streamlining asset management or a power user aut
2424
- [Configuration](#configuration)
2525
- [Versioning Dependencies](#versioning-dependencies)
2626
- [GitHub Releases](#github-releases)
27+
- [Filtering releases with releaseRegex](#filtering-releases-with-releaseregex)
2728
- [Default Configuration](#default-configuration)
2829
- [Commands](#commands)
2930
- [Sync](#sync)
@@ -216,6 +217,52 @@ To extract files from a compressed release archive, you can define an object tha
216217
}
217218
```
218219

220+
### Filtering releases with releaseRegex
221+
222+
Vendorfiles can now filter release tags/titles when searching for the "latest" release using the `releaseRegex` option. This should be a JavaScript-style regular expression string (without surrounding `/` delimiters). The regex is tested against the release tag/name (and can be used to match release titles where appropriate) to restrict which releases are considered the “latest”.
223+
224+
Use-cases:
225+
226+
- Only consider semver tags: `"^v\\d+\\.\\d+\\.\\d+$"`
227+
- Ignore pre-releases with `-alpha`/`-beta`: `"^v(?!.*-(?:alpha|beta)).*"`
228+
- Prefer releases whose title contains `stable`: `"stable"`
229+
230+
Examples:
231+
232+
JSON example (per-dependency):
233+
234+
```json
235+
{
236+
"vendorDependencies": {
237+
"fzf": {
238+
"version": "0.38.0",
239+
"repository": "https://github.com/junegunn/fzf",
240+
"releaseRegex": "^v\\d+\\.\\d+\\.\\d+$",
241+
"files": ["{release}/fzf-{version}-linux_amd64.tar.gz"]
242+
}
243+
}
244+
}
245+
```
246+
247+
YAML example (global default and per-dep override):
248+
249+
```yaml
250+
vendorConfig:
251+
vendorFolder: .
252+
default:
253+
releaseRegex: "^v\\d+\\.\\d+\\.\\d+$"
254+
vendorDependencies:
255+
fzf:
256+
repository: https://github.com/junegunn/fzf
257+
files:
258+
- "{release}/fzf-{version}-linux_amd64.tar.gz"
259+
```
260+
261+
Notes:
262+
263+
- If omitted, Vendorfiles considers all releases/tags when determining the latest release.
264+
- Use double escaping (e.g. `\\d`) in JSON strings.
265+
219266
## Default Configuration
220267

221268
For shared options across dependencies, use a `default` object at the same level as `vendorConfig` and `vendorDependencies`. Here's an example:

lib/commands.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,6 @@ export async function install({
258258
typeof file === 'object' ? Object.entries(file) : file,
259259
);
260260

261-
const ref = newVersion; // TODO delete this after typescript bug is fixed (newVersion is not a string)
262-
263261
type ReleaseFileOutput = string | { [input: string]: string };
264262

265263
const releaseFiles: { input: string; output: ReleaseFileOutput }[] = [];
@@ -296,7 +294,7 @@ export async function install({
296294
.getFile({
297295
repo,
298296
path: input,
299-
ref,
297+
ref: newVersion,
300298
})
301299
.catch(err => {
302300
if (err?.status === 404) {
@@ -309,7 +307,7 @@ export async function install({
309307
typeof file === 'string' ? file : file[0]
310308
}" from ${
311309
dependency.repository
312-
} with version ${ref}`,
310+
} with version ${newVersion}`,
313311
);
314312
}
315313
});
@@ -323,7 +321,7 @@ export async function install({
323321
await Promise.all(
324322
// file.output is either a string that or an object which would mean that we want to extract the files from the downloaded archive
325323
releaseFiles.map(async file => {
326-
const input = replaceVersion(file.input, ref).replace(
324+
const input = replaceVersion(file.input, newVersion).replace(
327325
'{release}/',
328326
'',
329327
);
@@ -332,7 +330,8 @@ export async function install({
332330
const releaseFile = await github.downloadReleaseFile({
333331
repo,
334332
path: input,
335-
version: ref,
333+
version: newVersion,
334+
releaseRegex: dependency.releaseRegex,
336335
});
337336

338337
if (typeof output === 'object') {
@@ -372,7 +371,7 @@ export async function install({
372371
inputPath = path.join(randomFolderName, inputPath);
373372
outputPath = path.join(
374373
depDirectory,
375-
replaceVersion(outputPath, ref),
374+
replaceVersion(outputPath, newVersion),
376375
);
377376
try {
378377
await fs.access(inputPath);
@@ -397,7 +396,7 @@ export async function install({
397396
} else {
398397
await readableToFile(
399398
releaseFile,
400-
path.join(depDirectory, replaceVersion(output, ref)),
399+
path.join(depDirectory, replaceVersion(output, newVersion)),
401400
);
402401
}
403402
}),

lib/github.ts

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createOAuthDeviceAuth } from '@octokit/auth-oauth-device';
2-
import { Octokit } from '@octokit/rest';
2+
import { Octokit, type RestEndpointMethodTypes } from '@octokit/rest';
33
import * as dotenv from 'dotenv';
44
import getEnvPaths from 'env-paths';
55
import _fetch, { type FetchOptions } from 'make-fetch-happen';
@@ -34,16 +34,23 @@ const octokit = () => {
3434
return _octokit;
3535
};
3636

37-
const releases = new Map();
37+
const releases = new Map<
38+
string,
39+
RestEndpointMethodTypes['repos']['getReleaseByTag']['response']['data']
40+
>();
3841
async function getReleaseFromTag({
3942
owner,
4043
name,
4144
tag,
4245
}: Repository & { tag: string }) {
43-
const key = `${owner}/${name}/${tag}`;
44-
if (releases.has(key)) {
45-
return releases.get(key);
46+
const repo = `${owner}/${name}`;
47+
const key = `${repo}/${tag}`;
48+
for (const [k, release] of releases) {
49+
if (k === key || (k.startsWith(repo) && release.tag_name === tag)) {
50+
return release;
51+
}
4652
}
53+
4754
const res = await octokit().repos.getReleaseByTag({
4855
owner,
4956
repo: name,
@@ -54,11 +61,18 @@ async function getReleaseFromTag({
5461
return res.data;
5562
}
5663

57-
export async function getLatestRelease({ owner, name: repo }: Repository) {
58-
const key = `${owner}/${repo}/latest`;
59-
if (releases.has(key)) {
60-
return releases.get(key);
64+
export async function getLatestRelease(
65+
{ owner, name: repo }: Repository,
66+
releaseRegex?: string,
67+
) {
68+
if (releaseRegex) {
69+
return getReleaseByRegex({ owner, name: repo }, releaseRegex);
6170
}
71+
const key = `${owner}/${repo}/latest`;
72+
73+
const cached = releases.get(key);
74+
if (cached !== undefined) return cached;
75+
6276
const res = await octokit().repos.getLatestRelease({
6377
owner,
6478
repo,
@@ -68,6 +82,35 @@ export async function getLatestRelease({ owner, name: repo }: Repository) {
6882
return res.data;
6983
}
7084

85+
export async function getReleaseByRegex(
86+
{ owner, name: repo }: Repository,
87+
releaseRegex: string,
88+
) {
89+
const key = `${owner}/${repo}/regex/${releaseRegex}`;
90+
91+
const cached = releases.get(key);
92+
if (cached !== undefined) return cached;
93+
94+
const releasesList = await octokit().repos.listReleases({
95+
owner,
96+
repo,
97+
per_page: 100,
98+
});
99+
100+
const releaseRegexObj = new RegExp(releaseRegex);
101+
const matchedRelease = releasesList.data.find(
102+
release =>
103+
releaseRegexObj.test(release.tag_name) ||
104+
releaseRegexObj.test(release.name || ''),
105+
);
106+
if (matchedRelease) {
107+
releases.set(key, matchedRelease);
108+
return matchedRelease;
109+
}
110+
111+
throw `No releases found matching ${releaseRegex.toString()} in ${owner}/${repo}`;
112+
}
113+
71114
export async function getFileCommitSha({
72115
repo,
73116
path,
@@ -121,18 +164,23 @@ export async function downloadReleaseFile({
121164
repo,
122165
path,
123166
version,
167+
releaseRegex,
124168
}: {
125169
repo: Repository;
126170
path: string;
127171
version: string;
172+
releaseRegex?: string;
128173
}) {
129174
const release = await (version
130175
? getReleaseFromTag({ ...repo, tag: version })
131-
: getLatestRelease(repo));
176+
: getLatestRelease(repo, releaseRegex));
132177

133-
assert(!!release, `Release "${version}" was not found in ${release.url}`);
178+
assert(
179+
!!release,
180+
`Release "${version}" was not found in ${repo.owner}/${repo.name}`,
181+
);
134182

135-
assert(release.assets, `Release assets were not found in ${release.url}`);
183+
assert(!!release.assets, `Release assets were not found in ${release.url}`);
136184

137185
const asset_id = release.assets.find(
138186
(asset: { name: string }) => asset.name === path,

lib/types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export type VendorDependency = {
4848
hashVersionFile?: string | boolean;
4949
name?: string;
5050
vendorFolder?: string;
51+
releaseRegex?: string;
5152
};
5253

5354
// LOCKFILE TYPES

lib/utils.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,10 @@ export async function getNewVersion(
158158
}
159159
} else {
160160
try {
161-
const latestRelease = await github.getLatestRelease(repo);
161+
const latestRelease = await github.getLatestRelease(
162+
repo,
163+
dependency.releaseRegex,
164+
);
162165
newVersion = latestRelease.tag_name as string;
163166
} catch {
164167
if (showOutdatedOnly) {
@@ -323,6 +326,28 @@ export function validateVendorDependency(
323326
Array.isArray(dependency.files) && dependency.files.length > 0,
324327
`config key 'vendorDependencies.${name}.files' is not a valid array`,
325328
);
329+
if (dependency.hashVersionFile) {
330+
assert(
331+
typeof dependency.hashVersionFile === 'string' ||
332+
dependency.hashVersionFile === true,
333+
`config key 'vendorDependencies.${name}.hashVersionFile' must be a string or true`,
334+
);
335+
}
336+
if (dependency.releaseRegex) {
337+
assert(
338+
typeof dependency.releaseRegex === 'string' &&
339+
dependency.releaseRegex.length > 0 &&
340+
(() => {
341+
try {
342+
new RegExp(dependency.releaseRegex);
343+
return true;
344+
} catch {
345+
return false;
346+
}
347+
})(),
348+
`config key 'vendorDependencies.${name}.releaseRegex' must be a valid regex string`,
349+
);
350+
}
326351
}
327352

328353
export function getDependencyFolder({

0 commit comments

Comments
 (0)