dslinter 0.0.12 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.0.16
4
+
5
+ [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.0.15...v0.0.16)
6
+
7
+ ## v0.0.15
8
+
9
+ [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.0.12...v0.0.15)
10
+
11
+ ### 🚀 Enhancements
12
+
13
+ - Update release workflow and versioning ([5e28f64](https://github.com/jrmybtlr/DSLinter/commit/5e28f64))
14
+
15
+ ### ❤️ Contributors
16
+
17
+ - Jeremy Butler <jeremy.butler@laravel.com>
18
+
3
19
  ## v0.0.12
4
20
 
5
21
  [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.0.11...v0.0.12)
package/README.md CHANGED
@@ -35,7 +35,7 @@ Environment variables:
35
35
  | `DSLINT_RELEASE_TAG` | Override release tag (default `v` + `dslinter` version from `package.json`). |
36
36
  | `DSLINT_GITHUB_REPO` | Override `owner/repo` for downloads (default from `package.json` → `jrmybtlr/DSLinter`). |
37
37
  | `DSLINT_VERBOSE=1` | Log which GitHub releases/assets were tried when downloading. |
38
- | `GITHUB_TOKEN` / `GH_TOKEN` | Optional token for private repos or higher API rate limits. |
38
+ | `GITHUB_TOKEN` / `GH_TOKEN` | **Required for private repos** (`jrmybtlr/DSLinter`). Token needs read access to releases. |
39
39
 
40
40
  ### How this differs from `oxlint`
41
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dslinter",
3
- "version": "0.0.12",
3
+ "version": "0.0.16",
4
4
  "description": "DSLinter dashboard UI: playground shell, token wall, and governance panels (consumes dslint-report.json).",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * Best-effort download of the prebuilt `dslinter` CLI for this platform.
3
- * Used by postinstall and on first `dslinter` / `npx dslinter` invocation.
4
3
  */
5
4
  import { readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
6
5
  import { chmod, mkdir, stat } from "node:fs/promises";
@@ -8,7 +7,9 @@ import { dirname, join } from "node:path";
8
7
  import { fileURLToPath } from "node:url";
9
8
  import {
10
9
  fetchReleasesForVersion,
10
+ githubAuthToken,
11
11
  pickReleaseAsset,
12
+ tryDirectAssetDownload,
12
13
  } from "./github-release.mjs";
13
14
  import {
14
15
  githubRepoFromPackage,
@@ -41,15 +42,31 @@ function releaseRepo(packageRoot) {
41
42
  return githubRepoFromPackage(packageRoot);
42
43
  }
43
44
 
45
+ /**
46
+ * @param {string} dest
47
+ * @param {{ buf: Buffer }} payload
48
+ */
49
+ async function writeVendorBinary(dest, { buf }) {
50
+ const tmp = `${dest}.part`;
51
+ writeFileSync(tmp, buf);
52
+ try {
53
+ if (await pathExists(dest)) unlinkSync(dest);
54
+ } catch {
55
+ /* ignore */
56
+ }
57
+ renameSync(tmp, dest);
58
+ if (process.platform !== "win32") {
59
+ await chmod(dest, 0o755);
60
+ }
61
+ }
62
+
44
63
  /**
45
64
  * @param {string} packageRoot
46
65
  * @param {{ quiet?: boolean }} [opts]
47
- * @returns {Promise<boolean>} true if vendored binary exists after this call
48
66
  */
49
67
  export async function ensureDslintBinary(packageRoot = defaultPackageRoot, opts = {}) {
50
68
  const { quiet = false } = opts;
51
69
  const log = quiet ? () => {} : console.warn.bind(console);
52
- const verbose = process.env.DSLINT_VERBOSE === "1";
53
70
 
54
71
  if (process.env.DSLINT_SKIP_DOWNLOAD === "1") {
55
72
  return pathExists(vendorBinaryPath(packageRoot));
@@ -68,89 +85,94 @@ export async function ensureDslintBinary(packageRoot = defaultPackageRoot, opts
68
85
 
69
86
  const version = readPackageVersion(packageRoot);
70
87
  const repo = releaseRepo(packageRoot);
71
- const vendorDir = join(packageRoot, "vendor");
72
- await mkdir(vendorDir, { recursive: true });
73
- const tmp = `${dest}.part`;
88
+ await mkdir(join(packageRoot, "vendor"), { recursive: true });
74
89
 
75
- let releases;
76
- try {
77
- releases = await fetchReleasesForVersion(repo, version);
78
- } catch (err) {
79
- log(
80
- `[dslinter] Could not query GitHub releases for ${repo}: ${err instanceof Error ? err.message : err}`,
81
- );
82
- if (process.env.GITHUB_TOKEN || process.env.GH_TOKEN) {
83
- log(" (token was set but request still failed — check repo access)");
84
- } else {
90
+ const direct = await tryDirectAssetDownload(repo, version, candidates, log);
91
+ if (direct) {
92
+ await writeVendorBinary(dest, direct);
93
+ if (direct.tag !== `v${version}` && !quiet) {
85
94
  log(
86
- " For private repos, set GITHUB_TOKEN or GH_TOKEN with read access to releases.",
95
+ `[dslinter] Installed ${direct.name} from release ${direct.tag} (npm v${version}).`,
87
96
  );
88
97
  }
89
- return false;
90
- }
91
-
92
- if (releases.length === 0) {
93
- log(
94
- `[dslinter] No GitHub release found on ${repo} for v${version}.\n` +
95
- ` Publish tag v${version} with workflow release-dslint-binaries.yml (assets: ${candidates.join(" or ")}).`,
96
- );
97
- return false;
98
+ return true;
98
99
  }
99
100
 
100
- for (const release of releases) {
101
- const asset = pickReleaseAsset(release, candidates);
102
- if (!asset) {
103
- if (verbose) {
104
- log(
105
- `[dslinter] Release ${release.tag_name} has no asset in ${candidates.join(", ")} (has: ${release.assets.map((a) => a.name).join(", ") || "none"})`,
106
- );
107
- }
108
- continue;
101
+ let apiDenied = false;
102
+ let tagExistsWithoutBinaries = null;
103
+ try {
104
+ const {
105
+ releases,
106
+ apiDenied: denied,
107
+ apiError,
108
+ tagExistsWithoutBinaries: emptyTag,
109
+ } = await fetchReleasesForVersion(repo, version);
110
+ apiDenied = denied;
111
+ tagExistsWithoutBinaries = emptyTag ?? null;
112
+
113
+ if (apiError) {
114
+ log(
115
+ `[dslinter] GitHub API error: ${apiError.message}`,
116
+ );
109
117
  }
110
118
 
111
- if (verbose) {
112
- log(`[dslinter] Downloading ${asset.name} from ${release.tag_name}…`);
113
- }
119
+ for (const release of releases) {
120
+ const asset = pickReleaseAsset(release, candidates);
121
+ if (!asset) continue;
114
122
 
115
- try {
116
- const res = await fetch(asset.browser_download_url, { redirect: "follow" });
117
- if (!res.ok) {
118
- log(`[dslinter] Download failed (${res.status}): ${asset.browser_download_url}`);
119
- continue;
120
- }
123
+ const headers = {};
124
+ const token = githubAuthToken();
125
+ if (token) headers.Authorization = `Bearer ${token}`;
126
+
127
+ const res = await fetch(asset.browser_download_url, {
128
+ headers,
129
+ redirect: "follow",
130
+ });
131
+ if (!res.ok) continue;
121
132
 
122
133
  const buf = Buffer.from(await res.arrayBuffer());
123
- writeFileSync(tmp, buf);
124
- try {
125
- if (await pathExists(dest)) unlinkSync(dest);
126
- } catch {
127
- /* ignore */
128
- }
129
- renameSync(tmp, dest);
130
- if (process.platform !== "win32") {
131
- await chmod(dest, 0o755);
132
- }
134
+ await writeVendorBinary(dest, { buf });
133
135
  if (release.tag_name !== `v${version}` && !quiet) {
134
136
  log(
135
- `[dslinter] Installed scanner from release ${release.tag_name} (npm v${version}).`,
137
+ `[dslinter] Installed ${asset.name} from release ${release.tag_name} (npm v${version}).`,
136
138
  );
137
139
  }
138
140
  return true;
139
- } catch (err) {
140
- log(
141
- `[dslinter] Could not download ${asset.name}: ${err instanceof Error ? err.message : err}`,
142
- );
143
- try {
144
- unlinkSync(tmp);
145
- } catch {
146
- /* ignore */
147
- }
148
141
  }
142
+ } catch (err) {
143
+ log(
144
+ `[dslinter] GitHub API: ${err instanceof Error ? err.message : err}`,
145
+ );
146
+ }
147
+
148
+ const tag = `v${version}`;
149
+ const releasePage = `https://github.com/${repo}/releases/tag/${tag}`;
150
+
151
+ if (tagExistsWithoutBinaries) {
152
+ log(
153
+ `[dslinter] GitHub release ${tagExistsWithoutBinaries} exists but has no scanner binaries attached.\n` +
154
+ ` Run the "Release dslinter binaries" workflow on ${repo} (Actions → workflow_dispatch, tag ${tagExistsWithoutBinaries}),\n` +
155
+ ` or push a new tag so CI uploads assets like ${candidates[0]}.\n` +
156
+ ` ${releasePage}`,
157
+ );
158
+ return false;
159
+ }
160
+
161
+ if (apiDenied || !githubAuthToken()) {
162
+ log(
163
+ `[dslinter] Cannot reach ${repo} releases without authentication (GitHub returned 404).\n` +
164
+ ` If you can open ${releasePage} in a browser, the repo is likely private.\n` +
165
+ ` Create a fine-grained or classic token with repo read access, then:\n` +
166
+ ` export GITHUB_TOKEN=ghp_...\n` +
167
+ ` npx dslinter\n` +
168
+ ` Or install from source: cargo install --git https://github.com/${repo} dslinter --locked`,
169
+ );
170
+ return false;
149
171
  }
150
172
 
151
173
  log(
152
- `[dslinter] Found releases on ${repo} but none include ${candidates.join(" or ")} for this platform.\n` +
153
- ` Upload platform binaries via .github/workflows/release-dslint-binaries.yml, or set DSLINT_BIN.`,
174
+ `[dslinter] No release with asset ${candidates.join(" or ")} on ${repo} for ${tag}.\n` +
175
+ ` Create ${releasePage} and run release-dslint-binaries.yml, or set DSLINT_BIN.`,
154
176
  );
155
177
  return false;
156
178
  }
@@ -1,79 +1,150 @@
1
1
  /**
2
- * Resolve GitHub release assets via the REST API (works with exact names + legacy names).
2
+ * Resolve and download GitHub release assets (API + direct URLs; supports private repos with a token).
3
3
  */
4
4
 
5
5
  const API = "https://api.github.com";
6
6
 
7
+ export function githubAuthToken() {
8
+ return (
9
+ process.env.GITHUB_TOKEN?.trim() || process.env.GH_TOKEN?.trim() || null
10
+ );
11
+ }
12
+
13
+ export function downloadAuthHeaders() {
14
+ const token = githubAuthToken();
15
+ return token ? { Authorization: `Bearer ${token}` } : {};
16
+ }
17
+
7
18
  function apiHeaders() {
8
- const headers = {
19
+ return {
9
20
  Accept: "application/vnd.github+json",
10
21
  "User-Agent": "dslinter-npm",
11
22
  "X-GitHub-Api-Version": "2022-11-28",
23
+ ...downloadAuthHeaders(),
12
24
  };
13
- const token =
14
- process.env.GITHUB_TOKEN?.trim() || process.env.GH_TOKEN?.trim();
15
- if (token) headers.Authorization = `Bearer ${token}`;
16
- return headers;
17
25
  }
18
26
 
19
27
  /**
20
28
  * @param {string} repo `owner/name`
29
+ * @param {string} tag `v0.0.12` or `latest`
30
+ * @param {string} assetName
31
+ */
32
+ export function directAssetUrl(repo, tag, assetName) {
33
+ const file = encodeURIComponent(assetName);
34
+ if (tag === "latest") {
35
+ return `https://github.com/${repo}/releases/latest/download/${file}`;
36
+ }
37
+ return `https://github.com/${repo}/releases/download/${encodeURIComponent(tag)}/${file}`;
38
+ }
39
+
40
+ /**
41
+ * @param {string} npmVersion
42
+ */
43
+ export function releaseTagsToTry(npmVersion) {
44
+ const override = process.env.DSLINT_RELEASE_TAG?.trim();
45
+ if (override) {
46
+ return [override.startsWith("v") ? override : `v${override}`];
47
+ }
48
+ return [`v${npmVersion}`, npmVersion, "latest"];
49
+ }
50
+
51
+ /**
52
+ * @param {string} repo
21
53
  * @param {string} path
54
+ * @returns {Promise<{ data: unknown } | { notFound: true } | { error: Error }>}
22
55
  */
23
56
  async function githubGet(repo, path) {
24
57
  const res = await fetch(`${API}/repos/${repo}${path}`, {
25
58
  headers: apiHeaders(),
26
59
  redirect: "follow",
27
60
  });
28
- if (res.status === 404) return null;
61
+ if (res.status === 404) return { notFound: true };
29
62
  if (!res.ok) {
30
63
  const body = await res.text().catch(() => "");
31
- throw new Error(`GitHub API ${res.status} for ${path}: ${body.slice(0, 200)}`);
64
+ return {
65
+ error: new Error(`GitHub API ${res.status} for ${path}: ${body.slice(0, 200)}`),
66
+ };
32
67
  }
33
- return res.json();
68
+ return { data: await res.json() };
34
69
  }
35
70
 
36
71
  /**
37
72
  * @param {string} repo
38
- * @param {string} tag e.g. `v0.0.11` or `latest`
73
+ * @param {string} tag e.g. `v0.0.12` or `latest`
39
74
  */
40
75
  export async function fetchRelease(repo, tag) {
41
- if (tag === "latest") {
42
- return githubGet(repo, "/releases/latest");
43
- }
44
- const byTag = await githubGet(repo, `/releases/tags/${encodeURIComponent(tag)}`);
45
- if (byTag) return byTag;
46
- // Some releases use tag without `v` prefix.
47
- if (tag.startsWith("v")) {
48
- return githubGet(repo, `/releases/tags/${encodeURIComponent(tag.slice(1))}`);
76
+ const path =
77
+ tag === "latest" ? "/releases/latest" : `/releases/tags/${encodeURIComponent(tag)}`;
78
+ let result = await githubGet(repo, path);
79
+ if (result.notFound && tag.startsWith("v")) {
80
+ result = await githubGet(
81
+ repo,
82
+ `/releases/tags/${encodeURIComponent(tag.slice(1))}`,
83
+ );
49
84
  }
85
+ if ("data" in result) return result.data;
50
86
  return null;
51
87
  }
52
88
 
53
89
  /**
54
90
  * @param {string} repo
55
- * @param {string} npmVersion e.g. `0.0.11`
91
+ * @param {string} npmVersion
56
92
  */
93
+ /** @param {unknown} release */
94
+ function hasScannerAssets(release) {
95
+ const names = release?.assets?.map((a) => a.name) ?? [];
96
+ return names.some((n) => n.startsWith("dslinter-") || n.startsWith("dslint-"));
97
+ }
98
+
57
99
  export async function fetchReleasesForVersion(repo, npmVersion) {
58
100
  const out = [];
59
101
  const seen = new Set();
102
+ /** @type {string | null} */
103
+ let tagExistsWithoutBinaries = null;
104
+
105
+ const consider = (release) => {
106
+ if (!release?.tag_name) return;
107
+ const tag = String(release.tag_name);
108
+ const matches =
109
+ tag === `v${npmVersion}` || tag === npmVersion;
110
+ if (matches && !hasScannerAssets(release)) {
111
+ tagExistsWithoutBinaries = tag;
112
+ }
113
+ };
60
114
 
61
115
  const push = (release) => {
62
- if (!release?.assets?.length) return;
116
+ if (!hasScannerAssets(release)) return;
63
117
  const key = release.id ?? release.tag_name;
64
118
  if (seen.has(key)) return;
65
119
  seen.add(key);
66
120
  out.push(release);
67
121
  };
68
122
 
69
- push(await fetchRelease(repo, `v${npmVersion}`));
70
- push(await fetchRelease(repo, npmVersion));
71
- push(await fetchRelease(repo, "latest"));
123
+ const vRelease = await fetchRelease(repo, `v${npmVersion}`);
124
+ consider(vRelease);
125
+ push(vRelease);
126
+
127
+ const bareRelease = await fetchRelease(repo, npmVersion);
128
+ consider(bareRelease);
129
+ push(bareRelease);
130
+
131
+ const latestRelease = await fetchRelease(repo, "latest");
132
+ push(latestRelease);
72
133
 
73
- if (out.length > 0) return out;
134
+ if (out.length > 0) {
135
+ return { releases: out, apiDenied: false, tagExistsWithoutBinaries };
136
+ }
74
137
 
75
- const list = await githubGet(repo, "/releases?per_page=30");
76
- if (!Array.isArray(list)) return out;
138
+ const listResult = await githubGet(repo, "/releases?per_page=30");
139
+ if ("error" in listResult) {
140
+ return { releases: out, apiDenied: false, apiError: listResult.error };
141
+ }
142
+ if (listResult.notFound) {
143
+ return { releases: out, apiDenied: !githubAuthToken() };
144
+ }
145
+
146
+ const list = listResult.data;
147
+ if (!Array.isArray(list)) return { releases: out, apiDenied: false };
77
148
 
78
149
  for (const release of list) {
79
150
  const tag = String(release.tag_name ?? "");
@@ -81,9 +152,10 @@ export async function fetchReleasesForVersion(repo, npmVersion) {
81
152
  push(release);
82
153
  }
83
154
  }
84
- if (out.length > 0) return out;
155
+ if (out.length > 0) {
156
+ return { releases: out, apiDenied: false, tagExistsWithoutBinaries };
157
+ }
85
158
 
86
- // Newest release that has any scanner-looking asset.
87
159
  for (const release of list) {
88
160
  const names = release.assets?.map((a) => a.name) ?? [];
89
161
  if (names.some((n) => n.startsWith("dslinter-") || n.startsWith("dslint-"))) {
@@ -92,13 +164,12 @@ export async function fetchReleasesForVersion(repo, npmVersion) {
92
164
  }
93
165
  }
94
166
 
95
- return out;
167
+ return { releases: out, apiDenied: false, tagExistsWithoutBinaries };
96
168
  }
97
169
 
98
170
  /**
99
171
  * @param {string[]} candidateNames
100
172
  * @param {{ assets: { name: string; browser_download_url: string }[] }} release
101
- * @returns {{ name: string; browser_download_url: string } | null}
102
173
  */
103
174
  export function pickReleaseAsset(release, candidateNames) {
104
175
  for (const name of candidateNames) {
@@ -107,3 +178,42 @@ export function pickReleaseAsset(release, candidateNames) {
107
178
  }
108
179
  return null;
109
180
  }
181
+
182
+ /**
183
+ * Download via public/private release URLs (no API metadata required).
184
+ * @param {(msg: string) => void} log
185
+ */
186
+ export async function tryDirectAssetDownload(repo, npmVersion, candidateNames, log) {
187
+ const verbose = process.env.DSLINT_VERBOSE === "1";
188
+ const headers = downloadAuthHeaders();
189
+ const tags = releaseTagsToTry(npmVersion);
190
+
191
+ for (const tag of tags) {
192
+ for (const name of candidateNames) {
193
+ const url = directAssetUrl(repo, tag, name);
194
+ if (verbose) log(`[dslinter] GET ${url}`);
195
+ try {
196
+ const res = await fetch(url, { headers, redirect: "follow" });
197
+ if (res.status === 404) continue;
198
+ if (!res.ok) {
199
+ if (verbose) log(`[dslinter] ${res.status} ${url}`);
200
+ continue;
201
+ }
202
+ const contentType = res.headers.get("content-type") ?? "";
203
+ if (contentType.includes("text/html")) continue;
204
+
205
+ const buf = Buffer.from(await res.arrayBuffer());
206
+ if (buf.length < 512) continue;
207
+
208
+ return { buf, tag, name, url };
209
+ } catch (err) {
210
+ if (verbose) {
211
+ log(
212
+ `[dslinter] ${url}: ${err instanceof Error ? err.message : err}`,
213
+ );
214
+ }
215
+ }
216
+ }
217
+ }
218
+ return null;
219
+ }
@@ -1,27 +1,32 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { githubRepoFromPackage } from "./resolve-dslint-binary.mjs";
4
5
 
5
6
  const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
6
7
  const version = JSON.parse(
7
8
  readFileSync(join(packageRoot, "package.json"), "utf8"),
8
9
  ).version;
10
+ const repo = githubRepoFromPackage(packageRoot);
9
11
 
10
12
  process.stderr.write(`dslinter: scanner binary not available.
11
13
 
12
- This npm package is NOT the same as \`cargo install dslint\` on crates.io (that is a
13
- different "design file" linter and will crash or misbehave).
14
+ Do NOT run: cargo install dslint
15
+ That installs a different package on crates.io (design-file linter).
14
16
 
15
- To run the design-system scanner:
17
+ Install the design-system scanner from this repo instead:
16
18
 
17
- 1. Re-run after a GitHub release exists for v${version} (prebuilt download), or
18
- 2. Build from this repo and point at it:
19
- cargo install --git https://github.com/jrmybtlr/DSLinter dslinter --locked
20
- export DSLINT_BIN="$(command -v dslinter)"
21
- npx dslinter ...
22
- 3. Or set DSLINT_BIN to your local target/release/dslinter
19
+ cargo install --git https://github.com/${repo} dslinter --locked
20
+ export DSLINT_BIN="$(command -v dslinter)"
21
+ npx dslinter ...
23
22
 
24
- Releases: https://github.com/jrmybtlr/DSLinter/releases
23
+ If releases exist at https://github.com/${repo}/releases/tag/v${version} but
24
+ download failed, the repo may be private — set a GitHub token then retry:
25
25
 
26
- Tip: re-run with DSLINT_VERBOSE=1 to see which GitHub releases/assets were tried.
26
+ export GITHUB_TOKEN=ghp_... # needs read access to ${repo}
27
+ npx dslinter
28
+
29
+ Or point at a local build: DSLINT_BIN=/path/to/dslinter
30
+
31
+ Tip: DSLINT_VERBOSE=1 shows each download URL tried.
27
32
  `);
@@ -1,7 +1,9 @@
1
1
  import { join } from "node:path";
2
2
  import { describe, expect, it } from "vitest";
3
3
  import {
4
+ directAssetUrl,
4
5
  pickReleaseAsset,
6
+ releaseTagsToTry,
5
7
  } from "../scripts/github-release.mjs";
6
8
  import {
7
9
  CLI_BINARY_NAME,
@@ -37,6 +39,30 @@ describe("parseGitHubRepo", () => {
37
39
  });
38
40
  });
39
41
 
42
+ describe("directAssetUrl", () => {
43
+ it("uses releases/latest/download for latest tag", () => {
44
+ expect(
45
+ directAssetUrl("jrmybtlr/DSLinter", "latest", "dslinter-aarch64-apple-darwin"),
46
+ ).toBe(
47
+ "https://github.com/jrmybtlr/DSLinter/releases/latest/download/dslinter-aarch64-apple-darwin",
48
+ );
49
+ });
50
+
51
+ it("uses releases/download for version tags", () => {
52
+ expect(
53
+ directAssetUrl("jrmybtlr/DSLinter", "v0.0.12", "dslinter-aarch64-apple-darwin"),
54
+ ).toBe(
55
+ "https://github.com/jrmybtlr/DSLinter/releases/download/v0.0.12/dslinter-aarch64-apple-darwin",
56
+ );
57
+ });
58
+ });
59
+
60
+ describe("releaseTagsToTry", () => {
61
+ it("tries v-prefixed tag first", () => {
62
+ expect(releaseTagsToTry("0.0.12")).toEqual(["v0.0.12", "0.0.12", "latest"]);
63
+ });
64
+ });
65
+
40
66
  describe("releaseAssetCandidateNames", () => {
41
67
  it("includes legacy dslint asset name", () => {
42
68
  expect(releaseAssetCandidateNames(proc("darwin", "arm64"))).toEqual([