From f72239ed41ce06c59010ea3ec971010322067818 Mon Sep 17 00:00:00 2001 From: tym83 <6355522@gmail.com> Date: Sun, 5 Apr 2026 19:51:13 +0500 Subject: [PATCH] Add OSS health pages and OpenSSF footer badge --- .github/workflows/update-oss-health.yaml | 59 +++ Makefile | 5 +- assets/scss/_oss_health.scss | 418 +++++++++++++++++++ assets/scss/main.scss | 12 + content/en/oss-health/_index.md | 6 + content/en/oss-health/devstats.md | 10 + content/en/oss-health/openssf.md | 10 + content/en/oss-health/oss-insight.md | 10 + data/oss-health/devstats.json | 496 +++++++++++++++++++++++ data/oss-health/openssf.json | 15 + data/oss-health/ossinsight.json | 496 +++++++++++++++++++++++ data/oss-health/summary.json | 20 + hack/update_oss_health.py | 470 +++++++++++++++++++++ hugo.yaml | 17 + layouts/_default/oss-health-app.html | 264 ++++++++++++ layouts/partials/footer.html | 15 + layouts/partials/footer/center.html | 18 + static/oss-health-data/devstats.json | 496 +++++++++++++++++++++++ static/oss-health-data/openssf.json | 15 + static/oss-health-data/ossinsight.json | 496 +++++++++++++++++++++++ static/oss-health-data/summary.json | 20 + 21 files changed, 3367 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/update-oss-health.yaml create mode 100644 assets/scss/_oss_health.scss create mode 100644 content/en/oss-health/_index.md create mode 100644 content/en/oss-health/devstats.md create mode 100644 content/en/oss-health/openssf.md create mode 100644 content/en/oss-health/oss-insight.md create mode 100644 data/oss-health/devstats.json create mode 100644 data/oss-health/openssf.json create mode 100644 data/oss-health/ossinsight.json create mode 100644 data/oss-health/summary.json create mode 100644 hack/update_oss_health.py create mode 100644 layouts/_default/oss-health-app.html create mode 100644 layouts/partials/footer.html create mode 100644 layouts/partials/footer/center.html create mode 100644 static/oss-health-data/devstats.json create mode 100644 static/oss-health-data/openssf.json create mode 100644 static/oss-health-data/ossinsight.json create mode 100644 static/oss-health-data/summary.json diff --git a/.github/workflows/update-oss-health.yaml b/.github/workflows/update-oss-health.yaml new file mode 100644 index 00000000..b2226ebf --- /dev/null +++ b/.github/workflows/update-oss-health.yaml @@ -0,0 +1,59 @@ +name: Update OSS health snapshots + +on: + workflow_dispatch: + schedule: + - cron: '0 4 1 * *' + +jobs: + update-oss-health: + runs-on: ubuntu-latest + + steps: + - name: Checkout target repo + uses: actions/checkout@v4 + with: + ref: 'main' + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Update OSS health data + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + make update-oss-health + git status -s + + - name: Commit & push changes + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add data/oss-health static/oss-health-data content/en/oss-health + if git diff --cached --quiet; then + echo "No changes to commit" + exit 0 + fi + git branch -D update-oss-health || true + git checkout -b update-oss-health + git commit --signoff -m "[oss-health] Update monthly OSS health snapshot $(date -u +'%Y-%m-%d %H:%M:%S')" + git push --force --set-upstream origin update-oss-health + + - name: Open pull request if not exists + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + pr_state=$(gh pr view update-oss-health --json state --jq .state 2>/dev/null || echo "") + echo "Current PR state: ${pr_state:-NONE}" + + if [[ "$pr_state" == "OPEN" ]]; then + echo "An open pull request already exists – skipping creation." + else + gh pr create \ + --title "[oss-health] Update monthly OSS health snapshot" \ + --body "Automated update via workflow." \ + --head update-oss-health \ + --base main + fi diff --git a/Makefile b/Makefile index 83e5515d..9361d492 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ NETWORKING_DEST_DIR ?= content/en/docs/networking SERVICES_DEST_DIR ?= content/en/docs/operations/services BRANCH ?= main -.PHONY: update-apps update-vms update-networking update-k8s update-services update-all template-apps template-vms template-networking template-k8s template-services template-all +.PHONY: update-apps update-vms update-networking update-k8s update-services update-oss-health update-all template-apps template-vms template-networking template-k8s template-services template-all update-apps: ./hack/update_apps.sh --apps "$(APPS)" --dest "$(APPS_DEST_DIR)" --branch "$(BRANCH)" @@ -27,6 +27,9 @@ update-k8s: update-services: ./hack/update_apps.sh --apps "$(SERVICES)" --dest "$(SERVICES_DEST_DIR)" --branch "$(BRANCH)" --pkgdir extra +update-oss-health: + ./hack/update_oss_health.py + # requires cluster authentication # to be replaced with downloading a build/release artifact from github.com/cozystack/cozystack update-api: diff --git a/assets/scss/_oss_health.scss b/assets/scss/_oss_health.scss new file mode 100644 index 00000000..b9398549 --- /dev/null +++ b/assets/scss/_oss_health.scss @@ -0,0 +1,418 @@ +.oss-health-shell { + background: + radial-gradient(circle at top left, rgba(50, 103, 214, 0.14), transparent 30%), + linear-gradient(180deg, #f7faff 0%, #eef3fb 100%); + margin: -2rem 0 0; + padding-bottom: 4.5rem; + + &__hero { + padding: 3.5rem 0 2rem; + } + + &__hero-inner { + align-items: end; + display: grid; + gap: 1.5rem; + grid-template-columns: minmax(0, 2.5fr) minmax(280px, 1fr); + + @include media-breakpoint-down(lg) { + grid-template-columns: 1fr; + } + } + + &__eyebrow { + color: #1f4aaf; + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.16em; + margin-bottom: 1rem; + text-transform: uppercase; + } + + h1 { + color: #132e6e; + font-size: clamp(2.2rem, 4vw, 3.4rem); + line-height: 1.05; + margin-bottom: 1rem; + max-width: 12ch; + } + + &__lede { + color: #334155; + font-size: 1.125rem; + line-height: 1.7; + margin-bottom: 1.25rem; + max-width: 56rem; + } + + &__meta { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + } + + &__meta-item, + &__meta-link { + align-items: center; + background: rgba(255, 255, 255, 0.84); + border: 1px solid rgba(19, 46, 110, 0.1); + border-radius: 999px; + color: #17397f; + display: inline-flex; + min-height: 2.75rem; + padding: 0.65rem 1rem; + } + + &__meta-link { + font-weight: 600; + } + + &__aside { + background: linear-gradient(180deg, rgba(19, 46, 110, 0.96), rgba(31, 74, 175, 0.94)); + border-radius: 24px; + box-shadow: 0 24px 60px rgba(14, 30, 66, 0.18); + color: #edf4ff; + padding: 1.5rem; + + p:last-child { + margin-bottom: 0; + } + } + + &__pulse { + align-items: center; + color: #dbeafe; + display: inline-flex; + font-size: 0.95rem; + font-weight: 600; + gap: 0.65rem; + margin-bottom: 1rem; + } + + &__pulse-dot { + animation: oss-health-pulse 1.8s ease-in-out infinite; + background: #86efac; + border-radius: 999px; + display: block; + height: 0.7rem; + width: 0.7rem; + } +} + +.oss-health-app { + position: relative; + z-index: 1; + + &__status { + background: #fff4d8; + border: 1px solid #f0d486; + border-radius: 18px; + color: #7a5a00; + margin-bottom: 1.5rem; + padding: 1rem 1.25rem; + } + + &__content { + min-height: 26rem; + } +} + +.oss-health-stage { + background: rgba(255, 255, 255, 0.72); + border: 1px solid rgba(19, 46, 110, 0.08); + border-radius: 28px; + box-shadow: 0 30px 80px rgba(15, 23, 42, 0.08); + padding: 1.5rem; + + @include media-breakpoint-up(lg) { + padding: 2rem; + } + + &__header { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 1rem; + justify-content: space-between; + margin-bottom: 1.5rem; + + h2 { + color: #132e6e; + font-size: clamp(1.5rem, 2vw, 2rem); + margin: 0; + } + } + + &__kicker { + color: #4f6ba8; + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.14em; + margin-bottom: 0.5rem; + text-transform: uppercase; + } +} + +.oss-health-switcher { + background: #dfe8f8; + border-radius: 999px; + display: inline-flex; + gap: 0.35rem; + padding: 0.3rem; + + &__button { + background: transparent; + border: none; + border-radius: 999px; + color: #36518e; + font-weight: 700; + min-width: 7rem; + padding: 0.72rem 1rem; + transition: 180ms ease; + + &.is-active { + background: #132e6e; + box-shadow: 0 10px 24px rgba(19, 46, 110, 0.2); + color: #fff; + } + } +} + +.oss-health-grid { + display: grid; + gap: 1rem; + + &--cards { + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + margin-bottom: 1rem; + } + + &--panels { + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin-bottom: 1rem; + + @include media-breakpoint-down(lg) { + grid-template-columns: 1fr; + } + } +} + +.oss-health-stat, +.oss-health-panel, +.oss-health-mini__item, +.oss-health-state__panel, +.oss-health-state__badge { + background: #fff; + border: 1px solid rgba(19, 46, 110, 0.09); + border-radius: 22px; + box-shadow: 0 12px 32px rgba(15, 23, 42, 0.05); +} + +.oss-health-stat { + min-height: 9rem; + padding: 1.2rem; + + &__label { + color: #637595; + display: block; + font-size: 0.82rem; + font-weight: 700; + letter-spacing: 0.12em; + margin-bottom: 1.25rem; + text-transform: uppercase; + } + + &__value { + color: #132e6e; + display: block; + font-size: clamp(2rem, 3vw, 2.8rem); + line-height: 1; + } +} + +.oss-health-mini { + display: grid; + gap: 1rem; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + margin-bottom: 1rem; + + &__item { + padding: 1rem 1.15rem; + + span { + color: #637595; + display: block; + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.12em; + margin-bottom: 0.45rem; + text-transform: uppercase; + } + + strong { + color: #17397f; + display: block; + font-size: 1rem; + line-height: 1.4; + } + } +} + +.oss-health-panel { + padding: 1.15rem 1.15rem 0.75rem; + + &__header { + margin-bottom: 0.8rem; + + h2 { + color: #132e6e; + font-size: 1.1rem; + margin: 0; + } + } +} + +.oss-health-table-wrap { + overflow-x: auto; +} + +.oss-health-table { + border-collapse: separate; + border-spacing: 0; + margin: 0; + width: 100%; + + thead th { + border-bottom: 1px solid #e6edf9; + color: #637595; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.12em; + padding: 0.9rem 0; + text-transform: uppercase; + } + + tbody td { + border-bottom: 1px solid #edf2fb; + color: #243b69; + padding: 0.9rem 0; + } + + tbody tr:last-child td { + border-bottom: none; + } +} + +.oss-health-language-cloud { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +.oss-health-language-pill { + align-items: center; + background: linear-gradient(180deg, #eef4ff, #e2ecff); + border-radius: 999px; + color: #17397f; + display: inline-flex; + gap: 0.5rem; + padding: 0.65rem 0.95rem; + + strong { + color: #132e6e; + font-size: 0.95rem; + } +} + +.oss-health-state { + display: grid; + gap: 1rem; + grid-template-columns: minmax(240px, 320px) minmax(0, 1fr); + + @include media-breakpoint-down(lg) { + grid-template-columns: 1fr; + } + + &__badge { + align-items: center; + display: flex; + flex-direction: column; + gap: 0.75rem; + justify-content: center; + padding: 2rem; + + img { + height: auto; + max-width: 100%; + } + } + + &__brand { + background: #0b1f4b; + border-radius: 16px; + display: block; + margin-bottom: 0.35rem; + max-width: 220px; + padding: 0.9rem 1rem; + } + + &__panel { + padding: 1.5rem; + + h2 { + color: #132e6e; + margin-bottom: 1rem; + } + } + + &__copy { + color: #475569; + font-size: 1.05rem; + line-height: 1.7; + margin-bottom: 1.25rem; + max-width: 52rem; + } + + &__pill { + border-radius: 999px; + display: inline-flex; + font-size: 0.95rem; + font-weight: 700; + margin-bottom: 1rem; + padding: 0.65rem 0.95rem; + + &.is-passing { + background: #ddfbe6; + color: #116149; + } + + &.is-in-progress { + background: #fff0cf; + color: #8a5a00; + } + + &.is-unknown { + background: #e8edf5; + color: #44556f; + } + } +} + +.oss-health-actions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-top: 1.25rem; +} + +@keyframes oss-health-pulse { + 0%, 100% { + opacity: 0.8; + transform: scale(1); + } + + 50% { + opacity: 1; + transform: scale(1.15); + } +} diff --git a/assets/scss/main.scss b/assets/scss/main.scss index 02ed92ed..1e633f9f 100644 --- a/assets/scss/main.scss +++ b/assets/scss/main.scss @@ -89,6 +89,17 @@ &__all_rights_reserved { display: none; } + + &__badge { + display: inline-flex; + vertical-align: middle; + + img { + height: 20px; + width: auto; + vertical-align: middle; + } + } } // Adjust anchors vs the fixed menu @@ -145,6 +156,7 @@ a { @import "gallery"; @import "support"; @import "ecosystem"; +@import "oss_health"; @import "adopters_wall"; @import "announcement-banner"; @import "tabs_alerts"; diff --git a/content/en/oss-health/_index.md b/content/en/oss-health/_index.md new file mode 100644 index 00000000..bb2dcec5 --- /dev/null +++ b/content/en/oss-health/_index.md @@ -0,0 +1,6 @@ +--- +title: "OSS Health" +description: "Open source project health snapshots for Cozystack." +--- + +Project health snapshots for Cozystack across community activity, repository trends, and security posture. diff --git a/content/en/oss-health/devstats.md b/content/en/oss-health/devstats.md new file mode 100644 index 00000000..5a8de1d9 --- /dev/null +++ b/content/en/oss-health/devstats.md @@ -0,0 +1,10 @@ +--- +title: "DevStats" +layout: "oss-health-app" +draft: false +description: "Monthly, quarterly, and yearly Cozystack community activity snapshots sourced from repository data." +oss_health_key: "devstats" +oss_health_kind: "timeseries" +source_url: "https://cozystack.devstats.cncf.io/" +lede: "Community activity snapshots for Cozystack with monthly, quarterly, and yearly views." +--- diff --git a/content/en/oss-health/openssf.md b/content/en/oss-health/openssf.md new file mode 100644 index 00000000..479c119d --- /dev/null +++ b/content/en/oss-health/openssf.md @@ -0,0 +1,10 @@ +--- +title: "OpenSSF" +layout: "oss-health-app" +draft: false +description: "Current OpenSSF Best Practices badge state for the Cozystack project." +oss_health_key: "openssf" +oss_health_kind: "state" +source_url: "https://www.bestpractices.dev/projects/10177" +lede: "Current Best Practices badge state for Cozystack and the latest verification status." +--- diff --git a/content/en/oss-health/oss-insight.md b/content/en/oss-health/oss-insight.md new file mode 100644 index 00000000..4c4f1390 --- /dev/null +++ b/content/en/oss-health/oss-insight.md @@ -0,0 +1,10 @@ +--- +title: "OSS Insight" +layout: "oss-health-app" +draft: false +description: "Monthly, quarterly, and yearly Cozystack repository snapshots including stars, forks, pull requests, and contributors." +oss_health_key: "ossinsight" +oss_health_kind: "timeseries" +source_url: "https://ossinsight.io/analyze/cozystack/cozystack" +lede: "Repository activity, social proof, and contributor trends for Cozystack across key time windows." +--- diff --git a/data/oss-health/devstats.json b/data/oss-health/devstats.json new file mode 100644 index 00000000..5412ced4 --- /dev/null +++ b/data/oss-health/devstats.json @@ -0,0 +1,496 @@ +{ + "periods": { + "month": { + "description": "Last 30 days", + "issues_closed": "14", + "issues_opened": "16", + "label": "Month", + "languages": [ + { + "name": "Go", + "value": "1,025,180" + }, + { + "name": "Go Template", + "value": "516,052" + }, + { + "name": "Shell", + "value": "289,217" + }, + { + "name": "Mustache", + "value": "116,962" + }, + { + "name": "Makefile", + "value": "79,121" + } + ], + "range": { + "from": "2026-03-06", + "to": "2026-04-05" + }, + "summary_cards": [ + { + "label": "Commits", + "value": "221" + }, + { + "label": "Contributors", + "value": "16" + }, + { + "label": "PR Authors", + "value": "16" + }, + { + "label": "PRs Opened", + "value": "151" + }, + { + "label": "PRs Merged", + "value": "127" + }, + { + "label": "Issues Closed", + "value": "14" + } + ], + "top_contributors": [ + { + "name": "kvaps", + "url": "https://github.com/kvaps", + "value": "71" + }, + { + "name": "sircthulhu", + "url": "https://github.com/sircthulhu", + "value": "44" + }, + { + "name": "lexfrei", + "url": "https://github.com/lexfrei", + "value": "28" + }, + { + "name": "lllamnyp", + "url": "https://github.com/lllamnyp", + "value": "13" + }, + { + "name": "myasnikovdaniil", + "url": "https://github.com/myasnikovdaniil", + "value": "13" + }, + { + "name": "cozystack-bot", + "url": "https://github.com/cozystack-bot", + "value": "12" + }, + { + "name": "IvanHunters", + "url": "https://github.com/IvanHunters", + "value": "12" + }, + { + "name": "androndo", + "url": "https://github.com/androndo", + "value": "5" + }, + { + "name": "mattia-eleuteri", + "url": "https://github.com/mattia-eleuteri", + "value": "5" + }, + { + "name": "sasha-sup", + "url": "https://github.com/sasha-sup", + "value": "4" + } + ], + "top_pr_authors": [ + { + "name": "github-actions[bot]", + "url": "https://github.com/apps/github-actions", + "value": "44" + }, + { + "name": "cozystack-bot", + "url": "https://github.com/cozystack-bot", + "value": "25" + }, + { + "name": "sircthulhu", + "url": "https://github.com/sircthulhu", + "value": "19" + }, + { + "name": "kvaps", + "url": "https://github.com/kvaps", + "value": "11" + }, + { + "name": "IvanHunters", + "url": "https://github.com/IvanHunters", + "value": "11" + }, + { + "name": "mattia-eleuteri", + "url": "https://github.com/mattia-eleuteri", + "value": "8" + }, + { + "name": "lllamnyp", + "url": "https://github.com/lllamnyp", + "value": "6" + }, + { + "name": "lexfrei", + "url": "https://github.com/lexfrei", + "value": "6" + }, + { + "name": "myasnikovdaniil", + "url": "https://github.com/myasnikovdaniil", + "value": "6" + }, + { + "name": "androndo", + "url": "https://github.com/androndo", + "value": "4" + } + ] + }, + "quarter": { + "description": "Last 90 days", + "issues_closed": "40", + "issues_opened": "54", + "label": "Quarter", + "languages": [ + { + "name": "Go", + "value": "1,025,180" + }, + { + "name": "Go Template", + "value": "516,052" + }, + { + "name": "Shell", + "value": "289,217" + }, + { + "name": "Mustache", + "value": "116,962" + }, + { + "name": "Makefile", + "value": "79,121" + } + ], + "range": { + "from": "2026-01-05", + "to": "2026-04-05" + }, + "summary_cards": [ + { + "label": "Commits", + "value": "864" + }, + { + "label": "Contributors", + "value": "18" + }, + { + "label": "PR Authors", + "value": "20" + }, + { + "label": "PRs Opened", + "value": "475" + }, + { + "label": "PRs Merged", + "value": "417" + }, + { + "label": "Issues Closed", + "value": "40" + } + ], + "top_contributors": [ + { + "name": "kvaps", + "url": "https://github.com/kvaps", + "value": "386" + }, + { + "name": "lexfrei", + "url": "https://github.com/lexfrei", + "value": "144" + }, + { + "name": "sircthulhu", + "url": "https://github.com/sircthulhu", + "value": "87" + }, + { + "name": "IvanHunters", + "url": "https://github.com/IvanHunters", + "value": "57" + }, + { + "name": "lllamnyp", + "url": "https://github.com/lllamnyp", + "value": "48" + }, + { + "name": "cozystack-bot", + "url": "https://github.com/cozystack-bot", + "value": "34" + }, + { + "name": "androndo", + "url": "https://github.com/androndo", + "value": "33" + }, + { + "name": "myasnikovdaniil", + "url": "https://github.com/myasnikovdaniil", + "value": "30" + }, + { + "name": "mattia-eleuteri", + "url": "https://github.com/mattia-eleuteri", + "value": "10" + }, + { + "name": "nbykov0", + "url": "https://github.com/nbykov0", + "value": "10" + } + ], + "top_pr_authors": [ + { + "name": "kvaps", + "url": "https://github.com/kvaps", + "value": "100" + }, + { + "name": "github-actions[bot]", + "url": "https://github.com/apps/github-actions", + "value": "92" + }, + { + "name": "cozystack-bot", + "url": "https://github.com/cozystack-bot", + "value": "77" + }, + { + "name": "sircthulhu", + "url": "https://github.com/sircthulhu", + "value": "53" + }, + { + "name": "IvanHunters", + "url": "https://github.com/IvanHunters", + "value": "34" + }, + { + "name": "lllamnyp", + "url": "https://github.com/lllamnyp", + "value": "24" + }, + { + "name": "lexfrei", + "url": "https://github.com/lexfrei", + "value": "22" + }, + { + "name": "myasnikovdaniil", + "url": "https://github.com/myasnikovdaniil", + "value": "17" + }, + { + "name": "mattia-eleuteri", + "url": "https://github.com/mattia-eleuteri", + "value": "14" + }, + { + "name": "androndo", + "url": "https://github.com/androndo", + "value": "12" + } + ] + }, + "year": { + "description": "Last 365 days", + "issues_closed": "212", + "issues_opened": "231", + "label": "Year", + "languages": [ + { + "name": "Go", + "value": "1,025,180" + }, + { + "name": "Go Template", + "value": "516,052" + }, + { + "name": "Shell", + "value": "289,217" + }, + { + "name": "Mustache", + "value": "116,962" + }, + { + "name": "Makefile", + "value": "79,121" + } + ], + "range": { + "from": "2025-04-05", + "to": "2026-04-05" + }, + "summary_cards": [ + { + "label": "Commits", + "value": "2,357" + }, + { + "label": "Contributors", + "value": "37" + }, + { + "label": "PR Authors", + "value": "40" + }, + { + "label": "PRs Opened", + "value": "1,334" + }, + { + "label": "PRs Merged", + "value": "1,174" + }, + { + "label": "Issues Closed", + "value": "212" + } + ], + "top_contributors": [ + { + "name": "kvaps", + "url": "https://github.com/kvaps", + "value": "1,190" + }, + { + "name": "lllamnyp", + "url": "https://github.com/lllamnyp", + "value": "301" + }, + { + "name": "lexfrei", + "url": "https://github.com/lexfrei", + "value": "157" + }, + { + "name": "IvanHunters", + "url": "https://github.com/IvanHunters", + "value": "136" + }, + { + "name": "nbykov0", + "url": "https://github.com/nbykov0", + "value": "97" + }, + { + "name": "klinch0", + "url": "https://github.com/klinch0", + "value": "92" + }, + { + "name": "sircthulhu", + "url": "https://github.com/sircthulhu", + "value": "87" + }, + { + "name": "cozystack-bot", + "url": "https://github.com/cozystack-bot", + "value": "65" + }, + { + "name": "NickVolynkin", + "url": "https://github.com/NickVolynkin", + "value": "52" + }, + { + "name": "androndo", + "url": "https://github.com/androndo", + "value": "38" + } + ], + "top_pr_authors": [ + { + "name": "kvaps", + "url": "https://github.com/kvaps", + "value": "423" + }, + { + "name": "github-actions[bot]", + "url": "https://github.com/apps/github-actions", + "value": "190" + }, + { + "name": "lllamnyp", + "url": "https://github.com/lllamnyp", + "value": "153" + }, + { + "name": "cozystack-bot", + "url": "https://github.com/cozystack-bot", + "value": "141" + }, + { + "name": "klinch0", + "url": "https://github.com/klinch0", + "value": "71" + }, + { + "name": "IvanHunters", + "url": "https://github.com/IvanHunters", + "value": "65" + }, + { + "name": "sircthulhu", + "url": "https://github.com/sircthulhu", + "value": "53" + }, + { + "name": "nbykov0", + "url": "https://github.com/nbykov0", + "value": "40" + }, + { + "name": "NickVolynkin", + "url": "https://github.com/NickVolynkin", + "value": "40" + }, + { + "name": "lexfrei", + "url": "https://github.com/lexfrei", + "value": "28" + } + ] + } + }, + "source": { + "label": "CNCF DevStats", + "url": "https://cozystack.devstats.cncf.io/" + }, + "title": "DevStats", + "updated_at": "2026-04-05T12:48:01Z" +} diff --git a/data/oss-health/openssf.json b/data/oss-health/openssf.json new file mode 100644 index 00000000..3a3622d1 --- /dev/null +++ b/data/oss-health/openssf.json @@ -0,0 +1,15 @@ +{ + "badge_last_updated_at": null, + "badge_url": "https://www.bestpractices.dev/projects/10177/badge", + "last_checked_at": "2026-04-05T12:48:01Z", + "project_name": "Cozystack", + "project_url": "https://www.bestpractices.dev/projects/10177", + "source": { + "label": "OpenSSF Best Practices", + "url": "https://www.bestpractices.dev/projects/10177" + }, + "state": "Passing", + "status_url": "https://www.bestpractices.dev/pt-BR/projects/10177/passing", + "title": "OpenSSF", + "updated_at": "2026-04-05T12:48:01Z" +} diff --git a/data/oss-health/ossinsight.json b/data/oss-health/ossinsight.json new file mode 100644 index 00000000..f06be4bd --- /dev/null +++ b/data/oss-health/ossinsight.json @@ -0,0 +1,496 @@ +{ + "periods": { + "month": { + "description": "Last 30 days", + "issues_closed": "14", + "issues_opened": "16", + "label": "Month", + "languages": [ + { + "name": "Go", + "value": "1,025,180" + }, + { + "name": "Go Template", + "value": "516,052" + }, + { + "name": "Shell", + "value": "289,217" + }, + { + "name": "Mustache", + "value": "116,962" + }, + { + "name": "Makefile", + "value": "79,121" + } + ], + "range": { + "from": "2026-03-06", + "to": "2026-04-05" + }, + "summary_cards": [ + { + "label": "Stars", + "value": "2,014" + }, + { + "label": "Forks", + "value": "145" + }, + { + "label": "Watchers", + "value": "30" + }, + { + "label": "Open Issues", + "value": "127" + }, + { + "label": "Commits", + "value": "221" + }, + { + "label": "PRs Merged", + "value": "127" + } + ], + "top_contributors": [ + { + "name": "kvaps", + "url": "https://github.com/kvaps", + "value": "71" + }, + { + "name": "sircthulhu", + "url": "https://github.com/sircthulhu", + "value": "44" + }, + { + "name": "lexfrei", + "url": "https://github.com/lexfrei", + "value": "28" + }, + { + "name": "lllamnyp", + "url": "https://github.com/lllamnyp", + "value": "13" + }, + { + "name": "myasnikovdaniil", + "url": "https://github.com/myasnikovdaniil", + "value": "13" + }, + { + "name": "cozystack-bot", + "url": "https://github.com/cozystack-bot", + "value": "12" + }, + { + "name": "IvanHunters", + "url": "https://github.com/IvanHunters", + "value": "12" + }, + { + "name": "androndo", + "url": "https://github.com/androndo", + "value": "5" + }, + { + "name": "mattia-eleuteri", + "url": "https://github.com/mattia-eleuteri", + "value": "5" + }, + { + "name": "sasha-sup", + "url": "https://github.com/sasha-sup", + "value": "4" + } + ], + "top_pr_authors": [ + { + "name": "github-actions[bot]", + "url": "https://github.com/apps/github-actions", + "value": "44" + }, + { + "name": "cozystack-bot", + "url": "https://github.com/cozystack-bot", + "value": "25" + }, + { + "name": "sircthulhu", + "url": "https://github.com/sircthulhu", + "value": "19" + }, + { + "name": "kvaps", + "url": "https://github.com/kvaps", + "value": "11" + }, + { + "name": "IvanHunters", + "url": "https://github.com/IvanHunters", + "value": "11" + }, + { + "name": "mattia-eleuteri", + "url": "https://github.com/mattia-eleuteri", + "value": "8" + }, + { + "name": "lllamnyp", + "url": "https://github.com/lllamnyp", + "value": "6" + }, + { + "name": "lexfrei", + "url": "https://github.com/lexfrei", + "value": "6" + }, + { + "name": "myasnikovdaniil", + "url": "https://github.com/myasnikovdaniil", + "value": "6" + }, + { + "name": "androndo", + "url": "https://github.com/androndo", + "value": "4" + } + ] + }, + "quarter": { + "description": "Last 90 days", + "issues_closed": "40", + "issues_opened": "54", + "label": "Quarter", + "languages": [ + { + "name": "Go", + "value": "1,025,180" + }, + { + "name": "Go Template", + "value": "516,052" + }, + { + "name": "Shell", + "value": "289,217" + }, + { + "name": "Mustache", + "value": "116,962" + }, + { + "name": "Makefile", + "value": "79,121" + } + ], + "range": { + "from": "2026-01-05", + "to": "2026-04-05" + }, + "summary_cards": [ + { + "label": "Stars", + "value": "2,014" + }, + { + "label": "Forks", + "value": "145" + }, + { + "label": "Watchers", + "value": "30" + }, + { + "label": "Open Issues", + "value": "127" + }, + { + "label": "Commits", + "value": "864" + }, + { + "label": "PRs Merged", + "value": "417" + } + ], + "top_contributors": [ + { + "name": "kvaps", + "url": "https://github.com/kvaps", + "value": "386" + }, + { + "name": "lexfrei", + "url": "https://github.com/lexfrei", + "value": "144" + }, + { + "name": "sircthulhu", + "url": "https://github.com/sircthulhu", + "value": "87" + }, + { + "name": "IvanHunters", + "url": "https://github.com/IvanHunters", + "value": "57" + }, + { + "name": "lllamnyp", + "url": "https://github.com/lllamnyp", + "value": "48" + }, + { + "name": "cozystack-bot", + "url": "https://github.com/cozystack-bot", + "value": "34" + }, + { + "name": "androndo", + "url": "https://github.com/androndo", + "value": "33" + }, + { + "name": "myasnikovdaniil", + "url": "https://github.com/myasnikovdaniil", + "value": "30" + }, + { + "name": "mattia-eleuteri", + "url": "https://github.com/mattia-eleuteri", + "value": "10" + }, + { + "name": "nbykov0", + "url": "https://github.com/nbykov0", + "value": "10" + } + ], + "top_pr_authors": [ + { + "name": "kvaps", + "url": "https://github.com/kvaps", + "value": "100" + }, + { + "name": "github-actions[bot]", + "url": "https://github.com/apps/github-actions", + "value": "92" + }, + { + "name": "cozystack-bot", + "url": "https://github.com/cozystack-bot", + "value": "77" + }, + { + "name": "sircthulhu", + "url": "https://github.com/sircthulhu", + "value": "53" + }, + { + "name": "IvanHunters", + "url": "https://github.com/IvanHunters", + "value": "34" + }, + { + "name": "lllamnyp", + "url": "https://github.com/lllamnyp", + "value": "24" + }, + { + "name": "lexfrei", + "url": "https://github.com/lexfrei", + "value": "22" + }, + { + "name": "myasnikovdaniil", + "url": "https://github.com/myasnikovdaniil", + "value": "17" + }, + { + "name": "mattia-eleuteri", + "url": "https://github.com/mattia-eleuteri", + "value": "14" + }, + { + "name": "androndo", + "url": "https://github.com/androndo", + "value": "12" + } + ] + }, + "year": { + "description": "Last 365 days", + "issues_closed": "212", + "issues_opened": "231", + "label": "Year", + "languages": [ + { + "name": "Go", + "value": "1,025,180" + }, + { + "name": "Go Template", + "value": "516,052" + }, + { + "name": "Shell", + "value": "289,217" + }, + { + "name": "Mustache", + "value": "116,962" + }, + { + "name": "Makefile", + "value": "79,121" + } + ], + "range": { + "from": "2025-04-05", + "to": "2026-04-05" + }, + "summary_cards": [ + { + "label": "Stars", + "value": "2,014" + }, + { + "label": "Forks", + "value": "145" + }, + { + "label": "Watchers", + "value": "30" + }, + { + "label": "Open Issues", + "value": "127" + }, + { + "label": "Commits", + "value": "2,357" + }, + { + "label": "PRs Merged", + "value": "1,174" + } + ], + "top_contributors": [ + { + "name": "kvaps", + "url": "https://github.com/kvaps", + "value": "1,190" + }, + { + "name": "lllamnyp", + "url": "https://github.com/lllamnyp", + "value": "301" + }, + { + "name": "lexfrei", + "url": "https://github.com/lexfrei", + "value": "157" + }, + { + "name": "IvanHunters", + "url": "https://github.com/IvanHunters", + "value": "136" + }, + { + "name": "nbykov0", + "url": "https://github.com/nbykov0", + "value": "97" + }, + { + "name": "klinch0", + "url": "https://github.com/klinch0", + "value": "92" + }, + { + "name": "sircthulhu", + "url": "https://github.com/sircthulhu", + "value": "87" + }, + { + "name": "cozystack-bot", + "url": "https://github.com/cozystack-bot", + "value": "65" + }, + { + "name": "NickVolynkin", + "url": "https://github.com/NickVolynkin", + "value": "52" + }, + { + "name": "androndo", + "url": "https://github.com/androndo", + "value": "38" + } + ], + "top_pr_authors": [ + { + "name": "kvaps", + "url": "https://github.com/kvaps", + "value": "423" + }, + { + "name": "github-actions[bot]", + "url": "https://github.com/apps/github-actions", + "value": "190" + }, + { + "name": "lllamnyp", + "url": "https://github.com/lllamnyp", + "value": "153" + }, + { + "name": "cozystack-bot", + "url": "https://github.com/cozystack-bot", + "value": "141" + }, + { + "name": "klinch0", + "url": "https://github.com/klinch0", + "value": "71" + }, + { + "name": "IvanHunters", + "url": "https://github.com/IvanHunters", + "value": "65" + }, + { + "name": "sircthulhu", + "url": "https://github.com/sircthulhu", + "value": "53" + }, + { + "name": "nbykov0", + "url": "https://github.com/nbykov0", + "value": "40" + }, + { + "name": "NickVolynkin", + "url": "https://github.com/NickVolynkin", + "value": "40" + }, + { + "name": "lexfrei", + "url": "https://github.com/lexfrei", + "value": "28" + } + ] + } + }, + "source": { + "label": "OSS Insight", + "url": "https://ossinsight.io/analyze/cozystack/cozystack" + }, + "title": "OSS Insight", + "updated_at": "2026-04-05T12:48:01Z" +} diff --git a/data/oss-health/summary.json b/data/oss-health/summary.json new file mode 100644 index 00000000..9107e950 --- /dev/null +++ b/data/oss-health/summary.json @@ -0,0 +1,20 @@ +{ + "reports": [ + { + "key": "devstats", + "title": "DevStats", + "url": "/oss-health/devstats/" + }, + { + "key": "openssf", + "title": "OpenSSF", + "url": "/oss-health/openssf/" + }, + { + "key": "ossinsight", + "title": "OSS Insight", + "url": "/oss-health/oss-insight/" + } + ], + "updated_at": "2026-04-05T13:44:49Z" +} diff --git a/hack/update_oss_health.py b/hack/update_oss_health.py new file mode 100644 index 00000000..59f66179 --- /dev/null +++ b/hack/update_oss_health.py @@ -0,0 +1,470 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import json +import os +import re +import time +import sys +import urllib.error +import urllib.parse +import urllib.request +from collections import Counter +from datetime import UTC, datetime, timedelta +from html import unescape +from pathlib import Path + + +REPO_OWNER = "cozystack" +REPO_NAME = "cozystack" +REPO_FULL_NAME = f"{REPO_OWNER}/{REPO_NAME}" +DATA_DIR = Path("data/oss-health") +STATIC_DATA_DIR = Path("static/oss-health-data") + +GITHUB_API = "https://api.github.com" +OPENSSF_PROJECT_URL = "https://www.bestpractices.dev/projects/10177" +OPENSSF_BADGE_URL = "https://www.bestpractices.dev/projects/10177/badge" +OPENSSF_STATUS_URL = "https://www.bestpractices.dev/pt-BR/projects/10177/passing" +DEVSTATS_URL = "https://cozystack.devstats.cncf.io/" +OSSINSIGHT_URL = "https://ossinsight.io/analyze/cozystack/cozystack" + +PERIODS = { + "month": {"label": "Month", "days": 30, "description": "Last 30 days"}, + "quarter": {"label": "Quarter", "days": 90, "description": "Last 90 days"}, + "year": {"label": "Year", "days": 365, "description": "Last 365 days"}, +} + + +def isoformat(dt: datetime) -> str: + return dt.replace(microsecond=0).isoformat().replace("+00:00", "Z") + + +def parse_link_header(value: str | None) -> dict[str, str]: + links: dict[str, str] = {} + if not value: + return links + for part in value.split(","): + section = part.strip().split(";") + if len(section) < 2: + continue + url = section[0].strip()[1:-1] + rel = section[1].strip() + if rel.startswith('rel="') and rel.endswith('"'): + links[rel[5:-1]] = url + return links + + +def build_headers() -> dict[str, str]: + headers = { + "Accept": "application/vnd.github+json", + "User-Agent": "cozystack-website-oss-health-updater", + } + token = os.getenv("GITHUB_TOKEN") or os.getenv("GH_TOKEN") + if token: + headers["Authorization"] = f"Bearer {token}" + return headers + + +def fetch_json(url: str, headers: dict[str, str]) -> tuple[object, dict[str, str]]: + request = urllib.request.Request(url, headers=headers) + with urllib.request.urlopen(request, timeout=60) as response: + payload = json.load(response) + return payload, dict(response.headers.items()) + + +def fetch_text(url: str, headers: dict[str, str] | None = None) -> str: + request = urllib.request.Request(url, headers=headers or {"User-Agent": "cozystack-website-oss-health-updater"}) + with urllib.request.urlopen(request, timeout=60) as response: + return response.read().decode("utf-8", errors="replace") + + +def paginate_json(url: str, headers: dict[str, str]) -> list[dict]: + items: list[dict] = [] + next_url: str | None = url + while next_url: + payload, response_headers = fetch_json(next_url, headers) + if not isinstance(payload, list): + raise RuntimeError(f"Expected list payload for {next_url}") + items.extend(payload) + next_url = parse_link_header(response_headers.get("Link")).get("next") + return items + + +def github_repo(headers: dict[str, str]) -> dict: + payload, _ = fetch_json(f"{GITHUB_API}/repos/{REPO_FULL_NAME}", headers) + if not isinstance(payload, dict): + raise RuntimeError("Unexpected repository payload") + return payload + + +def github_languages(headers: dict[str, str]) -> dict[str, int]: + payload, _ = fetch_json(f"{GITHUB_API}/repos/{REPO_FULL_NAME}/languages", headers) + if not isinstance(payload, dict): + raise RuntimeError("Unexpected languages payload") + return {str(key): int(value) for key, value in payload.items()} + + +def github_commits(since: datetime, until: datetime, headers: dict[str, str]) -> list[dict]: + params = urllib.parse.urlencode( + { + "since": isoformat(since), + "until": isoformat(until), + "per_page": 100, + } + ) + return paginate_json(f"{GITHUB_API}/repos/{REPO_FULL_NAME}/commits?{params}", headers) + + +def github_contributor_stats(headers: dict[str, str]) -> list[dict]: + request = urllib.request.Request( + f"{GITHUB_API}/repos/{REPO_FULL_NAME}/stats/contributors", + headers=headers, + ) + attempts = 0 + while attempts < 6: + attempts += 1 + with urllib.request.urlopen(request, timeout=60) as response: + if response.status == 202: + time.sleep(2) + continue + payload = json.load(response) + if not isinstance(payload, list): + raise RuntimeError("Unexpected contributor stats payload") + return payload + raise RuntimeError("GitHub contributor stats endpoint did not become ready in time") + + +def github_pulls_created(since: datetime, until: datetime, headers: dict[str, str]) -> list[dict]: + page = 1 + pulls: list[dict] = [] + while True: + params = urllib.parse.urlencode( + { + "state": "all", + "sort": "created", + "direction": "desc", + "per_page": 100, + "page": page, + } + ) + payload, _ = fetch_json(f"{GITHUB_API}/repos/{REPO_FULL_NAME}/pulls?{params}", headers) + if not isinstance(payload, list) or not payload: + break + stop = False + for pull in payload: + created_at = parse_datetime(pull["created_at"]) + if created_at < since: + stop = True + continue + if created_at <= until: + pulls.append(pull) + if stop: + break + page += 1 + return pulls + + +def github_search_total(query: str, headers: dict[str, str]) -> int: + params = urllib.parse.urlencode({"q": query, "per_page": 1}) + payload, _ = fetch_json(f"{GITHUB_API}/search/issues?{params}", headers) + if not isinstance(payload, dict): + raise RuntimeError("Unexpected search payload") + return int(payload.get("total_count", 0)) + + +def parse_datetime(value: str) -> datetime: + return datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=UTC) + + +def compact_number(value: int) -> str: + return f"{value:,}" + + +def contributor_name(commit: dict) -> tuple[str, str | None]: + author = commit.get("author") + if isinstance(author, dict) and author.get("login"): + return author["login"], author.get("html_url") + commit_author = commit.get("commit", {}).get("author", {}) + return commit_author.get("name", "Unknown"), None + + +def pull_author(pull: dict) -> tuple[str, str | None]: + user = pull.get("user") or {} + return user.get("login", "Unknown"), user.get("html_url") + + +def build_period_report( + report_kind: str, + since: datetime, + until: datetime, + repo: dict, + languages: dict[str, int], + headers: dict[str, str], + contributor_stats: list[dict] | None, + all_pulls: list[dict], + all_commits: list[dict] | None = None, +) -> dict: + pulls = [pull for pull in all_pulls if since <= parse_datetime(pull["created_at"]) <= until] + + commit_counter: Counter[str] = Counter() + commit_links: dict[str, str | None] = {} + if contributor_stats: + for contributor in contributor_stats: + user = contributor.get("author") or {} + name = user.get("login", "Unknown") + url = user.get("html_url") + commits_in_period = 0 + for week in contributor.get("weeks", []): + week_start = datetime.fromtimestamp(int(week["w"]), tz=UTC) + week_end = week_start + timedelta(days=7) + if week_end > since and week_start <= until: + commits_in_period += int(week.get("c", 0)) + if commits_in_period: + commit_counter[name] = commits_in_period + commit_links[name] = url + elif all_commits: + for commit in all_commits: + committed_at = parse_datetime(commit["commit"]["author"]["date"]) + if not (since <= committed_at <= until): + continue + name, url = contributor_name(commit) + commit_counter[name] += 1 + commit_links.setdefault(name, url) + + pr_author_counter: Counter[str] = Counter() + pr_author_links: dict[str, str | None] = {} + merged_prs = 0 + for pull in pulls: + name, url = pull_author(pull) + pr_author_counter[name] += 1 + pr_author_links.setdefault(name, url) + merged_at = pull.get("merged_at") + if merged_at: + merged_time = parse_datetime(merged_at) + if since <= merged_time <= until: + merged_prs += 1 + + date_range = f"{since.date().isoformat()}..{until.date().isoformat()}" + prs_opened = len(pulls) + issues_opened = github_search_total(f"repo:{REPO_FULL_NAME} is:issue created:{date_range}", headers) + issues_closed = github_search_total(f"repo:{REPO_FULL_NAME} is:issue closed:{date_range}", headers) + + top_languages = [ + {"name": name, "value": compact_number(bytes_of_code)} + for name, bytes_of_code in sorted(languages.items(), key=lambda item: item[1], reverse=True)[:5] + ] + + summary_cards = [ + {"label": "Commits", "value": compact_number(sum(commit_counter.values()))}, + {"label": "Contributors", "value": compact_number(len(commit_counter))}, + {"label": "PR Authors", "value": compact_number(len(pr_author_counter))}, + {"label": "PRs Opened", "value": compact_number(prs_opened)}, + {"label": "PRs Merged", "value": compact_number(merged_prs)}, + {"label": "Issues Closed", "value": compact_number(issues_closed)}, + ] + + if report_kind == "ossinsight": + summary_cards = [ + {"label": "Stars", "value": compact_number(int(repo.get("stargazers_count", 0)))}, + {"label": "Forks", "value": compact_number(int(repo.get("forks_count", 0)))}, + {"label": "Watchers", "value": compact_number(int(repo.get("subscribers_count", 0)))}, + {"label": "Open Issues", "value": compact_number(int(repo.get("open_issues_count", 0)))}, + {"label": "Commits", "value": compact_number(sum(commit_counter.values()))}, + {"label": "PRs Merged", "value": compact_number(merged_prs)}, + ] + + return { + "label": "", + "description": "", + "range": { + "from": since.date().isoformat(), + "to": until.date().isoformat(), + }, + "summary_cards": summary_cards, + "top_contributors": [ + {"name": name, "value": compact_number(count), "url": commit_links.get(name)} + for name, count in commit_counter.most_common(10) + ], + "top_pr_authors": [ + {"name": name, "value": compact_number(count), "url": pr_author_links.get(name)} + for name, count in pr_author_counter.most_common(10) + ], + "languages": top_languages, + "issues_opened": compact_number(issues_opened), + "issues_closed": compact_number(issues_closed), + } + + +def build_devstats( + repo: dict, + languages: dict[str, int], + headers: dict[str, str], + now: datetime, + contributor_stats: list[dict] | None, + all_pulls: list[dict], + all_commits: list[dict] | None = None, +) -> dict: + periods: dict[str, dict] = {} + for period_key, config in PERIODS.items(): + since = now - timedelta(days=config["days"]) + period_report = build_period_report("devstats", since, now, repo, languages, headers, contributor_stats, all_pulls, all_commits) + period_report["label"] = config["label"] + period_report["description"] = config["description"] + periods[period_key] = period_report + + return { + "title": "DevStats", + "source": {"label": "CNCF DevStats", "url": DEVSTATS_URL}, + "updated_at": isoformat(now), + "periods": periods, + } + + +def build_ossinsight( + repo: dict, + languages: dict[str, int], + headers: dict[str, str], + now: datetime, + contributor_stats: list[dict] | None, + all_pulls: list[dict], + all_commits: list[dict] | None = None, +) -> dict: + periods: dict[str, dict] = {} + for period_key, config in PERIODS.items(): + since = now - timedelta(days=config["days"]) + period_report = build_period_report("ossinsight", since, now, repo, languages, headers, contributor_stats, all_pulls, all_commits) + period_report["label"] = config["label"] + period_report["description"] = config["description"] + periods[period_key] = period_report + + return { + "title": "OSS Insight", + "source": {"label": "OSS Insight", "url": OSSINSIGHT_URL}, + "updated_at": isoformat(now), + "periods": periods, + } + + +def parse_openssf_state(page_text: str) -> str: + lowered = page_text.lower() + if "passing" in lowered: + return "Passing" + if "in progress" in lowered: + return "In Progress" + return "Unknown" + + +def parse_openssf_last_updated(page_text: str) -> str | None: + match = re.search(r"last updated on\s+(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC)", page_text, re.IGNORECASE) + if not match: + return None + try: + dt = datetime.strptime(match.group(1), "%Y-%m-%d %H:%M:%S UTC").replace(tzinfo=UTC) + except ValueError: + return None + return isoformat(dt) + + +def parse_openssf_project_name(page_text: str) -> str | None: + match = re.search(r"\s*([^<]+?)\s*\|\s*BadgeApp", page_text, re.IGNORECASE) + if not match: + return None + return unescape(match.group(1).strip()) + + +def build_openssf(now: datetime) -> dict: + page_text = fetch_text(OPENSSF_STATUS_URL) + return { + "title": "OpenSSF", + "source": {"label": "OpenSSF Best Practices", "url": OPENSSF_PROJECT_URL}, + "updated_at": isoformat(now), + "project_name": parse_openssf_project_name(page_text) or "Cozystack", + "state": parse_openssf_state(page_text), + "badge_url": OPENSSF_BADGE_URL, + "project_url": OPENSSF_PROJECT_URL, + "status_url": OPENSSF_STATUS_URL, + "last_checked_at": isoformat(now), + "badge_last_updated_at": parse_openssf_last_updated(page_text), + } + + +def write_json(path: Path, payload: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def load_json(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def render_outputs(devstats: dict, ossinsight: dict, openssf: dict) -> None: + summary = { + "updated_at": isoformat(datetime.now(UTC)), + "reports": [ + {"key": "devstats", "title": devstats["title"], "url": "/oss-health/devstats/"}, + {"key": "openssf", "title": openssf["title"], "url": "/oss-health/openssf/"}, + {"key": "ossinsight", "title": ossinsight["title"], "url": "/oss-health/oss-insight/"}, + ], + } + + for directory in (DATA_DIR, STATIC_DATA_DIR): + write_json(directory / "devstats.json", devstats) + write_json(directory / "ossinsight.json", ossinsight) + write_json(directory / "openssf.json", openssf) + write_json(directory / "summary.json", summary) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--render-only", action="store_true") + args = parser.parse_args() + + if args.render_only: + devstats = load_json(DATA_DIR / "devstats.json") + ossinsight = load_json(DATA_DIR / "ossinsight.json") + openssf = load_json(DATA_DIR / "openssf.json") + render_outputs(devstats, ossinsight, openssf) + print("OSS health assets rendered from local data.", file=sys.stderr) + return 0 + + now = datetime.now(UTC) + headers = build_headers() + + try: + print("Fetching repository metadata...", file=sys.stderr) + repo = github_repo(headers) + languages = github_languages(headers) + year_since = now - timedelta(days=PERIODS["year"]["days"]) + print("Fetching contributor stats...", file=sys.stderr) + contributor_stats = None + all_commits = None + try: + contributor_stats = github_contributor_stats(headers) + except RuntimeError: + print("Contributor stats were not ready; falling back to raw commits.", file=sys.stderr) + print("Fetching commits for the last year...", file=sys.stderr) + all_commits = github_commits(year_since, now, headers) + print("Fetching pull requests for the last year...", file=sys.stderr) + all_pulls = github_pulls_created(year_since, now, headers) + print("Building DevStats snapshot...", file=sys.stderr) + devstats = build_devstats(repo, languages, headers, now, contributor_stats, all_pulls, all_commits) + print("Building OSS Insight snapshot...", file=sys.stderr) + ossinsight = build_ossinsight(repo, languages, headers, now, contributor_stats, all_pulls, all_commits) + print("Fetching OpenSSF state...", file=sys.stderr) + openssf = build_openssf(now) + except urllib.error.HTTPError as exc: + print(f"HTTP error while updating OSS health data: {exc.code} {exc.reason}", file=sys.stderr) + return 1 + except urllib.error.URLError as exc: + print(f"Network error while updating OSS health data: {exc.reason}", file=sys.stderr) + return 1 + + render_outputs(devstats, ossinsight, openssf) + print("OSS health data updated.", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/hugo.yaml b/hugo.yaml index 797ec17b..87215d27 100644 --- a/hugo.yaml +++ b/hugo.yaml @@ -70,6 +70,8 @@ module: target: static - source: i18n target: i18n + - source: data + target: data # Markup settings # Ref: https://gohugo.io/getting-started/configuration-markup#goldmark @@ -187,6 +189,21 @@ params: menus: main: + - name: OSS Health + identifier: oss-health + weight: 35 + - name: DevStats + url: /oss-health/devstats/ + parent: oss-health + weight: 1 + - name: OpenSSF + url: /oss-health/openssf/ + parent: oss-health + weight: 2 + - name: OSS Insight + url: /oss-health/oss-insight/ + parent: oss-health + weight: 3 - name: Enterprise support url: /support weight: 5 diff --git a/layouts/_default/oss-health-app.html b/layouts/_default/oss-health-app.html new file mode 100644 index 00000000..a85aa981 --- /dev/null +++ b/layouts/_default/oss-health-app.html @@ -0,0 +1,264 @@ +{{ define "main" }} +<a class="td-offset-anchor"></a> +<section class="oss-health-shell" + data-oss-health-key="{{ .Params.oss_health_key }}" + data-oss-health-kind="{{ .Params.oss_health_kind }}"> + <div class="oss-health-shell__hero"> + <div class="container oss-health-shell__hero-inner"> + <div class="oss-health-shell__copy"> + <p class="oss-health-shell__eyebrow">Open source project health</p> + <h1>{{ .Title }}</h1> + {{ with .Params.lede }}<p class="oss-health-shell__lede">{{ . }}</p>{{ end }} + <div class="oss-health-shell__meta"> + <span class="oss-health-shell__meta-item" data-oss-health-updated>Loading latest snapshot...</span> + <a class="oss-health-shell__meta-link" href="{{ .Params.source_url }}" target="_blank" rel="noopener noreferrer">Source</a> + </div> + </div> + <div class="oss-health-shell__aside"> + <div class="oss-health-shell__pulse"> + <span class="oss-health-shell__pulse-dot"></span> + Monthly snapshot automation enabled + </div> + <p>{{ .Description }}</p> + </div> + </div> + </div> + + <div class="container oss-health-app"> + <div class="oss-health-app__status" data-oss-health-status hidden></div> + <div class="oss-health-app__content" data-oss-health-root></div> + <noscript> + <div class="alert alert-warning mt-4"> + This page needs JavaScript to render the latest snapshot. Use the source link above if scripting is disabled. + </div> + </noscript> + </div> +</section> + +<script> +(() => { + const shell = document.querySelector("[data-oss-health-key]"); + if (!shell) return; + + const key = shell.dataset.ossHealthKey; + const kind = shell.dataset.ossHealthKind; + const root = shell.querySelector("[data-oss-health-root]"); + const updatedNode = shell.querySelector("[data-oss-health-updated]"); + const statusNode = shell.querySelector("[data-oss-health-status]"); + + const formatDateTime = (value) => { + if (!value) return ""; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + return new Intl.DateTimeFormat("en", { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + timeZone: "UTC", + timeZoneName: "short", + }).format(date); + }; + + const escapeHtml = (value) => String(value ?? "") + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); + + const renderLink = (item) => { + const text = escapeHtml(item.name); + if (!item.url) return text; + return `<a href="${escapeHtml(item.url)}" target="_blank" rel="noopener noreferrer">${text}</a>`; + }; + + const renderCards = (cards) => ` + <div class="oss-health-grid oss-health-grid--cards"> + ${cards.map((card) => ` + <article class="oss-health-stat"> + <span class="oss-health-stat__label">${escapeHtml(card.label)}</span> + <strong class="oss-health-stat__value">${escapeHtml(card.value)}</strong> + </article> + `).join("")} + </div> + `; + + const renderMiniStats = (period) => ` + <div class="oss-health-mini"> + <div class="oss-health-mini__item"> + <span>Window</span> + <strong>${escapeHtml(period.description)}</strong> + </div> + <div class="oss-health-mini__item"> + <span>Range</span> + <strong>${escapeHtml(period.range.from)} to ${escapeHtml(period.range.to)}</strong> + </div> + <div class="oss-health-mini__item"> + <span>Issues Opened</span> + <strong>${escapeHtml(period.issues_opened)}</strong> + </div> + <div class="oss-health-mini__item"> + <span>Issues Closed</span> + <strong>${escapeHtml(period.issues_closed)}</strong> + </div> + </div> + `; + + const renderTable = (title, metricLabel, items) => ` + <section class="oss-health-panel"> + <div class="oss-health-panel__header"> + <h2>${escapeHtml(title)}</h2> + </div> + <div class="oss-health-table-wrap"> + <table class="oss-health-table"> + <thead> + <tr> + <th>Name</th> + <th>${escapeHtml(metricLabel)}</th> + </tr> + </thead> + <tbody> + ${items.map((item) => ` + <tr> + <td>${renderLink(item)}</td> + <td>${escapeHtml(item.value)}</td> + </tr> + `).join("")} + </tbody> + </table> + </div> + </section> + `; + + const renderLanguages = (languages) => ` + <section class="oss-health-panel"> + <div class="oss-health-panel__header"> + <h2>Languages</h2> + </div> + <div class="oss-health-language-cloud"> + ${languages.map((item) => ` + <span class="oss-health-language-pill"> + <span>${escapeHtml(item.name)}</span> + <strong>${escapeHtml(item.value)}</strong> + </span> + `).join("")} + </div> + </section> + `; + + const renderPeriod = (period) => ` + <div class="oss-health-period"> + ${renderCards(period.summary_cards)} + ${renderMiniStats(period)} + <div class="oss-health-grid oss-health-grid--panels"> + ${renderTable("Top Contributors", "Commits", period.top_contributors)} + ${renderTable("Top PR Authors", "PRs Opened", period.top_pr_authors)} + </div> + ${renderLanguages(period.languages)} + </div> + `; + + const renderTimeseries = (payload) => { + const order = ["month", "quarter", "year"]; + let active = "month"; + + const renderActive = () => { + const period = payload.periods[active]; + const controls = ` + <div class="oss-health-switcher" role="tablist" aria-label="Report period"> + ${order.map((name) => ` + <button class="oss-health-switcher__button${name === active ? " is-active" : ""}" data-period="${name}" role="tab" aria-selected="${name === active ? "true" : "false"}"> + ${escapeHtml(payload.periods[name].label)} + </button> + `).join("")} + </div> + `; + + root.innerHTML = ` + <section class="oss-health-stage"> + <div class="oss-health-stage__header"> + <div> + <p class="oss-health-stage__kicker">${escapeHtml(payload.source.label)}</p> + <h2>${escapeHtml(payload.title)} snapshot</h2> + </div> + ${controls} + </div> + ${renderPeriod(period)} + </section> + `; + + root.querySelectorAll("[data-period]").forEach((button) => { + button.addEventListener("click", () => { + active = button.dataset.period; + renderActive(); + }); + }); + }; + + renderActive(); + }; + + const renderOpenSSF = (payload) => { + const badgeUpdated = payload.badge_last_updated_at + ? `<div class="oss-health-mini__item"><span>Badge updated</span><strong>${escapeHtml(formatDateTime(payload.badge_last_updated_at))}</strong></div>` + : ""; + + root.innerHTML = ` + <section class="oss-health-stage"> + <div class="oss-health-state"> + <article class="oss-health-state__badge"> + <img + class="oss-health-state__brand" + src="https://openssf.org/wp-content/uploads/2023/12/Open-SSF-Logo-horizontal-colorwhite.svg" + alt="OpenSSF logo"> + <a href="${escapeHtml(payload.project_url)}" target="_blank" rel="noopener noreferrer"> + <img src="${escapeHtml(payload.badge_url)}" alt="OpenSSF Best Practices badge for Cozystack"> + </a> + </article> + <article class="oss-health-state__panel"> + <p class="oss-health-stage__kicker">${escapeHtml(payload.source.label)}</p> + <h2>${escapeHtml(payload.project_name)}</h2> + <div class="oss-health-state__pill is-${escapeHtml(payload.state.toLowerCase().replaceAll(" ", "-"))}"> + ${escapeHtml(payload.state)} + </div> + <p class="oss-health-state__copy">OpenSSF Best Practices tracks whether the project meets a published baseline for documentation, process, testing, and security hygiene.</p> + <div class="oss-health-mini"> + <div class="oss-health-mini__item"><span>Checked</span><strong>${escapeHtml(formatDateTime(payload.last_checked_at))}</strong></div> + ${badgeUpdated} + </div> + <div class="oss-health-actions"> + <a class="btn btn-primary" href="${escapeHtml(payload.project_url)}" target="_blank" rel="noopener noreferrer">Open project page</a> + <a class="btn btn-outline-primary" href="${escapeHtml(payload.status_url)}" target="_blank" rel="noopener noreferrer">Canonical status page</a> + </div> + </article> + </div> + </section> + `; + }; + + const setError = (message) => { + statusNode.hidden = false; + statusNode.textContent = message; + }; + + fetch(`/oss-health-data/${key}.json`, { credentials: "same-origin" }) + .then((response) => { + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json(); + }) + .then((payload) => { + updatedNode.textContent = `Updated ${formatDateTime(payload.updated_at)}`; + if (kind === "state") { + renderOpenSSF(payload); + } else { + renderTimeseries(payload); + } + }) + .catch((error) => { + setError(`Could not load the latest snapshot: ${error.message}`); + }); +})(); +</script> +{{ end }} diff --git a/layouts/partials/footer.html b/layouts/partials/footer.html new file mode 100644 index 00000000..94c2d1e0 --- /dev/null +++ b/layouts/partials/footer.html @@ -0,0 +1,15 @@ +<footer class="td-footer row d-print-none"> + <div class="container-fluid"> + <div class="row mx-md-2"> + <div class="td-footer__left col-6 col-sm-4 order-sm-1"> + {{ partial "footer/left.html" . }} + </div> + <div class="td-footer__right col-6 col-sm-4 order-sm-3"> + {{ partial "footer/right.html" . }} + </div> + <div class="td-footer__center col-12 col-sm-4 py-2 order-sm-2"> + {{ partial "footer/center.html" . }} + </div> + </div> + </div> +</footer> diff --git a/layouts/partials/footer/center.html b/layouts/partials/footer/center.html new file mode 100644 index 00000000..956246f2 --- /dev/null +++ b/layouts/partials/footer/center.html @@ -0,0 +1,18 @@ +{{ partial "footer/copyright.html" . -}} +<a class="td-footer__badge ms-2" href="https://www.bestpractices.dev/projects/10177" target="_blank" rel="noopener noreferrer"> + <img src="https://www.bestpractices.dev/projects/10177/badge" alt="OpenSSF Best Practices badge"> +</a> + +{{ with .Site.Params.privacy_policy -}} + <span class="ms-2"><a href="{{ . }}" target="_blank" rel="noopener">{{ T "footer_privacy_policy" }}</a></span> +{{- end -}} + +{{ if ne .Site.Params.ui.footer_about_disable nil -}} + {{ warnf "Config parameter '.params.ui.footer_about_disable' is DEPRECATED, use '.params.ui.footer_about_enable' instead." -}} +{{ end -}} + +{{ if or .Site.Params.ui.footer_about_enable (eq .Site.Params.ui.footer_about_disable false) -}} + {{ with .Site.GetPage "about" -}} + <p class="td-footer__about mt-2"><a href="{{ .RelPermalink }}">{{ .Title }}</a></p> + {{- end -}} +{{ end -}} diff --git a/static/oss-health-data/devstats.json b/static/oss-health-data/devstats.json new file mode 100644 index 00000000..5412ced4 --- /dev/null +++ b/static/oss-health-data/devstats.json @@ -0,0 +1,496 @@ +{ + "periods": { + "month": { + "description": "Last 30 days", + "issues_closed": "14", + "issues_opened": "16", + "label": "Month", + "languages": [ + { + "name": "Go", + "value": "1,025,180" + }, + { + "name": "Go Template", + "value": "516,052" + }, + { + "name": "Shell", + "value": "289,217" + }, + { + "name": "Mustache", + "value": "116,962" + }, + { + "name": "Makefile", + "value": "79,121" + } + ], + "range": { + "from": "2026-03-06", + "to": "2026-04-05" + }, + "summary_cards": [ + { + "label": "Commits", + "value": "221" + }, + { + "label": "Contributors", + "value": "16" + }, + { + "label": "PR Authors", + "value": "16" + }, + { + "label": "PRs Opened", + "value": "151" + }, + { + "label": "PRs Merged", + "value": "127" + }, + { + "label": "Issues Closed", + "value": "14" + } + ], + "top_contributors": [ + { + "name": "kvaps", + "url": "https://github.com/kvaps", + "value": "71" + }, + { + "name": "sircthulhu", + "url": "https://github.com/sircthulhu", + "value": "44" + }, + { + "name": "lexfrei", + "url": "https://github.com/lexfrei", + "value": "28" + }, + { + "name": "lllamnyp", + "url": "https://github.com/lllamnyp", + "value": "13" + }, + { + "name": "myasnikovdaniil", + "url": "https://github.com/myasnikovdaniil", + "value": "13" + }, + { + "name": "cozystack-bot", + "url": "https://github.com/cozystack-bot", + "value": "12" + }, + { + "name": "IvanHunters", + "url": "https://github.com/IvanHunters", + "value": "12" + }, + { + "name": "androndo", + "url": "https://github.com/androndo", + "value": "5" + }, + { + "name": "mattia-eleuteri", + "url": "https://github.com/mattia-eleuteri", + "value": "5" + }, + { + "name": "sasha-sup", + "url": "https://github.com/sasha-sup", + "value": "4" + } + ], + "top_pr_authors": [ + { + "name": "github-actions[bot]", + "url": "https://github.com/apps/github-actions", + "value": "44" + }, + { + "name": "cozystack-bot", + "url": "https://github.com/cozystack-bot", + "value": "25" + }, + { + "name": "sircthulhu", + "url": "https://github.com/sircthulhu", + "value": "19" + }, + { + "name": "kvaps", + "url": "https://github.com/kvaps", + "value": "11" + }, + { + "name": "IvanHunters", + "url": "https://github.com/IvanHunters", + "value": "11" + }, + { + "name": "mattia-eleuteri", + "url": "https://github.com/mattia-eleuteri", + "value": "8" + }, + { + "name": "lllamnyp", + "url": "https://github.com/lllamnyp", + "value": "6" + }, + { + "name": "lexfrei", + "url": "https://github.com/lexfrei", + "value": "6" + }, + { + "name": "myasnikovdaniil", + "url": "https://github.com/myasnikovdaniil", + "value": "6" + }, + { + "name": "androndo", + "url": "https://github.com/androndo", + "value": "4" + } + ] + }, + "quarter": { + "description": "Last 90 days", + "issues_closed": "40", + "issues_opened": "54", + "label": "Quarter", + "languages": [ + { + "name": "Go", + "value": "1,025,180" + }, + { + "name": "Go Template", + "value": "516,052" + }, + { + "name": "Shell", + "value": "289,217" + }, + { + "name": "Mustache", + "value": "116,962" + }, + { + "name": "Makefile", + "value": "79,121" + } + ], + "range": { + "from": "2026-01-05", + "to": "2026-04-05" + }, + "summary_cards": [ + { + "label": "Commits", + "value": "864" + }, + { + "label": "Contributors", + "value": "18" + }, + { + "label": "PR Authors", + "value": "20" + }, + { + "label": "PRs Opened", + "value": "475" + }, + { + "label": "PRs Merged", + "value": "417" + }, + { + "label": "Issues Closed", + "value": "40" + } + ], + "top_contributors": [ + { + "name": "kvaps", + "url": "https://github.com/kvaps", + "value": "386" + }, + { + "name": "lexfrei", + "url": "https://github.com/lexfrei", + "value": "144" + }, + { + "name": "sircthulhu", + "url": "https://github.com/sircthulhu", + "value": "87" + }, + { + "name": "IvanHunters", + "url": "https://github.com/IvanHunters", + "value": "57" + }, + { + "name": "lllamnyp", + "url": "https://github.com/lllamnyp", + "value": "48" + }, + { + "name": "cozystack-bot", + "url": "https://github.com/cozystack-bot", + "value": "34" + }, + { + "name": "androndo", + "url": "https://github.com/androndo", + "value": "33" + }, + { + "name": "myasnikovdaniil", + "url": "https://github.com/myasnikovdaniil", + "value": "30" + }, + { + "name": "mattia-eleuteri", + "url": "https://github.com/mattia-eleuteri", + "value": "10" + }, + { + "name": "nbykov0", + "url": "https://github.com/nbykov0", + "value": "10" + } + ], + "top_pr_authors": [ + { + "name": "kvaps", + "url": "https://github.com/kvaps", + "value": "100" + }, + { + "name": "github-actions[bot]", + "url": "https://github.com/apps/github-actions", + "value": "92" + }, + { + "name": "cozystack-bot", + "url": "https://github.com/cozystack-bot", + "value": "77" + }, + { + "name": "sircthulhu", + "url": "https://github.com/sircthulhu", + "value": "53" + }, + { + "name": "IvanHunters", + "url": "https://github.com/IvanHunters", + "value": "34" + }, + { + "name": "lllamnyp", + "url": "https://github.com/lllamnyp", + "value": "24" + }, + { + "name": "lexfrei", + "url": "https://github.com/lexfrei", + "value": "22" + }, + { + "name": "myasnikovdaniil", + "url": "https://github.com/myasnikovdaniil", + "value": "17" + }, + { + "name": "mattia-eleuteri", + "url": "https://github.com/mattia-eleuteri", + "value": "14" + }, + { + "name": "androndo", + "url": "https://github.com/androndo", + "value": "12" + } + ] + }, + "year": { + "description": "Last 365 days", + "issues_closed": "212", + "issues_opened": "231", + "label": "Year", + "languages": [ + { + "name": "Go", + "value": "1,025,180" + }, + { + "name": "Go Template", + "value": "516,052" + }, + { + "name": "Shell", + "value": "289,217" + }, + { + "name": "Mustache", + "value": "116,962" + }, + { + "name": "Makefile", + "value": "79,121" + } + ], + "range": { + "from": "2025-04-05", + "to": "2026-04-05" + }, + "summary_cards": [ + { + "label": "Commits", + "value": "2,357" + }, + { + "label": "Contributors", + "value": "37" + }, + { + "label": "PR Authors", + "value": "40" + }, + { + "label": "PRs Opened", + "value": "1,334" + }, + { + "label": "PRs Merged", + "value": "1,174" + }, + { + "label": "Issues Closed", + "value": "212" + } + ], + "top_contributors": [ + { + "name": "kvaps", + "url": "https://github.com/kvaps", + "value": "1,190" + }, + { + "name": "lllamnyp", + "url": "https://github.com/lllamnyp", + "value": "301" + }, + { + "name": "lexfrei", + "url": "https://github.com/lexfrei", + "value": "157" + }, + { + "name": "IvanHunters", + "url": "https://github.com/IvanHunters", + "value": "136" + }, + { + "name": "nbykov0", + "url": "https://github.com/nbykov0", + "value": "97" + }, + { + "name": "klinch0", + "url": "https://github.com/klinch0", + "value": "92" + }, + { + "name": "sircthulhu", + "url": "https://github.com/sircthulhu", + "value": "87" + }, + { + "name": "cozystack-bot", + "url": "https://github.com/cozystack-bot", + "value": "65" + }, + { + "name": "NickVolynkin", + "url": "https://github.com/NickVolynkin", + "value": "52" + }, + { + "name": "androndo", + "url": "https://github.com/androndo", + "value": "38" + } + ], + "top_pr_authors": [ + { + "name": "kvaps", + "url": "https://github.com/kvaps", + "value": "423" + }, + { + "name": "github-actions[bot]", + "url": "https://github.com/apps/github-actions", + "value": "190" + }, + { + "name": "lllamnyp", + "url": "https://github.com/lllamnyp", + "value": "153" + }, + { + "name": "cozystack-bot", + "url": "https://github.com/cozystack-bot", + "value": "141" + }, + { + "name": "klinch0", + "url": "https://github.com/klinch0", + "value": "71" + }, + { + "name": "IvanHunters", + "url": "https://github.com/IvanHunters", + "value": "65" + }, + { + "name": "sircthulhu", + "url": "https://github.com/sircthulhu", + "value": "53" + }, + { + "name": "nbykov0", + "url": "https://github.com/nbykov0", + "value": "40" + }, + { + "name": "NickVolynkin", + "url": "https://github.com/NickVolynkin", + "value": "40" + }, + { + "name": "lexfrei", + "url": "https://github.com/lexfrei", + "value": "28" + } + ] + } + }, + "source": { + "label": "CNCF DevStats", + "url": "https://cozystack.devstats.cncf.io/" + }, + "title": "DevStats", + "updated_at": "2026-04-05T12:48:01Z" +} diff --git a/static/oss-health-data/openssf.json b/static/oss-health-data/openssf.json new file mode 100644 index 00000000..3a3622d1 --- /dev/null +++ b/static/oss-health-data/openssf.json @@ -0,0 +1,15 @@ +{ + "badge_last_updated_at": null, + "badge_url": "https://www.bestpractices.dev/projects/10177/badge", + "last_checked_at": "2026-04-05T12:48:01Z", + "project_name": "Cozystack", + "project_url": "https://www.bestpractices.dev/projects/10177", + "source": { + "label": "OpenSSF Best Practices", + "url": "https://www.bestpractices.dev/projects/10177" + }, + "state": "Passing", + "status_url": "https://www.bestpractices.dev/pt-BR/projects/10177/passing", + "title": "OpenSSF", + "updated_at": "2026-04-05T12:48:01Z" +} diff --git a/static/oss-health-data/ossinsight.json b/static/oss-health-data/ossinsight.json new file mode 100644 index 00000000..f06be4bd --- /dev/null +++ b/static/oss-health-data/ossinsight.json @@ -0,0 +1,496 @@ +{ + "periods": { + "month": { + "description": "Last 30 days", + "issues_closed": "14", + "issues_opened": "16", + "label": "Month", + "languages": [ + { + "name": "Go", + "value": "1,025,180" + }, + { + "name": "Go Template", + "value": "516,052" + }, + { + "name": "Shell", + "value": "289,217" + }, + { + "name": "Mustache", + "value": "116,962" + }, + { + "name": "Makefile", + "value": "79,121" + } + ], + "range": { + "from": "2026-03-06", + "to": "2026-04-05" + }, + "summary_cards": [ + { + "label": "Stars", + "value": "2,014" + }, + { + "label": "Forks", + "value": "145" + }, + { + "label": "Watchers", + "value": "30" + }, + { + "label": "Open Issues", + "value": "127" + }, + { + "label": "Commits", + "value": "221" + }, + { + "label": "PRs Merged", + "value": "127" + } + ], + "top_contributors": [ + { + "name": "kvaps", + "url": "https://github.com/kvaps", + "value": "71" + }, + { + "name": "sircthulhu", + "url": "https://github.com/sircthulhu", + "value": "44" + }, + { + "name": "lexfrei", + "url": "https://github.com/lexfrei", + "value": "28" + }, + { + "name": "lllamnyp", + "url": "https://github.com/lllamnyp", + "value": "13" + }, + { + "name": "myasnikovdaniil", + "url": "https://github.com/myasnikovdaniil", + "value": "13" + }, + { + "name": "cozystack-bot", + "url": "https://github.com/cozystack-bot", + "value": "12" + }, + { + "name": "IvanHunters", + "url": "https://github.com/IvanHunters", + "value": "12" + }, + { + "name": "androndo", + "url": "https://github.com/androndo", + "value": "5" + }, + { + "name": "mattia-eleuteri", + "url": "https://github.com/mattia-eleuteri", + "value": "5" + }, + { + "name": "sasha-sup", + "url": "https://github.com/sasha-sup", + "value": "4" + } + ], + "top_pr_authors": [ + { + "name": "github-actions[bot]", + "url": "https://github.com/apps/github-actions", + "value": "44" + }, + { + "name": "cozystack-bot", + "url": "https://github.com/cozystack-bot", + "value": "25" + }, + { + "name": "sircthulhu", + "url": "https://github.com/sircthulhu", + "value": "19" + }, + { + "name": "kvaps", + "url": "https://github.com/kvaps", + "value": "11" + }, + { + "name": "IvanHunters", + "url": "https://github.com/IvanHunters", + "value": "11" + }, + { + "name": "mattia-eleuteri", + "url": "https://github.com/mattia-eleuteri", + "value": "8" + }, + { + "name": "lllamnyp", + "url": "https://github.com/lllamnyp", + "value": "6" + }, + { + "name": "lexfrei", + "url": "https://github.com/lexfrei", + "value": "6" + }, + { + "name": "myasnikovdaniil", + "url": "https://github.com/myasnikovdaniil", + "value": "6" + }, + { + "name": "androndo", + "url": "https://github.com/androndo", + "value": "4" + } + ] + }, + "quarter": { + "description": "Last 90 days", + "issues_closed": "40", + "issues_opened": "54", + "label": "Quarter", + "languages": [ + { + "name": "Go", + "value": "1,025,180" + }, + { + "name": "Go Template", + "value": "516,052" + }, + { + "name": "Shell", + "value": "289,217" + }, + { + "name": "Mustache", + "value": "116,962" + }, + { + "name": "Makefile", + "value": "79,121" + } + ], + "range": { + "from": "2026-01-05", + "to": "2026-04-05" + }, + "summary_cards": [ + { + "label": "Stars", + "value": "2,014" + }, + { + "label": "Forks", + "value": "145" + }, + { + "label": "Watchers", + "value": "30" + }, + { + "label": "Open Issues", + "value": "127" + }, + { + "label": "Commits", + "value": "864" + }, + { + "label": "PRs Merged", + "value": "417" + } + ], + "top_contributors": [ + { + "name": "kvaps", + "url": "https://github.com/kvaps", + "value": "386" + }, + { + "name": "lexfrei", + "url": "https://github.com/lexfrei", + "value": "144" + }, + { + "name": "sircthulhu", + "url": "https://github.com/sircthulhu", + "value": "87" + }, + { + "name": "IvanHunters", + "url": "https://github.com/IvanHunters", + "value": "57" + }, + { + "name": "lllamnyp", + "url": "https://github.com/lllamnyp", + "value": "48" + }, + { + "name": "cozystack-bot", + "url": "https://github.com/cozystack-bot", + "value": "34" + }, + { + "name": "androndo", + "url": "https://github.com/androndo", + "value": "33" + }, + { + "name": "myasnikovdaniil", + "url": "https://github.com/myasnikovdaniil", + "value": "30" + }, + { + "name": "mattia-eleuteri", + "url": "https://github.com/mattia-eleuteri", + "value": "10" + }, + { + "name": "nbykov0", + "url": "https://github.com/nbykov0", + "value": "10" + } + ], + "top_pr_authors": [ + { + "name": "kvaps", + "url": "https://github.com/kvaps", + "value": "100" + }, + { + "name": "github-actions[bot]", + "url": "https://github.com/apps/github-actions", + "value": "92" + }, + { + "name": "cozystack-bot", + "url": "https://github.com/cozystack-bot", + "value": "77" + }, + { + "name": "sircthulhu", + "url": "https://github.com/sircthulhu", + "value": "53" + }, + { + "name": "IvanHunters", + "url": "https://github.com/IvanHunters", + "value": "34" + }, + { + "name": "lllamnyp", + "url": "https://github.com/lllamnyp", + "value": "24" + }, + { + "name": "lexfrei", + "url": "https://github.com/lexfrei", + "value": "22" + }, + { + "name": "myasnikovdaniil", + "url": "https://github.com/myasnikovdaniil", + "value": "17" + }, + { + "name": "mattia-eleuteri", + "url": "https://github.com/mattia-eleuteri", + "value": "14" + }, + { + "name": "androndo", + "url": "https://github.com/androndo", + "value": "12" + } + ] + }, + "year": { + "description": "Last 365 days", + "issues_closed": "212", + "issues_opened": "231", + "label": "Year", + "languages": [ + { + "name": "Go", + "value": "1,025,180" + }, + { + "name": "Go Template", + "value": "516,052" + }, + { + "name": "Shell", + "value": "289,217" + }, + { + "name": "Mustache", + "value": "116,962" + }, + { + "name": "Makefile", + "value": "79,121" + } + ], + "range": { + "from": "2025-04-05", + "to": "2026-04-05" + }, + "summary_cards": [ + { + "label": "Stars", + "value": "2,014" + }, + { + "label": "Forks", + "value": "145" + }, + { + "label": "Watchers", + "value": "30" + }, + { + "label": "Open Issues", + "value": "127" + }, + { + "label": "Commits", + "value": "2,357" + }, + { + "label": "PRs Merged", + "value": "1,174" + } + ], + "top_contributors": [ + { + "name": "kvaps", + "url": "https://github.com/kvaps", + "value": "1,190" + }, + { + "name": "lllamnyp", + "url": "https://github.com/lllamnyp", + "value": "301" + }, + { + "name": "lexfrei", + "url": "https://github.com/lexfrei", + "value": "157" + }, + { + "name": "IvanHunters", + "url": "https://github.com/IvanHunters", + "value": "136" + }, + { + "name": "nbykov0", + "url": "https://github.com/nbykov0", + "value": "97" + }, + { + "name": "klinch0", + "url": "https://github.com/klinch0", + "value": "92" + }, + { + "name": "sircthulhu", + "url": "https://github.com/sircthulhu", + "value": "87" + }, + { + "name": "cozystack-bot", + "url": "https://github.com/cozystack-bot", + "value": "65" + }, + { + "name": "NickVolynkin", + "url": "https://github.com/NickVolynkin", + "value": "52" + }, + { + "name": "androndo", + "url": "https://github.com/androndo", + "value": "38" + } + ], + "top_pr_authors": [ + { + "name": "kvaps", + "url": "https://github.com/kvaps", + "value": "423" + }, + { + "name": "github-actions[bot]", + "url": "https://github.com/apps/github-actions", + "value": "190" + }, + { + "name": "lllamnyp", + "url": "https://github.com/lllamnyp", + "value": "153" + }, + { + "name": "cozystack-bot", + "url": "https://github.com/cozystack-bot", + "value": "141" + }, + { + "name": "klinch0", + "url": "https://github.com/klinch0", + "value": "71" + }, + { + "name": "IvanHunters", + "url": "https://github.com/IvanHunters", + "value": "65" + }, + { + "name": "sircthulhu", + "url": "https://github.com/sircthulhu", + "value": "53" + }, + { + "name": "nbykov0", + "url": "https://github.com/nbykov0", + "value": "40" + }, + { + "name": "NickVolynkin", + "url": "https://github.com/NickVolynkin", + "value": "40" + }, + { + "name": "lexfrei", + "url": "https://github.com/lexfrei", + "value": "28" + } + ] + } + }, + "source": { + "label": "OSS Insight", + "url": "https://ossinsight.io/analyze/cozystack/cozystack" + }, + "title": "OSS Insight", + "updated_at": "2026-04-05T12:48:01Z" +} diff --git a/static/oss-health-data/summary.json b/static/oss-health-data/summary.json new file mode 100644 index 00000000..9107e950 --- /dev/null +++ b/static/oss-health-data/summary.json @@ -0,0 +1,20 @@ +{ + "reports": [ + { + "key": "devstats", + "title": "DevStats", + "url": "/oss-health/devstats/" + }, + { + "key": "openssf", + "title": "OpenSSF", + "url": "/oss-health/openssf/" + }, + { + "key": "ossinsight", + "title": "OSS Insight", + "url": "/oss-health/oss-insight/" + } + ], + "updated_at": "2026-04-05T13:44:49Z" +}