@rmdes/indiekit-endpoint-github 1.0.4 → 1.0.5
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/index.js +2 -0
- package/lib/controllers/changelog.js +166 -0
- package/lib/github-client.js +7 -2
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
|
|
3
3
|
import { activityController } from "./lib/controllers/activity.js";
|
|
4
|
+
import { changelogController } from "./lib/controllers/changelog.js";
|
|
4
5
|
import { commitsController } from "./lib/controllers/commits.js";
|
|
5
6
|
import { contributionsController } from "./lib/controllers/contributions.js";
|
|
6
7
|
import { dashboardController } from "./lib/controllers/dashboard.js";
|
|
@@ -85,6 +86,7 @@ export default class GitHubEndpoint {
|
|
|
85
86
|
publicRouter.get("/api/contributions", contributionsController.api);
|
|
86
87
|
publicRouter.get("/api/activity", activityController.api);
|
|
87
88
|
publicRouter.get("/api/featured", featuredController.api);
|
|
89
|
+
publicRouter.get("/api/changelog", changelogController.api);
|
|
88
90
|
|
|
89
91
|
return publicRouter;
|
|
90
92
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { GitHubClient } from "../github-client.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Categorize a repo by its name prefix
|
|
5
|
+
* @param {string} name - Repository name
|
|
6
|
+
* @returns {string} - Category key
|
|
7
|
+
*/
|
|
8
|
+
function categorizeRepo(name) {
|
|
9
|
+
if (name === "indiekit") return "core";
|
|
10
|
+
if (name === "indiekit-cloudron" || name === "indiekit-deploy")
|
|
11
|
+
return "deployment";
|
|
12
|
+
if (name.includes("theme")) return "theme";
|
|
13
|
+
if (name.startsWith("indiekit-endpoint-")) return "endpoints";
|
|
14
|
+
if (name.startsWith("indiekit-syndicator-")) return "syndicators";
|
|
15
|
+
if (name.startsWith("indiekit-post-type-")) return "post-types";
|
|
16
|
+
if (name.startsWith("indiekit-preset-")) return "presets";
|
|
17
|
+
return "other";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const CATEGORY_LABELS = {
|
|
21
|
+
core: "Core",
|
|
22
|
+
deployment: "Deployment",
|
|
23
|
+
theme: "Theme",
|
|
24
|
+
endpoints: "Endpoints",
|
|
25
|
+
syndicators: "Syndicators",
|
|
26
|
+
"post-types": "Post Types",
|
|
27
|
+
presets: "Presets",
|
|
28
|
+
other: "Other",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Split an array into chunks of a given size
|
|
33
|
+
* @param {Array} array
|
|
34
|
+
* @param {number} size
|
|
35
|
+
* @returns {Array<Array>}
|
|
36
|
+
*/
|
|
37
|
+
function chunks(array, size) {
|
|
38
|
+
const result = [];
|
|
39
|
+
for (let i = 0; i < array.length; i += size) {
|
|
40
|
+
result.push(array.slice(i, i + size));
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Changelog API controller
|
|
47
|
+
* @type {import("express").RequestHandler}
|
|
48
|
+
*/
|
|
49
|
+
export const changelogController = {
|
|
50
|
+
async api(request, response, next) {
|
|
51
|
+
try {
|
|
52
|
+
const { username, token, cacheTtl } =
|
|
53
|
+
request.app.locals.application.githubConfig;
|
|
54
|
+
|
|
55
|
+
if (!username) {
|
|
56
|
+
return response.status(400).json({ error: "No username configured" });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const client = new GitHubClient({ token, cacheTtl });
|
|
60
|
+
|
|
61
|
+
// Parse days param — default 30, "all" for unlimited
|
|
62
|
+
const daysParam = request.query.days || "30";
|
|
63
|
+
let sinceDate = null;
|
|
64
|
+
let daysValue = daysParam;
|
|
65
|
+
|
|
66
|
+
if (daysParam !== "all") {
|
|
67
|
+
const days = Number.parseInt(daysParam, 10);
|
|
68
|
+
if (Number.isNaN(days) || days < 1) {
|
|
69
|
+
return response
|
|
70
|
+
.status(400)
|
|
71
|
+
.json({ error: "Invalid days parameter" });
|
|
72
|
+
}
|
|
73
|
+
daysValue = days;
|
|
74
|
+
const since = new Date();
|
|
75
|
+
since.setDate(since.getDate() - days);
|
|
76
|
+
sinceDate = since.toISOString();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Fetch all user repos (100 to cover all indiekit repos)
|
|
80
|
+
const repos = await client.getUserRepos(username, 100, "pushed");
|
|
81
|
+
|
|
82
|
+
if (!Array.isArray(repos)) {
|
|
83
|
+
return response.json({
|
|
84
|
+
commits: [],
|
|
85
|
+
categories: {},
|
|
86
|
+
totalCommits: 0,
|
|
87
|
+
days: daysValue,
|
|
88
|
+
generatedAt: new Date().toISOString(),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Filter to indiekit repos only
|
|
93
|
+
const indiekitRepos = repos.filter((repo) =>
|
|
94
|
+
repo.name.includes("indiekit"),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Build categories map from discovered repos
|
|
98
|
+
const categories = {};
|
|
99
|
+
for (const repo of indiekitRepos) {
|
|
100
|
+
const cat = categorizeRepo(repo.name);
|
|
101
|
+
if (!categories[cat]) {
|
|
102
|
+
categories[cat] = {
|
|
103
|
+
label: CATEGORY_LABELS[cat] || cat,
|
|
104
|
+
repos: [],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
categories[cat].repos.push(repo.name);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Fetch commits in batches of 5 to avoid secondary rate limits
|
|
111
|
+
const allCommits = [];
|
|
112
|
+
for (const batch of chunks(indiekitRepos, 5)) {
|
|
113
|
+
const results = await Promise.all(
|
|
114
|
+
batch.map(async (repo) => {
|
|
115
|
+
try {
|
|
116
|
+
const commits = await client.getRepoCommits(
|
|
117
|
+
repo.owner.login,
|
|
118
|
+
repo.name,
|
|
119
|
+
20,
|
|
120
|
+
sinceDate,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
if (!Array.isArray(commits)) return [];
|
|
124
|
+
|
|
125
|
+
// Filter out merge commits and map to response shape
|
|
126
|
+
return commits
|
|
127
|
+
.filter((c) => (c.parents?.length || 0) <= 1)
|
|
128
|
+
.map((c) => ({
|
|
129
|
+
sha: c.sha.slice(0, 7),
|
|
130
|
+
fullSha: c.sha,
|
|
131
|
+
title: c.commit.message.split("\n")[0],
|
|
132
|
+
body: c.commit.message.split("\n").slice(1).join("\n").trim(),
|
|
133
|
+
url: c.html_url,
|
|
134
|
+
date:
|
|
135
|
+
c.commit.author?.date || c.commit.committer?.date || null,
|
|
136
|
+
author: c.commit.author?.name || "Unknown",
|
|
137
|
+
repo: repo.full_name,
|
|
138
|
+
repoName: repo.name,
|
|
139
|
+
repoUrl: repo.html_url,
|
|
140
|
+
category: categorizeRepo(repo.name),
|
|
141
|
+
}));
|
|
142
|
+
} catch {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
allCommits.push(...results.flat());
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Sort by date descending
|
|
151
|
+
allCommits.sort(
|
|
152
|
+
(a, b) => new Date(b.date || 0) - new Date(a.date || 0),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
response.json({
|
|
156
|
+
commits: allCommits,
|
|
157
|
+
categories,
|
|
158
|
+
totalCommits: allCommits.length,
|
|
159
|
+
days: daysValue,
|
|
160
|
+
generatedAt: new Date().toISOString(),
|
|
161
|
+
});
|
|
162
|
+
} catch (error) {
|
|
163
|
+
next(error);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
};
|
package/lib/github-client.js
CHANGED
|
@@ -71,10 +71,15 @@ export class GitHubClient {
|
|
|
71
71
|
* @param {string} owner - Repository owner
|
|
72
72
|
* @param {string} repo - Repository name
|
|
73
73
|
* @param {number} [limit] - Number of commits to fetch
|
|
74
|
+
* @param {string} [since] - ISO 8601 date string to filter commits after
|
|
74
75
|
* @returns {Promise<Array>} - Repository commits
|
|
75
76
|
*/
|
|
76
|
-
async getRepoCommits(owner, repo, limit = 10) {
|
|
77
|
-
|
|
77
|
+
async getRepoCommits(owner, repo, limit = 10, since = null) {
|
|
78
|
+
let endpoint = `/repos/${owner}/${repo}/commits?per_page=${limit}`;
|
|
79
|
+
if (since) {
|
|
80
|
+
endpoint += `&since=${since}`;
|
|
81
|
+
}
|
|
82
|
+
return this.fetch(endpoint);
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
/**
|
package/package.json
CHANGED