summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cgithub/src/index.ts6
-rw-r--r--cgithub/src/scraper.test.ts13
-rw-r--r--cgithub/src/scraper.ts93
-rw-r--r--cgithub/static/style.css4
-rw-r--r--cgithub/views/_release.eta23
-rw-r--r--cgithub/views/release.eta7
-rw-r--r--cgithub/views/releases.eta20
7 files changed, 124 insertions, 42 deletions
diff --git a/cgithub/src/index.ts b/cgithub/src/index.ts
index 2f8efef..59c98ee 100644
--- a/cgithub/src/index.ts
+++ b/cgithub/src/index.ts
@@ -9,6 +9,7 @@ import {
getGitHubBlob,
getGitHubCommits,
getGitHubIssues,
+ getGitHubRelease,
getGitHubReleases,
getGitHubRepo,
getGitHubTree,
@@ -74,6 +75,11 @@ app.get("/:owner/:repo/commits/:branch/:path{.*}?", async (c) => {
return tryRender(c, "commits.eta", getGitHubCommits(owner, repo, branch, path));
});
+app.get("/:owner/:repo/releases/tag/:tag", async (c) => {
+ const { owner, repo, tag } = c.req.param();
+ return tryRender(c, "release.eta", getGitHubRelease(owner, repo, tag));
+});
+
app.get("/:owner/:repo/releases", async (c) => {
const { owner, repo } = c.req.param();
return tryRender(c, "releases.eta", getGitHubReleases(owner, repo));
diff --git a/cgithub/src/scraper.test.ts b/cgithub/src/scraper.test.ts
index 33f00f1..0b91b6a 100644
--- a/cgithub/src/scraper.test.ts
+++ b/cgithub/src/scraper.test.ts
@@ -4,6 +4,7 @@ import {
getGitHubBlob,
getGitHubCommits,
getGitHubIssues,
+ getGitHubRelease,
getGitHubReleases,
getGitHubRepo,
getGitHubTree,
@@ -174,6 +175,18 @@ describe("GitHub scraper", () => {
assert.ok(firstRelease.publishedAt.length > 0);
assert.ok(firstRelease.bodyHtml.length > 0);
});
+
+ it("should fetch single release", async () => {
+ const data = await getGitHubRelease("actions", "deploy-pages", "v4.0.5");
+
+ assert.strictEqual(data.repo.owner, "actions");
+ assert.strictEqual(data.repo.name, "deploy-pages");
+
+ assert.ok(data.release.tagName.length > 0);
+ assert.ok(data.release.title.length > 0);
+ assert.ok(data.release.publishedAt.length > 0);
+ assert.ok(data.release.bodyHtml.length > 0);
+ });
});
describe("rich file types", () => {
diff --git a/cgithub/src/scraper.ts b/cgithub/src/scraper.ts
index af39660..acfaac1 100644
--- a/cgithub/src/scraper.ts
+++ b/cgithub/src/scraper.ts
@@ -200,6 +200,10 @@ export interface GitHubReleases extends GitHubCommon {
releases: Release[];
}
+export interface GitHubRelease extends GitHubCommon {
+ release: Release;
+}
+
async function fetchGitHubPage(path: string): Promise<string> {
// GitHub throttles/blocks requests without realistic browser headers.
// These headers make the request appear as a standard browser visit.
@@ -420,6 +424,33 @@ export async function getGitHubCommits(
};
}
+function parseRelease(section: Element, title: string): Release {
+ // Extract tag name from link
+ const tagLink = cssSelect.selectOne('a[href*="/tree/"]', section);
+ let tagName = title;
+ if (tagLink) {
+ const href = getAttributeValue(tagLink, "href");
+ if (href) {
+ tagName = href.split("/").pop() || title;
+ }
+ }
+
+ // Extract published date
+ const relativeTime = cssSelect.selectOne("relative-time", section);
+ const publishedAt = relativeTime ? getAttributeValue(relativeTime, "datetime") || "" : "";
+
+ // Extract body HTML (markdown content)
+ const body = cssSelect.selectOne(".markdown-body", section);
+ const bodyHtml = body ? getInnerHTML(body).trim() : "";
+
+ return {
+ tagName,
+ title,
+ publishedAt,
+ bodyHtml,
+ };
+}
+
export async function getGitHubReleases(owner: string, repo: string): Promise<GitHubReleases> {
const html = await fetchGitHubPage(`${owner}/${repo}/releases`);
@@ -436,37 +467,53 @@ export async function getGitHubReleases(owner: string, repo: string): Promise<Gi
const title = textContent(h2).trim();
if (!title.startsWith("v")) continue;
- // Extract tag name from link
- const tagLink = cssSelect.selectOne('a[href*="/tree/"]', section);
- let tagName = title;
- if (tagLink) {
- const href = getAttributeValue(tagLink, "href");
- if (href) {
- tagName = href.split("/").pop() || title;
- }
- }
+ releases.push(parseRelease(section, title));
+ }
+
+ return {
+ repo: {
+ owner,
+ name: repo,
+ },
+ releases,
+ };
+}
- // Extract published date
- const relativeTime = cssSelect.selectOne("relative-time", section);
- const publishedAt = relativeTime ? getAttributeValue(relativeTime, "datetime") || "" : "";
+export async function getGitHubRelease(
+ owner: string,
+ repo: string,
+ tag: string,
+): Promise<GitHubRelease> {
+ const html = await fetchGitHubPage(`${owner}/${repo}/releases/tag/${tag}`);
- // Extract body HTML (markdown content)
- const body = cssSelect.selectOne(".markdown-body", section);
- const bodyHtml = body ? getInnerHTML(body).trim() : "";
+ // Find the h1 tag with the release title
+ const h1Match = html.match(/<h1[^>]*class="[^"]*d-inline[^"]*"[^>]*>([^<]*)<\/h1>/);
+ const title = h1Match?.[1]?.trim() || tag;
- releases.push({
- tagName,
- title,
- publishedAt,
- bodyHtml,
- });
- }
+ // Extract tag name from tree link
+ const tagLinkMatch = html.match(/<a[^>]*href="[^"]*\/tree\/([^"]+)"[^>]*>/);
+ const tagName = tagLinkMatch?.[1] || tag;
+
+ // Extract published date
+ const dateMatch = html.match(/<relative-time[^>]*datetime="([^"]*)"[^>]*>/);
+ const publishedAt = dateMatch?.[1] || "";
+
+ // 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() || "";
return {
repo: {
owner,
name: repo,
},
- releases,
+ release: {
+ tagName,
+ title,
+ publishedAt,
+ bodyHtml,
+ },
};
}
diff --git a/cgithub/static/style.css b/cgithub/static/style.css
index fdf3285..896f138 100644
--- a/cgithub/static/style.css
+++ b/cgithub/static/style.css
@@ -232,6 +232,10 @@ pre.box {
font-size: 0.8rem;
}
+.release-nav {
+ margin-bottom: 1rem;
+}
+
.release {
margin-bottom: 2rem;
}
diff --git a/cgithub/views/_release.eta b/cgithub/views/_release.eta
new file mode 100644
index 0000000..85126a2
--- /dev/null
+++ b/cgithub/views/_release.eta
@@ -0,0 +1,23 @@
+<div class="box release">
+ <div class="release-header">
+ <h3>
+ <% if (it.linkToDetail) { %>
+ <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/releases/tag/<%= it.release.tagName %>">
+ <%= it.release.title %>
+ </a>
+ <% } else { %>
+ <%= it.release.title %>
+ <% } %>
+ </h3>
+ <% if (it.release.publishedAt) { %>
+ <time class="timestamp" title="<%= it.release.publishedAt %>">
+ <%= it.release.publishedAt.split("T")[0] %>
+ </time>
+ <% } %>
+ </div>
+ <% if (it.release.bodyHtml) { %>
+ <div class="release-body">
+ <%~ it.release.bodyHtml %>
+ </div>
+ <% } %>
+</div>
diff --git a/cgithub/views/release.eta b/cgithub/views/release.eta
new file mode 100644
index 0000000..b8c0fe6
--- /dev/null
+++ b/cgithub/views/release.eta
@@ -0,0 +1,7 @@
+<% layout("./layout.eta") %>
+
+<div class="release-nav">
+ <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/releases">← All releases</a>
+</div>
+
+<%~ include("./_release.eta", { repo: it.repo, release: it.release, linkToDetail: false }) %>
diff --git a/cgithub/views/releases.eta b/cgithub/views/releases.eta
index 39850d8..b3cb254 100644
--- a/cgithub/views/releases.eta
+++ b/cgithub/views/releases.eta
@@ -3,23 +3,5 @@
<h2>Releases</h2>
<% for (const release of it.releases) { %>
- <div class="box release">
- <div class="release-header">
- <h3>
- <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/releases/tag/<%= release.tagName %>">
- <%= release.title %>
- </a>
- </h3>
- <% if (release.publishedAt) { %>
- <time class="timestamp" title="<%= release.publishedAt %>">
- <%= release.publishedAt.split("T")[0] %>
- </time>
- <% } %>
- </div>
- <% if (release.bodyHtml) { %>
- <div class="release-body">
- <%~ release.bodyHtml %>
- </div>
- <% } %>
- </div>
+ <%~ include("./_release.eta", { repo: it.repo, release, linkToDetail: true }) %>
<% } %>