From c6bc3c83a95d8d178089951488b06438c2002077 Mon Sep 17 00:00:00 2001 From: Federico Grandi Date: Wed, 29 Oct 2025 12:18:52 +0000 Subject: [PATCH 1/3] Filter event types before parsing them See https://github.com/lowlighter/metrics/discussions/1759 --- source/plugins/activity/index.mjs | 94 ++++++++++++++++++++----------- 1 file changed, 60 insertions(+), 34 deletions(-) diff --git a/source/plugins/activity/index.mjs b/source/plugins/activity/index.mjs index fcebc868404..e6893cd5b74 100644 --- a/source/plugins/activity/index.mjs +++ b/source/plugins/activity/index.mjs @@ -7,11 +7,11 @@ export default async function({login, data, rest, q, account, imports}, {enabled return null //Context - let context = {mode: "user"} + let context = {mode:"user"} if (q.repo) { console.debug(`metrics/compute/${login}/plugins > activity > switched to repository mode`) - const {owner, repo} = data.user.repositories.nodes.map(({name: repo, owner: {login: owner}}) => ({repo, owner})).shift() - context = {...context, mode: "repository", owner, repo} + const {owner, repo} = data.user.repositories.nodes.map(({name:repo, owner:{login:owner}}) => ({repo, owner})).shift() + context = {...context, mode:"repository", owner, repo} } //Load inputs @@ -29,7 +29,7 @@ export default async function({login, data, rest, q, account, imports}, {enabled try { for (let page = 1; page <= pages; page++) { console.debug(`metrics/compute/${login}/plugins > activity > loading page ${page}/${pages}`) - events.push(...(context.mode === "repository" ? await rest.activity.listRepoEvents({owner: context.owner, repo: context.repo}) : await rest.activity.listEventsForAuthenticatedUser({username: login, per_page: 100, page})).data) + events.push(...(context.mode === "repository" ? await rest.activity.listRepoEvents({owner:context.owner, repo:context.repo}) : await rest.activity.listEventsForAuthenticatedUser({username:login, per_page:100, page})).data) } } catch { @@ -37,126 +37,153 @@ export default async function({login, data, rest, q, account, imports}, {enabled } console.debug(`metrics/compute/${login}/plugins > activity > ${events.length} events loaded`) + const payloadTypesToCustomTypes = { + CommitCommentEvent:"comment", + CreateEvent:"ref/create", + DeleteEvent:"ref/delete", + ForkEvent:"fork", + GollumEvent:"wiki", + IssueCommentEvent:"comment", + IssuesEvent:"issue", + MemberEvent:"member", + PublicEvent:"public", + PullRequestEvent:"pr", + PullRequestReviewEvent:"review", + PullRequestReviewCommentEvent:"comment", + PushEvent:"push", + ReleaseEvent:"release", + WatchEvent:"star", + } + //Extract activity events const activity = (await Promise.all( events .filter(({actor}) => account === "organization" ? true : actor.login?.toLocaleLowerCase() === login.toLocaleLowerCase()) .filter(({created_at}) => Number.isFinite(days) ? new Date(created_at) > new Date(Date.now() - days * 24 * 60 * 60 * 1000) : true) .filter(event => visibility === "public" ? event.public : true) - .map(async ({type, payload, actor: {login: actor}, repo: {name: repo}, created_at}) => { + .map(event => ({event, customType:payloadTypesToCustomTypes[event.type]})) + .filter(({customType}) => !!customType) //Ignore events with an unknown type + .filter(({customType}) => filter.includes("all") || filter.includes(customType)) //Filter events based on user preference + .map(async ({event}) => event) //Discard customType, it will be re-assigned + .map(async ({type, payload, actor:{login:actor}, repo:{name:repo}, created_at}) => { //See https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/github-event-types const timestamp = new Date(created_at) if (!imports.filters.repo(repo, skipped)) return null + + //Get custom type from the previously delcared mapping, so that it acts as a single source of truth + const customType = payloadTypesToCustomTypes[type] + if (!customType) throw new Error(`Missing event mapping for type: ${type}`) + switch (type) { //Commented on a commit case "CommitCommentEvent": { if (!["created"].includes(payload.action)) return null - const {comment: {user: {login: user}, commit_id: sha, body: content}} = payload + const {comment:{user:{login:user}, commit_id:sha, body:content}} = payload if (!imports.filters.text(user, ignored)) return null - return {type: "comment", on: "commit", actor, timestamp, repo, content: await imports.markdown(content, {mode: markdown, codelines}), user, mobile: null, number: sha.substring(0, 7), title: ""} + return {type:customType, on:"commit", actor, timestamp, repo, content:await imports.markdown(content, {mode:markdown, codelines}), user, mobile:null, number:sha.substring(0, 7), title:""} } //Created a git branch or tag case "CreateEvent": { - const {ref: name, ref_type: type} = payload - return {type: "ref/create", actor, timestamp, repo, ref: {name, type}} + const {ref:name, ref_type:type} = payload + return {type:customType, actor, timestamp, repo, ref:{name, type}} } //Deleted a git branch or tag case "DeleteEvent": { - const {ref: name, ref_type: type} = payload - return {type: "ref/delete", actor, timestamp, repo, ref: {name, type}} + const {ref:name, ref_type:type} = payload + return {type:customType, actor, timestamp, repo, ref:{name, type}} } //Forked repository case "ForkEvent": { - const {forkee: {full_name: forked}} = payload - return {type: "fork", actor, timestamp, repo, forked} + const {forkee:{full_name:forked}} = payload + return {type:customType, actor, timestamp, repo, forked} } //Wiki changes case "GollumEvent": { const {pages} = payload - return {type: "wiki", actor, timestamp, repo, pages: pages.map(({title}) => title)} + return {type:customType, actor, timestamp, repo, pages:pages.map(({title}) => title)} } //Commented on an issue case "IssueCommentEvent": { if (!["created"].includes(payload.action)) return null - const {issue: {user: {login: user}, title, number}, comment: {body: content, performed_via_github_app: mobile}} = payload + const {issue:{user:{login:user}, title, number}, comment:{body:content, performed_via_github_app:mobile}} = payload if (!imports.filters.text(user, ignored)) return null - return {type: "comment", on: "issue", actor, timestamp, repo, content: await imports.markdown(content, {mode: markdown, codelines}), user, mobile, number, title} + return {type:customType, on:"issue", actor, timestamp, repo, content:await imports.markdown(content, {mode:markdown, codelines}), user, mobile, number, title} } //Issue event case "IssuesEvent": { if (!["opened", "closed", "reopened"].includes(payload.action)) return null - const {action, issue: {user: {login: user}, title, number, body: content}} = payload + const {action, issue:{user:{login:user}, title, number, body:content}} = payload if (!imports.filters.text(user, ignored)) return null - return {type: "issue", actor, timestamp, repo, action, user, number, title, content: await imports.markdown(content, {mode: markdown, codelines})} + return {type:customType, actor, timestamp, repo, action, user, number, title, content:await imports.markdown(content, {mode:markdown, codelines})} } //Activity from repository collaborators case "MemberEvent": { if (!["added"].includes(payload.action)) return null - const {member: {login: user}} = payload + const {member:{login:user}} = payload if (!imports.filters.text(user, ignored)) return null - return {type: "member", actor, timestamp, repo, user} + return {type:customType, actor, timestamp, repo, user} } //Made repository public case "PublicEvent": { - return {type: "public", actor, timestamp, repo} + return {type:customType, actor, timestamp, repo} } //Pull requests events case "PullRequestEvent": { if (!["opened", "closed"].includes(payload.action)) return null - const {action, pull_request: {user: {login: user}, title, number, body: content, additions: added, deletions: deleted, changed_files: changed, merged}} = payload + const {action, pull_request:{user:{login:user}, title, number, body:content, additions:added, deletions:deleted, changed_files:changed, merged}} = payload if (!imports.filters.text(user, ignored)) return null - return {type: "pr", actor, timestamp, repo, action: (action === "closed") && (merged) ? "merged" : action, user, title, number, content: await imports.markdown(content, {mode: markdown, codelines}), lines: {added, deleted}, files: {changed}} + return {type:customType, actor, timestamp, repo, action:(action === "closed") && (merged) ? "merged" : action, user, title, number, content:await imports.markdown(content, {mode:markdown, codelines}), lines:{added, deleted}, files:{changed}} } //Reviewed a pull request case "PullRequestReviewEvent": { - const {review: {state: review}, pull_request: {user: {login: user}, number, title}} = payload + const {review:{state:review}, pull_request:{user:{login:user}, number, title}} = payload if (!imports.filters.text(user, ignored)) return null - return {type: "review", actor, timestamp, repo, review, user, number, title} + return {type:customType, actor, timestamp, repo, review, user, number, title} } //Commented on a pull request case "PullRequestReviewCommentEvent": { if (!["created"].includes(payload.action)) return null - const {pull_request: {user: {login: user}, title, number}, comment: {body: content, performed_via_github_app: mobile}} = payload + const {pull_request:{user:{login:user}, title, number}, comment:{body:content, performed_via_github_app:mobile}} = payload if (!imports.filters.text(user, ignored)) return null - return {type: "comment", on: "pr", actor, timestamp, repo, content: await imports.markdown(content, {mode: markdown, codelines}), user, mobile, number, title} + return {type:customType, on:"pr", actor, timestamp, repo, content:await imports.markdown(content, {mode:markdown, codelines}), user, mobile, number, title} } //Pushed commits case "PushEvent": { let {size, commits, ref} = payload - commits = commits.filter(({author: {email}}) => imports.filters.text(email, ignored)) + commits = commits.filter(({author:{email}}) => imports.filters.text(email, ignored)) if (!commits.length) return null if (commits.slice(-1).pop()?.message.startsWith("Merge branch ")) commits = commits.slice(-1) - return {type: "push", actor, timestamp, repo, size, branch: ref.match(/refs.heads.(?.*)/)?.groups?.branch ?? null, commits: commits.reverse().map(({sha, message}) => ({sha: sha.substring(0, 7), message}))} + return {type:customType, actor, timestamp, repo, size, branch:ref.match(/refs.heads.(?.*)/)?.groups?.branch ?? null, commits:commits.reverse().map(({sha, message}) => ({sha:sha.substring(0, 7), message}))} } //Released case "ReleaseEvent": { if (!["published"].includes(payload.action)) return null - const {action, release: {name, tag_name, prerelease, draft, body: content}} = payload - return {type: "release", actor, timestamp, repo, action, name: name || tag_name, prerelease, draft, content: await imports.markdown(content, {mode: markdown, codelines})} + const {action, release:{name, tag_name, prerelease, draft, body:content}} = payload + return {type:customType, actor, timestamp, repo, action, name:name || tag_name, prerelease, draft, content:await imports.markdown(content, {mode:markdown, codelines})} } //Starred a repository case "WatchEvent": { if (!["started"].includes(payload.action)) return null const {action} = payload - return {type: "star", actor, timestamp, repo, action} + return {type:customType, actor, timestamp, repo, action} } //Unknown event default: { @@ -166,11 +193,10 @@ export default async function({login, data, rest, q, account, imports}, {enabled }), )) .filter(event => event) - .filter(event => filter.includes("all") || filter.includes(event.type)) .slice(0, limit) //Results - return {timestamps, events: activity} + return {timestamps, events:activity} } //Handle errors catch (error) { From 8c95f5f2fbc427d8935b591a772dc8b172ff82ca Mon Sep 17 00:00:00 2001 From: Federico Grandi Date: Wed, 29 Oct 2025 12:47:26 +0000 Subject: [PATCH 2/3] Get commit data for PushEvent from GitHub API See https://github.com/lowlighter/metrics/discussions/1759 --- source/plugins/activity/index.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/source/plugins/activity/index.mjs b/source/plugins/activity/index.mjs index e6893cd5b74..e921080a365 100644 --- a/source/plugins/activity/index.mjs +++ b/source/plugins/activity/index.mjs @@ -163,12 +163,17 @@ export default async function({login, data, rest, q, account, imports}, {enabled } //Pushed commits case "PushEvent": { - let {size, commits, ref} = payload + let {size, ref, head, before} = payload + const [owner, repoName] = repo.split("/") + + let {commits} = await rest.compareCommitsWithBasehead({owner, repo:repoName, basehead:`${before}...${head}`}) + commits = commits.filter(({author:{email}}) => imports.filters.text(email, ignored)) if (!commits.length) return null if (commits.slice(-1).pop()?.message.startsWith("Merge branch ")) commits = commits.slice(-1) + return {type:customType, actor, timestamp, repo, size, branch:ref.match(/refs.heads.(?.*)/)?.groups?.branch ?? null, commits:commits.reverse().map(({sha, message}) => ({sha:sha.substring(0, 7), message}))} } //Released From deb1c308cdceaf0431647ae50079abfc34b4415d Mon Sep 17 00:00:00 2001 From: Federico Grandi Date: Wed, 29 Oct 2025 13:23:14 +0000 Subject: [PATCH 3/3] Misc fixes --- source/plugins/activity/index.mjs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/source/plugins/activity/index.mjs b/source/plugins/activity/index.mjs index e921080a365..85365dde47e 100644 --- a/source/plugins/activity/index.mjs +++ b/source/plugins/activity/index.mjs @@ -64,7 +64,7 @@ export default async function({login, data, rest, q, account, imports}, {enabled .map(event => ({event, customType:payloadTypesToCustomTypes[event.type]})) .filter(({customType}) => !!customType) //Ignore events with an unknown type .filter(({customType}) => filter.includes("all") || filter.includes(customType)) //Filter events based on user preference - .map(async ({event}) => event) //Discard customType, it will be re-assigned + .map(({event}) => event) //Discard customType, it will be re-assigned .map(async ({type, payload, actor:{login:actor}, repo:{name:repo}, created_at}) => { //See https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/github-event-types const timestamp = new Date(created_at) @@ -166,12 +166,13 @@ export default async function({login, data, rest, q, account, imports}, {enabled let {size, ref, head, before} = payload const [owner, repoName] = repo.split("/") - let {commits} = await rest.compareCommitsWithBasehead({owner, repo:repoName, basehead:`${before}...${head}`}) + const res = await rest.repos.compareCommitsWithBasehead({owner, repo:repoName, basehead:`${before}...${head}`}) + let {commits} = res.data commits = commits.filter(({author:{email}}) => imports.filters.text(email, ignored)) if (!commits.length) return null - if (commits.slice(-1).pop()?.message.startsWith("Merge branch ")) + if (commits.slice(-1).pop()?.commit.message.startsWith("Merge branch ")) commits = commits.slice(-1) return {type:customType, actor, timestamp, repo, size, branch:ref.match(/refs.heads.(?.*)/)?.groups?.branch ?? null, commits:commits.reverse().map(({sha, message}) => ({sha:sha.substring(0, 7), message}))}