Skip to content

Commit 19c8b0b

Browse files
feat(cache): add support for repositories with tags & changelogs
yes this wasn't *yet* ported
1 parent 777fe0c commit 19c8b0b

File tree

5 files changed

+185
-44
lines changed

5 files changed

+185
-44
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"@tailwindcss/vite": "^4.0.14",
2929
"@total-typescript/ts-reset": "^0.6.1",
3030
"@types/eslint-config-prettier": "^6.11.3",
31+
"@types/node": "^22.13.13",
3132
"@types/semver": "^7.5.8",
3233
"@upstash/redis": "^1.34.6",
3334
"@vercel/speed-insights": "^1.2.0",

pnpm-lock.yaml

Lines changed: 44 additions & 28 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/server/github-cache.ts

Lines changed: 130 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { GITHUB_TOKEN, KV_REST_API_TOKEN, KV_REST_API_URL } from "$env/static/private";
22
import { Redis } from "@upstash/redis";
33
import { Octokit } from "octokit";
4+
import type { Repository } from "$lib/repositories";
5+
import parseChangelog from "$lib/changelog-parser";
46

57
export type GitHubRelease = Awaited<
68
ReturnType<InstanceType<typeof Octokit>["rest"]["repos"]["listReleases"]>
@@ -61,12 +63,11 @@ export class GitHubCache {
6163
/**
6264
* Get all the releases for a given repository
6365
*
64-
* @param owner the owner of the GitHub repository to get releases from
65-
* @param repo the name of the GitHub repository to get releases from
66+
* @param repository the repository to get the releases for
6667
* @returns the releases, either cached or fetched
6768
*/
68-
async getReleases(owner: string, repo: string) {
69-
const cacheKey = this.#getRepoKey(owner, repo);
69+
async getReleases(repository: Repository) {
70+
const cacheKey = this.#getRepoKey(repository.owner, repository.repoName);
7071

7172
const cachedReleases = await this.#redis.json.get<GitHubRelease[]>(cacheKey);
7273
if (cachedReleases) {
@@ -76,11 +77,7 @@ export class GitHubCache {
7677

7778
console.log(`Cache miss for ${cacheKey}, fetching from GitHub API`);
7879

79-
const { data: releases } = await this.#octokit.rest.repos.listReleases({
80-
owner,
81-
repo,
82-
per_page
83-
});
80+
const releases = await this.#fetchReleases(repository);
8481

8582
await this.#redis.json.set(cacheKey, "$", releases);
8683

@@ -146,6 +143,130 @@ export class GitHubCache {
146143
return releases;
147144
}
148145

146+
/**
147+
* A utility method to fetch the releases based on the
148+
* mode we want to use to get them
149+
*
150+
* @param repository the repository to fetch the releases for
151+
* @returns the fetched releases
152+
* @private
153+
*/
154+
async #fetchReleases(repository: Repository): Promise<GitHubRelease[]> {
155+
const { owner, repoName: repo, changesMode, changelogContentsReplacer } = repository;
156+
if (changesMode === "releases" || !changesMode) {
157+
const { data: releases } = await this.#octokit.rest.repos.listReleases({
158+
owner,
159+
repo,
160+
per_page
161+
});
162+
return releases;
163+
}
164+
165+
// Changelog mode: we'll need to get the tags and re-build releases from them
166+
167+
// 1. Fetch tags
168+
const { data: tags } = await this.#octokit.rest.repos.listTags({
169+
owner,
170+
repo,
171+
per_page
172+
});
173+
174+
// 2. Fetch changelog
175+
const { data: changelogResult } = await this.#octokit.rest.repos.getContent({
176+
owner,
177+
repo,
178+
ref:
179+
owner === "sveltejs" &&
180+
repo === "prettier-plugin-svelte" && // this repo is a bit of a mess
181+
tags[0] &&
182+
repository.metadataFromTag(tags[0].name)[1].startsWith("3")
183+
? "version-3" // a temporary fix to get the changelog from the right branch while v4 isn't out yet
184+
: undefined,
185+
path: "CHANGELOG.md"
186+
});
187+
188+
if (!("content" in changelogResult)) return []; // filter out empty or multiple results
189+
const { content, encoding, type } = changelogResult;
190+
if (type !== "file" || !content) return []; // filter out directories and empty files
191+
const changelogFileContents =
192+
encoding === "base64" ? Buffer.from(content, "base64").toString() : content;
193+
// Actually parse the changelog file
194+
const { versions } = await parseChangelog(
195+
changelogContentsReplacer?.(changelogFileContents) ?? changelogFileContents
196+
);
197+
198+
/**
199+
* Returns a simili-hash for local ID creation purposes
200+
*
201+
* @param input the input string
202+
* @returns a (hopefully unique) and pure hashcode
203+
*/
204+
function simpleHash(input: string) {
205+
return Math.abs(
206+
input.split("").reduce((hash, char) => (hash * 31 + char.charCodeAt(0)) & 0xffffffff, 0)
207+
);
208+
}
209+
210+
// 3. Return the recreated releases
211+
return await Promise.all(
212+
tags.map(
213+
async (
214+
{ name: tag_name, commit: { sha }, zipball_url, tarball_url, node_id },
215+
tagIndex
216+
) => {
217+
const {
218+
data: { author, committer }
219+
} = await this.#octokit.rest.git.getCommit({ owner, repo, commit_sha: sha });
220+
const [, cleanVersion] = repository.metadataFromTag(tag_name);
221+
const changelogVersion = versions.find(
222+
({ version }) => !!version?.includes(cleanVersion)
223+
);
224+
return {
225+
url: "",
226+
html_url: `https://github.com/${owner}/${repo}/releases/tag/${tag_name}`,
227+
assets_url: "",
228+
upload_url: "",
229+
tarball_url,
230+
zipball_url,
231+
id: simpleHash(`${owner}/${repo}`) + tagIndex,
232+
node_id,
233+
tag_name,
234+
target_commitish: "main",
235+
name: `${repo}@${cleanVersion}`,
236+
body: changelogVersion?.body ?? "_No changelog provided._",
237+
draft: false,
238+
prerelease: tag_name.includes("-"),
239+
created_at: committer.date,
240+
published_at: null,
241+
author: {
242+
name: author.name,
243+
login: "",
244+
email: author.email,
245+
id: 0,
246+
node_id: "",
247+
avatar_url: "",
248+
gravatar_id: null,
249+
url: "",
250+
html_url: "",
251+
followers_url: "",
252+
following_url: "",
253+
gists_url: "",
254+
starred_url: "",
255+
subscriptions_url: "",
256+
organizations_url: "",
257+
repos_url: "",
258+
events_url: "",
259+
received_events_url: "",
260+
type: "",
261+
site_admin: false
262+
},
263+
assets: []
264+
} satisfies GitHubRelease;
265+
}
266+
)
267+
);
268+
}
269+
149270
/**
150271
* Checks if releases are present in the cache for the
151272
* given GitHub info

src/lib/server/package-discoverer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export class PackageDiscoverer {
7373
async discoverAll() {
7474
this.#packages = await Promise.all(
7575
this.#repos.map(async repo => {
76-
const releases = await this.#cache.getReleases(repo.owner, repo.repoName);
76+
const releases = await this.#cache.getReleases(repo);
7777
const packages = [
7878
...new Set(
7979
releases

0 commit comments

Comments
 (0)