Skip to content
Merged
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
45 changes: 35 additions & 10 deletions .github/workflows/update-contributor-metrics.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@ on:
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
group: ${{ github.workflow }}
cancel-in-progress: false

permissions:
contents: write
pull-requests: write
issues: read
discussions: read

jobs:
update-contributor-metrics:
name: Compute contributor metrics and create PR
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 30

steps:
- name: Harden Runner
Expand All @@ -33,10 +35,33 @@ jobs:
with:
node-version: "24"

- name: Verify collector offline
run: node --test scripts/fetch-contributor-metrics.test.js

- name: Retain the newest verified refresh awaiting review
run: |
node <<'NODE'
const fs = require('node:fs');
const { execFileSync, spawnSync } = require('node:child_process');
const ref = 'refs/remotes/origin/chore/update-contributor-metrics';
const found = spawnSync('git', ['show-ref', '--verify', '--quiet', ref]);
if (found.status === 1) process.exit(0);
if (found.status !== 0) throw new Error('Could not inspect the pending refresh branch');
const file = 'static/data/contributor-metrics.json';
const pending = JSON.parse(execFileSync('git', ['show', `${ref}:${file}`], { encoding: 'utf8' }));
const current = JSON.parse(fs.readFileSync(file, 'utf8'));
const stamp = Date.parse(pending.generatedAt);
if (pending.schemaVersion === 2 && pending.coverage?.complete === true &&
Number.isFinite(stamp) && stamp <= Date.now() &&
stamp > (Date.parse(current.generatedAt) || 0)) {
fs.writeFileSync(file, JSON.stringify(pending, null, 2) + '\n');
}
NODE

- name: Compute contributor metrics
env:
GITHUB_TOKEN: ${{ github.token }}
run: node scripts/fetch-contributor-metrics.js
run: node scripts/fetch-contributor-metrics.js --force

- name: Check for contributor metrics dataset changes
id: check-changes
Expand All @@ -51,16 +76,16 @@ jobs:
fi

- name: Create or update PR with fresh contributor data
if: steps.check-changes.outputs.changed == 'true'
if: steps.check-changes.outputs.changed == 'true' && github.ref == 'refs/heads/main'
env:
GH_TOKEN: ${{ github.token }}
run: |
BRANCH="chore/update-contributor-metrics-$(date +%Y%m%d)"
BRANCH="chore/update-contributor-metrics"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b "$BRANCH"
git commit -m "chore(analytics): update contributor metrics cache"
git push -u origin "$BRANCH" --force
git commit -m "chore(analytics): refresh public contributor participation"
git push -u origin "$BRANCH" --force-with-lease

# Check if PR already exists before opening duplicate
EXISTING_PR=$(gh pr list --repo projectbluefin/documentation --head "$BRANCH" --state open --json number --jq '.[0].number')
Expand All @@ -71,6 +96,6 @@ jobs:
--repo projectbluefin/documentation \
--base main \
--head "$BRANCH" \
--title "chore(analytics): update contributor metrics cache" \
--body "Automated update of monthly contributor metrics, active cohorts, and activity streams. Requires maintainer approval before merge per repository policy."
--title "chore(analytics): refresh public contributor participation" \
--body "Refreshes the complete public GitHub participation snapshot. Known bots and imported fork commit history are excluded; failed collection preserves last-good history and marks unavailability. Requires maintainer approval before merge."
fi
41 changes: 31 additions & 10 deletions docs/analytics.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,34 @@ See the [Homebrew analytics documentation](https://docs.brew.sh/Analytics) for a

### Contributor Metrics Methodology

Contributor analytics adopt a CNCF DevStats and LFX-derived methodology:

- **4 summary metrics and 3 trend charts:** Tracking monthly active human contributors (merged PR authors), first-time contributors, pull request merge rate, and human throughput alongside historical trends for contributor cohorts, activity breakdowns, and PR velocity.
- **Public GitHub dataset with automated bot and AI agent filtering:** Metrics are derived from public GitHub activity across the entire `projectbluefin` organization while strictly filtering automated bots, dependency managers, and autonomous AI agents to ensure counts represent real human open-source participation.
- **Organization-wide activity streams:** Activity streams track merged pull requests and issues across all public repositories in `projectbluefin` on a shared domain.
- **Community forum discussions:** Community discussions are hosted at [ublue-os/bluefin](https://github.com/ublue-os/bluefin/discussions).

For real-time development and organizational velocity metrics across all 15 factory repositories, see the [Factory metrics view](/factory/metrics).

Upstream repository metrics for `projectbluefin/bluefin` are also tracked on [Linux Foundation Insights](https://insights.linuxfoundation.org/project/ublue-os-bluefin).
These charts measure participation, not pull request throughput. A contributor is
one identifiable GitHub account with a recorded public contribution in a repository
currently owned by `projectbluefin`. Each account is counted once across the organization.

- **Ways to contribute:** code and documentation changes, pull requests, issues,
comments, submitted reviews, and GitHub Discussions, including replies. Changes
do not have to be merged to recognize their authors.
- **Repository coverage:** all public organization repositories, including archived
repositories and forks. Commit authors are read from non-merge commits on non-fork
default branches; imported fork commit history is excluded. Other participation
in forks is included. Recorded history may predate a repository joining the org.
- **Community growth:** the cumulative number of accounts by their first recorded
contribution date. This is surviving public history, not a claim to recover deleted
contributions or activity in private repositories.
- **Recent participation:** distinct accounts active in the last 90 days. Monthly
counts use UTC calendar months; the current month is incomplete. The same person
may help in multiple ways, so the contribution-type counts must not be added together.
- **Identity limits:** known bots and bot account types are excluded. Accounts that
cannot be linked to a public GitHub identity are not counted. Public GitHub data
cannot establish whether an ordinary account used AI assistance, or whether two
accounts belong to the same person.
- **Freshness and gaps:** the scheduled collector follows every required page of
the original public GitHub APIs. A failed collection retains the last successful
snapshot and its timestamp, with a visible reason rather than a partial total.

Sources: GitHub's [repository API](https://docs.github.com/en/rest/repos/repos#list-organization-repositories),
[GraphQL repository reference](https://docs.github.com/en/graphql/reference/objects#repository),
and [pagination documentation](https://docs.github.com/en/graphql/guides/using-pagination-in-the-graphql-api).
The precise local definition is implemented in `scripts/fetch-contributor-metrics.js`.
Community forum discussions at [ublue-os/bluefin](https://github.com/ublue-os/bluefin/discussions)
are outside this organization and are not mixed into its totals.
51 changes: 28 additions & 23 deletions docs/skills/factory-dashboard-content.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,29 +164,34 @@ authorization to it.

### Contributor analytics: methodology, scoping, and data integrity

The `/analytics` page presents contributor analytics alongside countme metrics.
Data is generated by `scripts/fetch-contributor-metrics.js` and rendered via
`src/components/analytics/ContributorMetricsCharts.tsx`. When maintaining or
updating contributor analytics content and charts, follow these conventions:

- **In-flight month refetching window:** Contributor data pipelines must refetch
the current in-flight month AND the immediately preceding calendar month.
Month-end tails can have landing events delayed by timezone differences or
late pushes; freezing too early cuts off true activity. A calendar month is
only frozen after it has completely elapsed and no pagination truncation occurred.
- **Tier-1 scope for activity streams:** Activity streams (Code & Builds, Code Reviews,
Issues & Triage, and Documentation) track the 9 tier-1 core repositories on a
shared domain. Do not dilute activity stream metrics across low-velocity forks
or peripheral repos.
- **Bot and autonomous agent exclusion patterns:** Machine automation must not
be counted as human contribution. Pipelines strictly filter bot patterns
(`[bot]`, `kubestellar-hive`, `web-flow`, `github-actions`) and autonomous AI
agents. Where automation PRs or workflows are reported, keep them in distinct
channels separate from human contributor counts.
- **Truncation guards over zero-coercion:** If GitHub API pagination or rate
limits are tripped during metrics harvesting, the affected stream must record
`null` (rendering as unavailable / accumulating data) rather than coercing
to `0` or publishing a misleading partial undercount.
`/analytics` reads the tracked schema-2 snapshot generated by
`scripts/fetch-contributor-metrics.js`. The source is public GitHub resource
history, not a leaderboard, monthly report snapshots, or hosted Hive.

- **Count account unions, not events or per-repo sums.** The total is the union
of stable GitHub account IDs across every discovered public repository. Include
archived repositories and native activity in forks; exclude imported fork git
history. Keep the identity records in the dataset so the total is auditable.
- **Recognize non-code work.** Issue authors, commenters, reviewers and discussion
participants count alongside change authors. Documentation uses the same change
pipeline as code; a repository name does not establish that every change is docs.
- **Use one source contract end to end.** Summary values, cumulative history,
monthly participation, offline render fixtures, tables and chart options must
come from the same schema. Do not synthesize missing fields, add a ratio fallback,
or advertise a series with no producer.
- **Make identity limits visible.** Exclude canonical known-bot patterns and bot
account types, not guessed employers or profile bios. An ordinary account may
use AI. Deleted or unlinked identities are not invented people.
- **Require complete pagination.** Follow all repository, resource and nested
comment/review/reply cursors. A failed page invalidates the refreshed snapshot;
keep the last good history and `generatedAt` with visible unavailability.
- **Do not sum overlapping participation.** Each contribution-type figure is a
distinct-account count; one account may appear in several types. Monthly active
counts are not cumulative growth. The cumulative curve uses first observed
dates from full surviving public history, including pre-org repository history.
- **Verify the actual surface.** Check both themes and narrow layouts, a verified
zero, missing data and too few points. `EChart` owns the accessible numbers table
and accumulating state; component tests should exercise it, not a mocked echo.

### CountMe: first-party stream counts only

Expand Down
41 changes: 41 additions & 0 deletions docs/skills/github-api-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,45 @@ instead of restating a token read or a header object.
If truncation or rate limiting is tripped, record `null` for the affected
stream rather than coercing to 0 or publishing a partial undercount.

14. **Contributor totals require a complete account union, not search counts.**
`fetch-contributor-metrics.js` discovers every current public organization
repository, including archived repositories and forks. It traverses direct
GraphQL resource connections and every nested comment, review and discussion
reply cursor. Connection totals, unique resource IDs and cursor progress must
agree before a scan is complete. A failed or missing page invalidates the
whole new scan; a partial result cannot become an all-time total.
Compare discovered repository coverage with the org's REST `public_repos`
control total, not a curated list or token-filtered subset. A live connection
may append resources during pagination: traverse issues and PRs oldest-first,
require all collected raw IDs to match the final advertised total, and apply
the snapshot timestamp cutoff to participation. Do not reject a complete scan
solely because a total grew, or accept a missing final page. Log only safe
source operation names and fixed failure codes, never upstream error bodies.
Repository-wide REST inline review comments include replies on old PRs;
paginated REST default-branch commits count linked authors on non-merge
commits only, and skip fork git history while retaining fork-native resources.
Deduplicate accounts by public node ID, filter the canonical
`contributor-tracker.mjs` bot policy plus GitHub's Bot type, and count missing
or ghost identities separately instead of inventing account IDs.
This collector rescans recorded history: the incremental freeze rule above
does not apply, and source corrections may change earlier month counts.
Keep nested page sizes modest and exhaust their continuations instead of
multiplying large parent/comment/review connections. A live full scan hit
GitHub's secondary limit despite unused primary quota. Respect the server's
`Retry-After` through a shared worker cooldown; stop further requests after an
unrecoverable HTTP failure. Offline tests must cover both recovery and bounded
failure retaining the last-good snapshot.

Run `node scripts/fetch-contributor-metrics.js` with `GITHUB_TOKEN` or
`GH_TOKEN`; `--force` bypasses its 24-hour `seed-cache.js` generatedAt TTL.
Successful scans print request counts and GraphQL cost for real budget
measurement. Only verified scans advance generatedAt. Identical serialized
output is not rewritten. Failures preserve schema2 history, coverage,
identity records and their old timestamp, with a fixed public reason;
schema1 counts are never a last-good fallback. The offline fixture in
`fetch-contributor-metrics.test.js` drives the actual injectable transport
through two-page nested connections and dropped-page failures.

## Common Rationalizations

- _"It's one line, inlining the token read is clearer than importing a helper."_
Expand Down Expand Up @@ -176,5 +215,7 @@ instead of restating a token read or a header object.
- `AGENTS.md` → _Data pipelines_ — the never-fail-the-build and null-vs-zero
rules restated in steps 6 and 8.
- `eslint.config.mjs` — the `files` glob that excludes `.mjs`.
- GitHub GraphQL documentation (`/websites/github_en_graphql` in Context7):
direct node lookup, discussions pagination, submitted review fields and states.
- `adr/0003-factory-two-level-navigation.md` — authorizes the factory layer's
shared client.
Loading
Loading