mirror of
https://github.com/sst/opencode.git
synced 2025-07-07 16:14:59 +00:00
ci: daily stats script
This commit is contained in:
parent
f0962e2d9c
commit
780419ecae
3 changed files with 260 additions and 1 deletions
30
.github/workflows/stats.yml
vendored
Normal file
30
.github/workflows/stats.yml
vendored
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
name: stats
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 12 * * *" # Run daily at 12:00 UTC
|
||||||
|
workflow_dispatch: # Allow manual trigger
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stats:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
|
- name: Run stats script
|
||||||
|
run: bun scripts/stats.ts
|
||||||
|
|
||||||
|
- name: Commit stats
|
||||||
|
run: |
|
||||||
|
git config --local user.email "action@github.com"
|
||||||
|
git config --local user.name "GitHub Action"
|
||||||
|
git add STATS.md
|
||||||
|
git diff --staged --quiet || git commit -m "Update download stats $(date -I)"
|
||||||
|
git push
|
225
scripts/stats.ts
Executable file
225
scripts/stats.ts
Executable file
|
@ -0,0 +1,225 @@
|
||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
interface Asset {
|
||||||
|
name: string
|
||||||
|
download_count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Release {
|
||||||
|
tag_name: string
|
||||||
|
name: string
|
||||||
|
assets: Asset[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NpmDownloadsRange {
|
||||||
|
start: string
|
||||||
|
end: string
|
||||||
|
package: string
|
||||||
|
downloads: Array<{
|
||||||
|
downloads: number
|
||||||
|
day: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchNpmDownloads(packageName: string): Promise<number> {
|
||||||
|
try {
|
||||||
|
// Use a range from 2020 to current year + 5 years to ensure it works forever
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
|
const endYear = currentYear + 5
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.npmjs.org/downloads/range/2020-01-01:${endYear}-12-31/${packageName}`,
|
||||||
|
)
|
||||||
|
if (!response.ok) {
|
||||||
|
console.warn(
|
||||||
|
`Failed to fetch npm downloads for ${packageName}: ${response.status}`,
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const data: NpmDownloadsRange = await response.json()
|
||||||
|
return data.downloads.reduce((total, day) => total + day.downloads, 0)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Error fetching npm downloads for ${packageName}:`, error)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchReleases(): Promise<Release[]> {
|
||||||
|
const releases: Release[] = []
|
||||||
|
let page = 1
|
||||||
|
const per = 100
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const url = `https://api.github.com/repos/sst/opencode/releases?page=${page}&per_page=${per}`
|
||||||
|
|
||||||
|
const response = await fetch(url)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`GitHub API error: ${response.status} ${response.statusText}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const batch: Release[] = await response.json()
|
||||||
|
if (batch.length === 0) break
|
||||||
|
|
||||||
|
releases.push(...batch)
|
||||||
|
console.log(`Fetched page ${page} with ${batch.length} releases`)
|
||||||
|
|
||||||
|
if (batch.length < per) break
|
||||||
|
page++
|
||||||
|
}
|
||||||
|
|
||||||
|
return releases
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculate(releases: Release[]) {
|
||||||
|
let total = 0
|
||||||
|
const stats = []
|
||||||
|
|
||||||
|
for (const release of releases) {
|
||||||
|
let downloads = 0
|
||||||
|
const assets = []
|
||||||
|
|
||||||
|
for (const asset of release.assets) {
|
||||||
|
downloads += asset.download_count
|
||||||
|
assets.push({
|
||||||
|
name: asset.name,
|
||||||
|
downloads: asset.download_count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
total += downloads
|
||||||
|
stats.push({
|
||||||
|
tag: release.tag_name,
|
||||||
|
name: release.name,
|
||||||
|
downloads,
|
||||||
|
assets,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { total, stats }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save(githubTotal: number, npmDownloads: number) {
|
||||||
|
const file = "STATS.md"
|
||||||
|
const date = new Date().toISOString().split("T")[0]
|
||||||
|
const total = githubTotal + npmDownloads
|
||||||
|
|
||||||
|
let previousGithub = 0
|
||||||
|
let previousNpm = 0
|
||||||
|
let previousTotal = 0
|
||||||
|
let content = ""
|
||||||
|
|
||||||
|
try {
|
||||||
|
content = await Bun.file(file).text()
|
||||||
|
const lines = content.trim().split("\n")
|
||||||
|
|
||||||
|
for (let i = lines.length - 1; i >= 0; i--) {
|
||||||
|
const line = lines[i].trim()
|
||||||
|
if (
|
||||||
|
line.startsWith("|") &&
|
||||||
|
!line.includes("Date") &&
|
||||||
|
!line.includes("---")
|
||||||
|
) {
|
||||||
|
const match = line.match(
|
||||||
|
/\|\s*[\d-]+\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|/,
|
||||||
|
)
|
||||||
|
if (match) {
|
||||||
|
previousGithub = parseInt(match[1].replace(/,/g, ""))
|
||||||
|
previousNpm = parseInt(match[2].replace(/,/g, ""))
|
||||||
|
previousTotal = parseInt(match[3].replace(/,/g, ""))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
content =
|
||||||
|
"# Download Stats\n\n| Date | GitHub Downloads | npm Downloads | Total |\n|------|------------------|---------------|-------|\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
const githubChange = githubTotal - previousGithub
|
||||||
|
const npmChange = npmDownloads - previousNpm
|
||||||
|
const totalChange = total - previousTotal
|
||||||
|
|
||||||
|
const githubChangeStr =
|
||||||
|
githubChange > 0
|
||||||
|
? ` (+${githubChange.toLocaleString()})`
|
||||||
|
: githubChange < 0
|
||||||
|
? ` (${githubChange.toLocaleString()})`
|
||||||
|
: " (+0)"
|
||||||
|
const npmChangeStr =
|
||||||
|
npmChange > 0
|
||||||
|
? ` (+${npmChange.toLocaleString()})`
|
||||||
|
: npmChange < 0
|
||||||
|
? ` (${npmChange.toLocaleString()})`
|
||||||
|
: " (+0)"
|
||||||
|
const totalChangeStr =
|
||||||
|
totalChange > 0
|
||||||
|
? ` (+${totalChange.toLocaleString()})`
|
||||||
|
: totalChange < 0
|
||||||
|
? ` (${totalChange.toLocaleString()})`
|
||||||
|
: " (+0)"
|
||||||
|
const line = `| ${date} | ${githubTotal.toLocaleString()}${githubChangeStr} | ${npmDownloads.toLocaleString()}${npmChangeStr} | ${total.toLocaleString()}${totalChangeStr} |\n`
|
||||||
|
|
||||||
|
if (!content.includes("# Download Stats")) {
|
||||||
|
content =
|
||||||
|
"# Download Stats\n\n| Date | GitHub Downloads | npm Downloads | Total |\n|------|------------------|---------------|-------|\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
await Bun.write(file, content + line)
|
||||||
|
await Bun.spawn(["bunx", "prettier", "--write", file]).exited
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`\nAppended stats to ${file}: GitHub ${githubTotal.toLocaleString()}${githubChangeStr}, npm ${npmDownloads.toLocaleString()}${npmChangeStr}, Total ${total.toLocaleString()}${totalChangeStr}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Fetching GitHub releases for sst/opencode...\n")
|
||||||
|
|
||||||
|
const releases = await fetchReleases()
|
||||||
|
console.log(`\nFetched ${releases.length} releases total\n`)
|
||||||
|
|
||||||
|
const { total: githubTotal, stats } = calculate(releases)
|
||||||
|
|
||||||
|
console.log("Fetching npm all-time downloads for opencode-ai...\n")
|
||||||
|
const npmDownloads = await fetchNpmDownloads("opencode-ai")
|
||||||
|
console.log(
|
||||||
|
`Fetched npm all-time downloads: ${npmDownloads.toLocaleString()}\n`,
|
||||||
|
)
|
||||||
|
|
||||||
|
await save(githubTotal, npmDownloads)
|
||||||
|
|
||||||
|
const totalDownloads = githubTotal + npmDownloads
|
||||||
|
|
||||||
|
console.log("=".repeat(60))
|
||||||
|
console.log(`TOTAL DOWNLOADS: ${totalDownloads.toLocaleString()}`)
|
||||||
|
console.log(` GitHub: ${githubTotal.toLocaleString()}`)
|
||||||
|
console.log(` npm: ${npmDownloads.toLocaleString()}`)
|
||||||
|
console.log("=".repeat(60))
|
||||||
|
|
||||||
|
console.log("\nDownloads by release:")
|
||||||
|
console.log("-".repeat(60))
|
||||||
|
|
||||||
|
stats
|
||||||
|
.sort((a, b) => b.downloads - a.downloads)
|
||||||
|
.forEach((release) => {
|
||||||
|
console.log(
|
||||||
|
`${release.tag.padEnd(15)} ${release.downloads.toLocaleString().padStart(10)} downloads`,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (release.assets.length > 1) {
|
||||||
|
release.assets
|
||||||
|
.sort((a, b) => b.downloads - a.downloads)
|
||||||
|
.forEach((asset) => {
|
||||||
|
console.log(
|
||||||
|
` └─ ${asset.name.padEnd(25)} ${asset.downloads.toLocaleString().padStart(8)}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log("-".repeat(60))
|
||||||
|
console.log(
|
||||||
|
`GitHub Total: ${githubTotal.toLocaleString()} downloads across ${releases.length} releases`,
|
||||||
|
)
|
||||||
|
console.log(`npm Total: ${npmDownloads.toLocaleString()} downloads`)
|
||||||
|
console.log(`Combined Total: ${totalDownloads.toLocaleString()} downloads`)
|
|
@ -1 +1,5 @@
|
||||||
{}
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "@tsconfig/bun/tsconfig.json",
|
||||||
|
"compilerOptions": {}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue