summaryrefslogtreecommitdiff
path: root/cgithub
diff options
context:
space:
mode:
Diffstat (limited to 'cgithub')
-rw-r--r--cgithub/src/scraper.test.ts6
-rw-r--r--cgithub/src/scraper.ts100
-rw-r--r--cgithub/static/style.css14
-rw-r--r--cgithub/views/_release.eta13
-rw-r--r--cgithub/views/layout.eta2
5 files changed, 106 insertions, 29 deletions
diff --git a/cgithub/src/scraper.test.ts b/cgithub/src/scraper.test.ts
index 5bd609a..125e6a6 100644
--- a/cgithub/src/scraper.test.ts
+++ b/cgithub/src/scraper.test.ts
@@ -188,6 +188,12 @@ describe("GitHub scraper", () => {
assert.ok(data.release.title.length > 0);
assert.ok(data.release.publishedAt.length > 0);
assert.ok(data.release.bodyHtml.length > 0);
+ assert.ok(data.release.assets && data.release.assets.length >= 2);
+ assert.strictEqual(data.release.assets[0].name, "Source code (zip)");
+ assert.strictEqual(
+ data.release.assets[0].url,
+ "https://github.com/actions/deploy-pages/archive/refs/tags/v4.0.5.zip",
+ );
});
});
diff --git a/cgithub/src/scraper.ts b/cgithub/src/scraper.ts
index 0ed77b5..769b870 100644
--- a/cgithub/src/scraper.ts
+++ b/cgithub/src/scraper.ts
@@ -196,6 +196,14 @@ interface Release {
title: string;
publishedAt: string;
bodyHtml: string;
+ assets?: ReleaseAsset[];
+}
+
+export interface ReleaseAsset {
+ name: string;
+ url: string;
+ size?: string;
+ publishedAt?: string;
}
export interface GitHubReleases extends GitHubCommon {
@@ -259,14 +267,14 @@ function parseRepoInfo(html: string): RepoInfo {
const websiteElem = cssSelect.selectOne(
"a[rel='noopener noreferrer nofollow']",
parent,
- ) as Element;
+ ) as Element | null;
if (websiteElem) {
website = getAttributeValue(websiteElem, "href") || null;
}
}
}
- const starsElem = cssSelect.selectOne("#repo-stars-counter-star", document) as unknown as Element;
+ const starsElem = cssSelect.selectOne("#repo-stars-counter-star", document) as Element | null;
const stars = starsElem
? getAttributeValue(starsElem, "title") || textContent(starsElem).trim()
: null;
@@ -274,7 +282,7 @@ function parseRepoInfo(html: string): RepoInfo {
const watchersElem = cssSelect.selectOne("a[href$='/watchers'] strong", document);
const watchers = watchersElem ? textContent(watchersElem).trim() : null;
- const forksElem = cssSelect.selectOne("#repo-network-counter", document) as unknown as Element;
+ const forksElem = cssSelect.selectOne("#repo-network-counter", document) as Element | null;
const forks = forksElem
? getAttributeValue(forksElem, "title") || textContent(forksElem).trim()
: null;
@@ -282,7 +290,7 @@ function parseRepoInfo(html: string): RepoInfo {
const releaseElem = cssSelect.selectOne(
"a[href$='/releases'] .Counter",
document,
- ) as unknown as Element;
+ ) as Element | null;
let numReleases: number | undefined;
let latestReleaseTag: string | undefined;
let latestReleaseDate: string | undefined;
@@ -520,7 +528,6 @@ export async function getGitHubReleases(owner: string, repo: string): Promise<Gi
if (!h2) continue;
const title = textContent(h2).trim();
- if (!title.startsWith("v")) continue;
releases.push(parseRelease(section, title));
}
@@ -534,41 +541,82 @@ export async function getGitHubReleases(owner: string, repo: string): Promise<Gi
};
}
-export async function getGitHubRelease(
+export async function getGitHubReleaseBase(
owner: string,
repo: string,
tag: string,
): Promise<GitHubRelease> {
const html = await fetchGitHubPage(`${owner}/${repo}/releases/tag/${tag}`);
- // Find the h1 tag with the release title
- const h1Match = html.match(/<h1[^>]*class="[^"]*d-inline[^"]*"[^>]*>([^<]*)<\/h1>/);
- const title = h1Match?.[1]?.trim() || tag;
+ const document = parseDocument(html);
- // Extract tag name from tree link
- const tagLinkMatch = html.match(/<a[^>]*href="[^"]*\/tree\/([^"]+)"[^>]*>/);
- const tagName = tagLinkMatch?.[1] || tag;
+ const box = cssSelect.selectOne(".Box:has(h1)", document) as Element | null;
+ if (!box) throw new Error("Could not find release box");
- // Extract published date
- const dateMatch = html.match(/<relative-time[^>]*datetime="([^"]*)"[^>]*>/);
- const publishedAt = dateMatch?.[1] || "";
+ const h1 = cssSelect.selectOne("h1", box);
+ const title = h1 ? textContent(h1).trim() : "";
- // Extract body HTML (markdown content) - find the markdown-body div
- const bodyMatch = html.match(
- /<div[^>]*class="[^"]*markdown-body[^"]*"[^>]*>([\s\S]*?)<\/div>\s*<div[^>]*class="mb-3"/,
- );
- const bodyHtml = bodyMatch?.[1]?.trim() || "";
+ const release = parseRelease(box, title);
return {
repo: {
owner,
name: repo,
},
- release: {
- tagName,
- title,
- publishedAt,
- bodyHtml,
- },
+ release,
};
}
+
+export async function getGitHubReleaseAssets(
+ owner: string,
+ repo: string,
+ tag: string,
+): Promise<ReleaseAsset[]> {
+ const html = await fetchGitHubPage(`${owner}/${repo}/releases/expanded_assets/${tag}`);
+ const document = parseDocument(html);
+ const assets: ReleaseAsset[] = [];
+
+ for (const row of cssSelect.selectAll("li", document) as unknown as Element[]) {
+ const link = cssSelect.selectOne("a[href]", row);
+ if (!link) continue;
+
+ const url = getAttributeValue(link, "href") || "";
+ // Filename is in the link, often split into multiple spans
+ const name = textContent(link).replace(/\s+/g, " ").trim();
+
+ let size: string | undefined;
+ let publishedAt: string | undefined;
+ for (const s of cssSelect.selectAll("span.color-fg-muted.text-right", row)) {
+ // Size is in a span with color-fg-muted and text-right, but avoid the one with relative-time
+ const timeElem = cssSelect.selectOne("relative-time", row);
+ if (timeElem) {
+ publishedAt = timeElem ? getAttributeValue(timeElem, "datetime") || "" : "";
+ } else {
+ const text = textContent(s).trim();
+ if (text) size = text;
+ }
+ }
+
+ assets.push({
+ name,
+ url: url.startsWith("/") ? `https://github.com${url}` : url,
+ size,
+ publishedAt,
+ });
+ }
+
+ return assets;
+}
+
+export async function getGitHubRelease(
+ owner: string,
+ repo: string,
+ tag: string,
+): Promise<GitHubRelease> {
+ const [base, assets] = await Promise.all([
+ getGitHubReleaseBase(owner, repo, tag),
+ getGitHubReleaseAssets(owner, repo, tag),
+ ]);
+ base.release.assets = assets;
+ return base;
+}
diff --git a/cgithub/static/style.css b/cgithub/static/style.css
index fdf1b34..b3ebb46 100644
--- a/cgithub/static/style.css
+++ b/cgithub/static/style.css
@@ -111,11 +111,16 @@ section {
margin-bottom: 0;
padding: .5rem;
border-bottom: 1px solid var(--border-color);
- font-weight: bold;
+ }
+
+ > footer:last-child {
+ margin: 0 calc(-1 * var(--box-padding));
+ padding: .5rem;
+ border-top: 1px solid var(--border-color);
}
}
-footer {
+body > footer {
margin-top: 1rem;
text-align: center;
}
@@ -207,11 +212,16 @@ pre.box {
}
}
+.overview > header {
+ font-weight: bold;
+}
+
.release > header {
display: flex;
align-items: baseline;
> :first-child {
flex-grow: 1;
+ font-weight: bold;
}
}
diff --git a/cgithub/views/_release.eta b/cgithub/views/_release.eta
index 4769f9b..a8838fb 100644
--- a/cgithub/views/_release.eta
+++ b/cgithub/views/_release.eta
@@ -16,4 +16,17 @@
<% if (it.release.bodyHtml) { %>
<%~ it.release.bodyHtml %>
<% } %>
+ <% if (it.release.assets && it.release.assets.length > 0) { %>
+ <footer>
+ <h3>Assets</h3>
+ <% for (const asset of it.release.assets) { %>
+ <div>
+ <a href="<%= asset.url %>"><%= asset.name %></a>
+ <% if (asset.publishedAt) { %>
+ <time class="timestamp" title="<%= asset.publishedAt %>"><%= asset.publishedAt.split("T")[0] %></time>
+ <% } %>
+ </div>
+ <% } %>
+ </footer>
+ <% } %>
</div>
diff --git a/cgithub/views/layout.eta b/cgithub/views/layout.eta
index 3910d4b..c42d593 100644
--- a/cgithub/views/layout.eta
+++ b/cgithub/views/layout.eta
@@ -13,10 +13,10 @@ git clone git@github.com:<%= it.repo.owner %>/<%= it.repo.name %>.git</code></pr
<nav>
<a href="/<%= it.repo.owner %>/<%= it.repo.name %>">Code</a>
<a href="/<%= it.repo.owner %>/<%= it.repo.name %>/issues">Issues</a>
+ <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/releases">Releases</a>
<% if (it.branch) { %>
<a href="/<%= it.repo.owner %>/<%= it.repo.name %>/commits/<%= it.branch %><%= it.path && it.path !== '/' ? '/' + it.path : '' %>">Commits</a>
<% } %>
- <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/releases">Releases</a>
</nav>
</div>