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
64 changes: 64 additions & 0 deletions .github/workflows/star-history.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Star History

on:
schedule:
- cron: "17 2 * * *"
workflow_dispatch:

permissions:
contents: write

concurrency:
group: star-history
cancel-in-progress: true

jobs:
update:
if: github.repository == 'Devin-AXIS/iPolloWork'
runs-on: ubuntu-latest
steps:
- name: Check out updater
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
path: source
persist-credentials: false

- name: Check out chart data
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: star-history
path: chart

- name: Read current star count
id: repository
env:
GH_TOKEN: ${{ github.token }}
run: |
response="$(curl --fail --silent --show-error \
--header "Accept: application/vnd.github+json" \
--header "Authorization: Bearer ${GH_TOKEN}" \
--header "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/${GITHUB_REPOSITORY}")"
count="$(jq --exit-status '.stargazers_count | select(type == "number" and . >= 0)' <<<"${response}")"
echo "count=${count}" >> "${GITHUB_OUTPUT}"

- name: Render chart
run: |
node source/scripts/update-star-history.mjs \
--repository "${GITHUB_REPOSITORY}" \
--count "${{ steps.repository.outputs.count }}" \
--data chart/docs/star-history/data.json \
--output-dir chart/docs/star-history

- name: Publish changed chart
working-directory: chart
run: |
if git diff --quiet -- docs/star-history; then
echo "Star history is already current."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add docs/star-history/data.json docs/star-history/star-history-light.svg docs/star-history/star-history-dark.svg
git commit -m "chore: update star history [skip ci]"
git push origin HEAD:star-history
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,18 @@ git diff --check

See `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, and `SECURITY.md` for contribution, community, and security policies.

## Star History

<p align="center">
<a href="https://www.star-history.com/?repos=Devin-AXIS%2FiPolloWork&amp;type=date&amp;legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Devin-AXIS/iPolloWork/star-history/docs/star-history/star-history-dark.svg" />
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Devin-AXIS/iPolloWork/star-history/docs/star-history/star-history-light.svg" />
<img alt="iPolloWork Star History" src="https://raw.githubusercontent.com/Devin-AXIS/iPolloWork/star-history/docs/star-history/star-history-light.svg" width="900" />
</picture>
</a>
</p>

## License

iPolloWork uses the **iPolloWork Source Available License 1.0**:
Expand Down
12 changes: 12 additions & 0 deletions docs/translations/README_JA.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,18 @@ git diff --check

コントリビューション、コミュニティ、セキュリティに関する方針は、`CONTRIBUTING.md`、`CODE_OF_CONDUCT.md`、`SECURITY.md` を参照してください。

## Star の推移

<p align="center">
<a href="https://www.star-history.com/?repos=Devin-AXIS%2FiPolloWork&amp;type=date&amp;legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Devin-AXIS/iPolloWork/star-history/docs/star-history/star-history-dark.svg" />
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Devin-AXIS/iPolloWork/star-history/docs/star-history/star-history-light.svg" />
<img alt="iPolloWork Star の推移" src="https://raw.githubusercontent.com/Devin-AXIS/iPolloWork/star-history/docs/star-history/star-history-light.svg" width="900" />
</picture>
</a>
</p>

## ライセンス

iPolloWork は **iPolloWork Source Available License 1.0** を使用します。
Expand Down
12 changes: 12 additions & 0 deletions docs/translations/README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,18 @@ iPolloWork 桌面/UI ──> iPolloWork Server ──> OpenCode
- 不连接 Cloud 时,iPolloWork 仍可完整本地运行。
- iPolloWork 不修改 OpenCode,OpenCode 可以继续独立升级。

## Star 增长趋势

<p align="center">
<a href="https://www.star-history.com/?repos=Devin-AXIS%2FiPolloWork&amp;type=date&amp;legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Devin-AXIS/iPolloWork/star-history/docs/star-history/star-history-dark.svg" />
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Devin-AXIS/iPolloWork/star-history/docs/star-history/star-history-light.svg" />
<img alt="iPolloWork Star 增长趋势" src="https://raw.githubusercontent.com/Devin-AXIS/iPolloWork/star-history/docs/star-history/star-history-light.svg" width="900" />
</picture>
</a>
</p>

## 使用许可

- 仅个人自己使用免费;少于 3 名总用户的小规模内部使用免费。
Expand Down
12 changes: 12 additions & 0 deletions docs/translations/README_ZH_hk.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,18 @@ iPolloWork 桌面/UI ──> iPolloWork Server ──> OpenCode
- 不連接 Cloud 時,iPolloWork 仍可完整本地運行。
- iPolloWork 不修改 OpenCode,OpenCode 可以繼續獨立升級。

## Star 增長趨勢

<p align="center">
<a href="https://www.star-history.com/?repos=Devin-AXIS%2FiPolloWork&amp;type=date&amp;legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Devin-AXIS/iPolloWork/star-history/docs/star-history/star-history-dark.svg" />
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Devin-AXIS/iPolloWork/star-history/docs/star-history/star-history-light.svg" />
<img alt="iPolloWork Star 增長趨勢" src="https://raw.githubusercontent.com/Devin-AXIS/iPolloWork/star-history/docs/star-history/star-history-light.svg" width="900" />
</picture>
</a>
</p>

## 使用許可

- 僅個人自己使用免費;少於 3 名總用户的小規模內部使用免費。
Expand Down
186 changes: 186 additions & 0 deletions scripts/update-star-history.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
#!/usr/bin/env node

import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";

const args = new Map();
for (let index = 2; index < process.argv.length; index += 2) {
const key = process.argv[index];
const value = process.argv[index + 1];
if (!key?.startsWith("--") || value === undefined) {
throw new Error(`Invalid argument near ${key ?? "end of command"}`);
}
args.set(key.slice(2), value);
}

const repository = args.get("repository");
const count = Number(args.get("count"));
const dataPath = args.get("data");
const outputDir = args.get("output-dir");
const updatedAt = args.get("updated-at") ?? new Date().toISOString();

if (!repository?.match(/^[^/]+\/[^/]+$/)) {
throw new Error("--repository must use owner/name format");
}
if (!Number.isSafeInteger(count) || count < 0) {
throw new Error("--count must be a non-negative integer");
}
if (!dataPath || !outputDir || Number.isNaN(Date.parse(updatedAt))) {
throw new Error("--data, --output-dir, and a valid --updated-at are required");
}

const data = JSON.parse(await readFile(dataPath, "utf8"));
if (data.repository !== repository || !Array.isArray(data.points)) {
throw new Error("Star history data does not match the requested repository");
}

const date = updatedAt.slice(0, 10);
const lastPoint = data.points.at(-1);
if (lastPoint && date < lastPoint.date) {
throw new Error(`Refusing to insert ${date} before existing point ${lastPoint.date}`);
}
if (lastPoint?.date === date && lastPoint.count !== count) {
lastPoint.count = count;
data.updatedAt = updatedAt;
} else if (lastPoint?.date !== date) {
data.points.push({ date, count });
data.updatedAt = updatedAt;
}

await mkdir(outputDir, { recursive: true });
await writeFile(dataPath, `${JSON.stringify(data, null, 2)}\n`);
await Promise.all([
writeFile(path.join(outputDir, "star-history-light.svg"), renderChart(data, "light")),
writeFile(path.join(outputDir, "star-history-dark.svg"), renderChart(data, "dark")),
]);

function renderChart(history, themeName) {
const theme =
themeName === "dark"
? {
background: "#0d1117",
grid: "#30363d",
muted: "#8b949e",
text: "#f0f6fc",
line: "#ff7b54",
fill: "#ff7b54",
}
: {
background: "#ffffff",
grid: "#d8dee4",
muted: "#57606a",
text: "#24292f",
line: "#f05a37",
fill: "#f05a37",
};
const width = 900;
const height = 500;
const margin = { top: 88, right: 40, bottom: 64, left: 76 };
const plotWidth = width - margin.left - margin.right;
const plotHeight = height - margin.top - margin.bottom;
const parsed = history.points.map((point) => ({
count: point.count,
date: point.date,
time: Date.parse(`${point.date}T00:00:00Z`),
}));
if (parsed.length === 0) {
throw new Error("Star history needs at least one point");
}

const minTime = parsed[0].time;
const maxTime = parsed.at(-1).time;
const maxCount = Math.max(...parsed.map((point) => point.count), 1);
const yMax = niceCeiling(maxCount);
const timeSpan = Math.max(maxTime - minTime, 1);
const sampled = samplePoints(parsed, 720);
const x = (time) => margin.left + ((time - minTime) / timeSpan) * plotWidth;
const y = (value) => margin.top + plotHeight - (value / yMax) * plotHeight;
const linePath = sampled
.map((point, index) => `${index === 0 ? "M" : "L"}${x(point.time).toFixed(1)} ${y(point.count).toFixed(1)}`)
.join(" ");
const fillPath = `${linePath} L${x(sampled.at(-1).time).toFixed(1)} ${margin.top + plotHeight} L${x(sampled[0].time).toFixed(1)} ${margin.top + plotHeight} Z`;
const identifier = `star-history-${themeName}`;
const latest = parsed.at(-1);

const yTicks = Array.from({ length: 6 }, (_, index) => (yMax * index) / 5);
const xTicks = Array.from({ length: 5 }, (_, index) => minTime + (timeSpan * index) / 4);
const grid = yTicks
.map(
(value) =>
`<line x1="${margin.left}" y1="${y(value).toFixed(1)}" x2="${width - margin.right}" y2="${y(value).toFixed(1)}" stroke="${theme.grid}" stroke-width="1" />` +
`<text x="${margin.left - 14}" y="${(y(value) + 5).toFixed(1)}" fill="${theme.muted}" font-size="13" text-anchor="end">${formatCount(value)}</text>`,
)
.join("");
const dates = xTicks
.map(
(time, index) =>
`<text x="${x(time).toFixed(1)}" y="${height - 26}" fill="${theme.muted}" font-size="13" text-anchor="${index === 0 ? "start" : index === xTicks.length - 1 ? "end" : "middle"}">${formatDate(time)}</text>`,
)
.join("");

return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-labelledby="${identifier}-title ${identifier}-description">
<title id="${identifier}-title">${escapeXml(history.repository)} star history</title>
<desc id="${identifier}-description">${formatCount(latest.count)} stars as of ${escapeXml(latest.date)}</desc>
<defs>
<linearGradient id="${identifier}-fill" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="${theme.fill}" stop-opacity="0.26" />
<stop offset="100%" stop-color="${theme.fill}" stop-opacity="0.02" />
</linearGradient>
</defs>
<rect width="${width}" height="${height}" rx="16" fill="${theme.background}" />
<text x="${margin.left}" y="38" fill="${theme.text}" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-size="24" font-weight="700">Star History</text>
<text x="${margin.left}" y="65" fill="${theme.muted}" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" font-size="14">${escapeXml(history.repository)} · ${formatCount(latest.count)} stars · Updated ${escapeXml(latest.date)}</text>
<g font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif">
${grid}
${dates}
<path d="${fillPath}" fill="url(#${identifier}-fill)" />
<path d="${linePath}" fill="none" stroke="${theme.line}" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" />
<circle cx="${x(latest.time).toFixed(1)}" cy="${y(latest.count).toFixed(1)}" r="5" fill="${theme.background}" stroke="${theme.line}" stroke-width="3" />
<text x="${width - margin.right}" y="65" fill="${theme.muted}" font-size="13" text-anchor="end">Interactive chart: star-history.com</text>
</g>
</svg>
`;
}

function samplePoints(points, limit) {
if (points.length <= limit) return points;
const result = [];
for (let index = 0; index < limit; index += 1) {
result.push(points[Math.round((index * (points.length - 1)) / (limit - 1))]);
}
return result;
}

function niceCeiling(value) {
const magnitude = 10 ** Math.floor(Math.log10(value));
const normalized = value / magnitude;
const ceiling = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10;
return ceiling * magnitude;
}

function formatCount(value) {
if (value >= 1_000_000) return `${trimDecimal(value / 1_000_000)}M`;
if (value >= 1_000) return `${trimDecimal(value / 1_000)}K`;
return String(Math.round(value));
}

function trimDecimal(value) {
return value >= 10 || Number.isInteger(value) ? value.toFixed(0) : value.toFixed(1);
}

function formatDate(time) {
return new Intl.DateTimeFormat("en", {
month: "short",
timeZone: "UTC",
year: "numeric",
}).format(new Date(time));
}

function escapeXml(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&apos;");
}
Loading