Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions action.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

117 changes: 68 additions & 49 deletions source/plugins/base/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
export default async function({login, graphql, rest, data, q, queries, imports, callbacks}, conf) {
//Load inputs
console.debug(`metrics/compute/${login}/base > started`)
let {indepth, hireable, skip, "repositories.forks": _forks, "repositories.affiliations": _affiliations, "repositories.batch": _batch} = imports.metadata.plugins.base.inputs({data, q, account: "bypass"})
let {indepth, hireable, skip, "repositories.forks": _forks, "repositories.affiliations": _affiliations, "repositories.owned": _owned, "repositories.batch": _batch} = imports.metadata.plugins.base.inputs({data, q, account: "bypass"})
const repositories = conf.settings.repositories || 100
const forks = _forks ? "" : ", isFork: false"
const affiliations = _affiliations?.length ? `, ownerAffiliations: [${_affiliations.map(x => x.toLocaleUpperCase()).join(", ")}]${conf.authenticated === login ? `, affiliations: [${_affiliations.map(x => x.toLocaleUpperCase()).join(", ")}]` : ""}` : ""
Expand Down Expand Up @@ -40,7 +40,7 @@ export default async function({login, graphql, rest, data, q, queries, imports,
console.debug(`metrics/compute/${login}/base > failed to load bulk query, falling back to unit queries`)
//Query basic fields
const fields = {
user: ["packages", "starredRepositories", "watching", "sponsorshipsAsSponsor", "sponsorshipsAsMaintainer", "followers", "following", "issueComments", "organizations", "repositoriesContributedTo(includeUserRepositories: true)"],
user: ["packages", "starredRepositories", "watching", "sponsorshipsAsSponsor", "sponsorshipsAsMaintainer", "followers", "following", "issueComments", "organizations", `repositoriesContributedTo(includeUserRepositories: ${_owned})`],
organization: ["packages", "sponsorshipsAsSponsor", "sponsorshipsAsMaintainer", "membersWithRole"],
Comment on lines +43 to 44
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new repositories.owned input is used here to control includeUserRepositories in unit field queries and in the indepth lifetime aggregation, but the bulk BaseUserX query (source/plugins/base/queries/user.x.graphql:30) still hardcodes repositoriesContributedTo(includeUserRepositories: true). This means that when indepth mode is disabled and the bulk query succeeds, toggling repositories_owned has no effect on the "Contributed to X repositories" count; to align behaviour with the option description, the bulk query should also be parameterised to respect _owned.

Copilot uses AI. Check for mistakes.
}[account] ?? []
for (const field of fields) {
Expand All @@ -53,28 +53,24 @@ export default async function({login, graphql, rest, data, q, queries, imports,
}
}
//Query repositories fields
for (const field of ["totalCount", "totalDiskUsage"]) {
try {
Object.assign(data.user.repositories, (await graphql(queries.base["field.repositories"]({login, account, field, affiliations})))[account].repositories)
}
catch (error) {
console.debug(`metrics/compute/${login}/base > failed to retrieve repositories.${field}`)
data.user.repositories[field] = NaN
}
try {
Object.assign(data.user.repositories, (await graphql(queries.base["field.repositories"]({ login, account, affiliations })))[account].repositories)
}
catch {
console.debug(`metrics/compute/${login}/base > failed to retrieve repositories count and disk usage`)
data.user.repositories["totalCount"] = NaN
data.user.repositories["totalDiskUsage"] = NaN
}
//Query user account fields
if (account === "user") {
//Query contributions collection
{
const fields = ["totalRepositoriesWithContributedCommits", "totalCommitContributions", "restrictedContributionsCount", "totalIssueContributions", "totalPullRequestContributions", "totalPullRequestReviewContributions"]
for (const field of fields) {
try {
Object.assign(data.user.contributionsCollection, (await graphql(queries.base.contributions({login, account, field, range: ""})))[account].contributionsCollection)
}
catch {
console.debug(`metrics/compute/${login}/base > failed to retrieve contributionsCollection.${field}`)
data.user.contributionsCollection[field] = NaN
}
try {
Object.assign(data.user.contributionsCollection, (await graphql(queries.base.contributions({ login, account, range: "", indepthFields: ""})))[account].contributionsCollection)
}
catch {
console.debug(`metrics/compute/${login}/base > failed to retrieve contributionsCollection`)
data.user.contributionsCollection ??= {}
}
}
//Query calendar
Expand All @@ -90,39 +86,62 @@ export default async function({login, graphql, rest, data, q, queries, imports,
//Query contributions collection over account lifetime instead of last year
if (account === "user") {
if ((indepth) && (imports.metadata.plugins.base.extras("indepth", {...conf.settings, error: false}))) {
const fields = ["totalRepositoriesWithContributedCommits", "totalCommitContributions", "restrictedContributionsCount", "totalIssueContributions", "totalPullRequestContributions", "totalPullRequestReviewContributions"]
const start = new Date(data.user.createdAt)
const end = new Date()
const collection = {}
for (const field of fields) {
collection[field] = 0
//Load contribution calendar
for (let from = new Date(start); from < end;) {
//Set date range
let to = new Date(from)
to.setUTCHours(+6 * 4 * 7 * 24)
if (to > end)
to = end
//Ensure that date ranges are not overlapping by setting it to previous day at 23:59:59.999
const dto = new Date(to)
dto.setUTCHours(-1)
dto.setUTCMinutes(59)
dto.setUTCSeconds(59)
dto.setUTCMilliseconds(999)
//Fetch data from api
try {
console.debug(`metrics/compute/${login}/plugins > base > loading contributions collections for ${field} from "${from.toISOString()}" to "${dto.toISOString()}"`)
const {[account]: {contributionsCollection}} = await graphql(queries.base.contributions({login, account, field, range: `(from: "${from.toISOString()}", to: "${dto.toISOString()}")`}))
collection[field] += contributionsCollection[field]
}
catch {
console.debug(`metrics/compute/${login}/plugins > base > failed to load contributions collections for ${field} from "${from.toISOString()}" to "${dto.toISOString()}"`)
}
//Set next date range start
from = new Date(to)
const contributions = []
const indepthExtraFields = ["commitContributionsByRepository", "pullRequestContributionsByRepository", "issueContributionsByRepository", "pullRequestReviewContributionsByRepository"]
const totalReposContributed = new Set()
const lifetimeCollection = {}
//Load contribution calendar
for (let from = new Date(start); from < end;) {
//Set date range
let to = new Date(from)
to.setUTCHours(+6 * 4 * 7 * 24)
if (to > end)
to = end
//Ensure that date ranges are not overlapping by setting it to previous day at 23:59:59.999
const dto = new Date(to)
dto.setUTCHours(-1)
dto.setUTCMinutes(59)
dto.setUTCSeconds(59)
dto.setUTCMilliseconds(999)
//Fetch data from api
try {
console.debug(`metrics/compute/${login}/plugins > base > loading contributions collections from "${from.toISOString()}" to "${dto.toISOString()}"`)
const extraFieldsWithParams = indepthExtraFields.map(x => `${x} { repository { nameWithOwner } }`).join('\n')
const { [account]: { contributionsCollection } } = await graphql(queries.base.contributions({ login, account, indepthFields: extraFieldsWithParams, range: `(from: "${from.toISOString()}", to: "${dto.toISOString()}")` }))
contributions.push(contributionsCollection)
}
catch {
console.debug(`metrics/compute/${login}/plugins > base > failed to load contributions collections from "${from.toISOString()}" to "${dto.toISOString()}"`)
}
data.user.contributionsCollection[field] = Math.max(collection[field], data.user.contributionsCollection[field])
//Set next date range start
from = new Date(to)
}

for (const contribution of contributions) {
for (const field in contribution) {
if (indepthExtraFields.includes(field)) {
for (const repo of contribution[field]) {
const nameWithOwner = repo.repository.nameWithOwner
if (!_owned && nameWithOwner.split('/')[0] === login) continue
totalReposContributed.add(nameWithOwner)
}
} else {
lifetimeCollection[field] ??= 0
lifetimeCollection[field] += contribution[field]
}
Comment on lines +122 to +133
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the indepth lifetime aggregation, you accumulate each range's numeric fields into data.user.contributionsCollection[field] using +=, but this object has already been populated earlier from the bulk or fallback contributions query, so totals like totalCommitContributions and totalIssueContributions will be double-counted (once from the initial query, again from the lifetime ranges). To avoid inflated statistics, either base the lifetime totals on a fresh accumulator and assign them at the end, or overwrite the existing values (or use a Math.max-style comparison as in the previous implementation) rather than incrementing them on top of the last-year snapshot.

Copilot uses AI. Check for mistakes.
}
}

for (const field in lifetimeCollection) {
data.user.contributionsCollection[field] = Math.max(
lifetimeCollection[field],
data.user.contributionsCollection[field] ?? 0
)
}

Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data.user.repositoriesContributedTo may be undefined when this indepth lifetime contributions path runs, because postprocess.user does not initialise this property and both the bulk BaseUserX query and the fallback unit repositoriesContributedTo query can fail. In that case, assigning to data.user.repositoriesContributedTo.totalCount will throw at runtime; consider initialising data.user.repositoriesContributedTo to a safe default (for example mirroring the structure used in postprocess.organization/postprocess.skip) before writing totalCount.

Suggested change
data.user.repositoriesContributedTo ??= { totalCount: 0 }

Copilot uses AI. Check for mistakes.
data.user.repositoriesContributedTo.totalCount = totalReposContributed.size
}
//Fallback to load whole commit history rather than last year
else {
Expand All @@ -146,7 +165,7 @@ export default async function({login, graphql, rest, data, q, queries, imports,
//Iterate through repositories
let cursor = null
let pushed = 0
const options = {repositories: {forks, affiliations, constraints: ""}, repositoriesContributedTo: {forks: "", affiliations: "", constraints: ", includeUserRepositories: false, contributionTypes: COMMIT"}}[type] ?? null
const options = {repositories: {forks, affiliations, constraints: ""}, repositoriesContributedTo: {forks: "", affiliations: "", constraints: `, includeUserRepositories: ${_owned}, contributionTypes: COMMIT`}}[type] ?? null
data.user[type] = data.user[type] ?? {}
data.user[type].nodes = data.user[type].nodes ?? []
do {
Expand Down
6 changes: 6 additions & 0 deletions source/plugins/base/metadata.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ inputs:
type: boolean
default: no

repositories_owned:
description: |
Include your own repositories in the "Contributed to X repositories" count
type: boolean
default: yes
Comment on lines +94 to +98
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new repositories_owned input is defined here but does not yet appear in the base plugin README options table (which currently documents neighbouring inputs such as repositories_forks and repositories_affiliations). To keep user-facing configuration documentation in sync with available inputs, consider adding a README entry or regenerating the plugin docs from this metadata.

Copilot uses AI. Check for mistakes.

repositories_affiliations:
description: |
Repositories affiliations
Expand Down
10 changes: 7 additions & 3 deletions source/plugins/base/queries/contributions.graphql
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
query BaseContributions {
user(login: "$login") {
contributionsCollection$range {
$field
totalRepositoriesWithContributedCommits
totalCommitContributions
restrictedContributionsCount
totalIssueContributions
totalPullRequestContributions
totalPullRequestReviewContributions
$indepthFields
}
}
}


5 changes: 3 additions & 2 deletions source/plugins/base/queries/field.repositories.graphql
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
query BaseFieldRepositories{
$account(login: "$login") {
repositories(last: 0 $affiliations) {
$field
totalCount
totalDiskUsage
}
}
}
}
Loading