chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
---
|
||||
name: commit-smart
|
||||
description: Analyze staged/unstaged changes and create semantic conventional commits with context about WHY, not just WHAT. Auto-detects commit type and scope from the diff. Supports optional type/scope arguments. Usage - /commit-smart, /commit-smart fix, /commit-smart refactor api
|
||||
---
|
||||
|
||||
# Smart Commit
|
||||
|
||||
Create meaningful conventional commits by analyzing your actual changes.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Assess the working tree
|
||||
|
||||
Run these commands to understand the current state:
|
||||
|
||||
```bash
|
||||
git status
|
||||
git diff --stat
|
||||
git diff --cached --stat
|
||||
```
|
||||
|
||||
### Step 2: Handle unstaged changes
|
||||
|
||||
If nothing is staged (`git diff --cached` is empty):
|
||||
|
||||
1. Show the user what files have changed
|
||||
2. Suggest what to stage based on logical grouping (e.g., "these 3 files are all related to the auth refactor")
|
||||
3. Ask if they want to stage all, or select specific files
|
||||
4. Stage the approved files with `git add <files>`
|
||||
|
||||
If changes are already staged, proceed to analysis.
|
||||
|
||||
### Step 3: Analyze the diff
|
||||
|
||||
Read the full staged diff:
|
||||
|
||||
```bash
|
||||
git diff --cached
|
||||
```
|
||||
|
||||
Determine the commit type from the changes:
|
||||
|
||||
| Signal | Type |
|
||||
|--------|------|
|
||||
| New files with new functionality | `feat` |
|
||||
| New test files or test additions | `test` |
|
||||
| Changes to existing logic fixing incorrect behavior | `fix` |
|
||||
| Structural changes without behavior change | `refactor` |
|
||||
| package.json, tsconfig, CI config changes | `chore` |
|
||||
| Build/bundler config changes | `build` |
|
||||
| README, docs, comments only | `docs` |
|
||||
| Formatting, whitespace, semicolons only | `style` |
|
||||
| Performance improvements | `perf` |
|
||||
|
||||
Determine the scope from the primary directory or module affected:
|
||||
- `src/api/` -> `api`
|
||||
- `src/components/auth/` -> `auth`
|
||||
- `tests/` -> `tests`
|
||||
- Root config files -> omit scope
|
||||
- Multiple unrelated areas -> omit scope
|
||||
|
||||
### Step 4: Check for user overrides
|
||||
|
||||
If the user provided arguments via `$ARGUMENTS`:
|
||||
- Single word (e.g., `fix`) -> use as commit type
|
||||
- Two words (e.g., `refactor api`) -> use as type and scope
|
||||
- Otherwise -> use auto-detected values
|
||||
|
||||
### Step 5: Compose the commit message
|
||||
|
||||
Format: `type(scope): imperative short description`
|
||||
|
||||
Rules:
|
||||
- Subject line max 72 characters
|
||||
- Use imperative mood ("add", "fix", "refactor", not "added", "fixes")
|
||||
- Don't end with a period
|
||||
- Body explains **WHY** this change was made, not what changed (the diff shows what)
|
||||
- If changes are trivial (typo fix, formatting), skip the body
|
||||
|
||||
Example:
|
||||
```
|
||||
feat(auth): add JWT refresh token rotation
|
||||
|
||||
Tokens were expiring mid-session for users with slow connections.
|
||||
Rotating refresh tokens extends the session without compromising
|
||||
security, since each refresh token can only be used once.
|
||||
```
|
||||
|
||||
### Step 6: Confirm and commit
|
||||
|
||||
Show the user the proposed commit message and ask for confirmation.
|
||||
|
||||
If confirmed, run:
|
||||
```bash
|
||||
git commit -m "<message>"
|
||||
```
|
||||
|
||||
Then verify with:
|
||||
```bash
|
||||
git log --oneline -1
|
||||
```
|
||||
|
||||
Show the committed hash and message.
|
||||
|
||||
## Tips
|
||||
|
||||
- Run after completing a logical unit of work, not after every file change
|
||||
- If the diff is too large for one commit, suggest splitting into multiple commits
|
||||
- For breaking changes, add `!` after the scope: `feat(api)!: change response format`
|
||||
- The body should answer "if someone reads this commit in 6 months, will they understand WHY?"
|
||||
@@ -0,0 +1,152 @@
|
||||
---
|
||||
name: gcc
|
||||
description: "Git Context Controller (GCC) - Manages agent memory as a versioned file system under .GCC/. This skill should be used when working on multi-step projects that benefit from structured memory persistence, milestone tracking, branching for alternative approaches, and cross-session context recovery. Triggers on /gcc commands or natural language like 'commit this progress', 'branch to try an alternative', 'merge results', 'recover context'."
|
||||
---
|
||||
|
||||
# Git Context Controller (GCC)
|
||||
|
||||
## Overview
|
||||
|
||||
GCC transforms agent memory from a passive token stream into a structured, versioned file system under `.GCC/`. Inspired by Git, it provides four operations — COMMIT, BRANCH, MERGE, CONTEXT — to persist milestones, explore alternatives in isolation, synthesize results, and recover historical context efficiently.
|
||||
|
||||
## Initialization
|
||||
|
||||
On first use, check if `.GCC/` exists in the project root. If not, run `scripts/gcc_init.sh` to create the directory structure:
|
||||
|
||||
```
|
||||
.GCC/
|
||||
├── main.md # Global roadmap and objectives
|
||||
├── metadata.yaml # Infrastructure state (branches, file tree, config)
|
||||
├── commit.md # Commit history for main branch
|
||||
├── log.md # OTA execution log for main branch
|
||||
└── branches/ # Isolated workspaces for experiments
|
||||
└── <branch-name>/
|
||||
├── commit.md
|
||||
├── log.md
|
||||
└── summary.md
|
||||
```
|
||||
|
||||
For detailed file format specifications, read `references/file_formats.md`.
|
||||
|
||||
## Configuration
|
||||
|
||||
GCC behavior is controlled via `metadata.yaml`:
|
||||
|
||||
- `proactive_commits: true` — Automatically suggest commits after completing coherent sub-tasks
|
||||
- `proactive_commits: false` — Only commit when explicitly requested
|
||||
|
||||
Toggle with: "enable/disable proactive commits" or by editing `metadata.yaml`.
|
||||
|
||||
## Commands
|
||||
|
||||
### COMMIT
|
||||
|
||||
Persist a milestone on the current branch.
|
||||
|
||||
**Triggers**: `/gcc commit <summary>`, "commit this progress", "save this milestone", "checkpoint"
|
||||
|
||||
**Procedure**:
|
||||
1. Read the current branch's `commit.md` to determine the next commit number
|
||||
2. Append a new entry to `commit.md` with:
|
||||
- Sequential ID (e.g., `[C004]`)
|
||||
- Date (UTC ISO 8601)
|
||||
- Current branch name
|
||||
- Branch purpose (from `summary.md` if on a branch, or from `main.md`)
|
||||
- Previous progress summary (1-2 sentences from last commit)
|
||||
- This commit's contribution (detailed technical description with files touched)
|
||||
3. Append an OTA entry to `log.md` recording the commit action
|
||||
4. Update `metadata.yaml` file tree if files were created/modified
|
||||
5. If on main branch, update milestones section in `main.md`
|
||||
|
||||
**Proactive behavior**: When `proactive_commits: true`, suggest a commit after:
|
||||
- Completing a function, module, or coherent unit of work
|
||||
- Fixing a bug and verifying the fix
|
||||
- Finishing a research/exploration phase with conclusions
|
||||
- Any point where losing context would mean re-doing significant work
|
||||
|
||||
### BRANCH
|
||||
|
||||
Create an isolated workspace for exploring an alternative approach.
|
||||
|
||||
**Triggers**: `/gcc branch <name>`, "branch to try...", "explore alternative...", "experiment with..."
|
||||
|
||||
**Procedure**:
|
||||
1. Create `.GCC/branches/<branch-name>/` directory
|
||||
2. Create `summary.md` with: purpose, parent branch, creation date, key hypotheses
|
||||
3. Create empty `commit.md` and `log.md` for the branch
|
||||
4. Update `metadata.yaml` to register the new branch
|
||||
5. Update `main.md` Active Branches section
|
||||
6. Log the branch creation in the parent branch's `log.md`
|
||||
|
||||
From this point, all COMMITs and OTA logs go to the branch-specific files until a MERGE or explicit branch switch.
|
||||
|
||||
### MERGE
|
||||
|
||||
Integrate a completed branch back into the main flow.
|
||||
|
||||
**Triggers**: `/gcc merge <branch>`, "merge results from...", "integrate the experiment", "branch X is done"
|
||||
|
||||
**Procedure**:
|
||||
1. Read the branch's `summary.md` and `commit.md` to understand outcomes
|
||||
2. Append a synthesis commit to main's `commit.md` summarizing:
|
||||
- What was tried
|
||||
- What was learned
|
||||
- What is being integrated (or why the branch is being abandoned)
|
||||
3. Update `main.md`:
|
||||
- Add milestone entry with branch results
|
||||
- Remove from Active Branches
|
||||
- Update objectives if applicable
|
||||
4. Update `metadata.yaml`: set branch status to `merged` or `abandoned`
|
||||
5. Log the merge in main's `log.md`
|
||||
|
||||
### CONTEXT
|
||||
|
||||
Retrieve historical memory at different resolution levels.
|
||||
|
||||
**Triggers**: `/gcc context <flag>`, "what did we do on...", "recover context", "show me the history", "where were we"
|
||||
|
||||
**Flags**:
|
||||
|
||||
- `--branch [name]` — Read `summary.md` and latest commits for a specific branch (or current branch if no name). Provides high-level understanding of what happened and why.
|
||||
|
||||
- `--log [n]` — Read last N entries (default 20) from the current branch's `log.md`. Provides fine-grained OTA traces for debugging or resuming interrupted work.
|
||||
|
||||
- `--metadata` — Read `metadata.yaml` to recover project structure: file tree, dependencies, active branches, configuration.
|
||||
|
||||
- `--full` — Read `main.md` for the complete project roadmap, all milestones, and active branches. Use for cross-session recovery or handoff to another agent.
|
||||
|
||||
When no flag is specified, default to `--branch` for the current active branch.
|
||||
|
||||
## OTA Logging
|
||||
|
||||
Throughout all work (not just during explicit commands), maintain the OTA execution log:
|
||||
|
||||
1. **Observation**: What was noticed or discovered
|
||||
2. **Thought**: Reasoning about what to do next
|
||||
3. **Action**: What action was taken
|
||||
|
||||
Append entries to the active branch's `log.md`. Keep a maximum of 50 entries; when exceeding, remove the oldest entries. Each entry includes a sequential ID, timestamp, and branch name.
|
||||
|
||||
Log OTA entries at meaningful decision points — not every single action, but significant observations, strategy changes, and outcomes.
|
||||
|
||||
## Cross-Session Recovery
|
||||
|
||||
When starting a new session on an existing project with `.GCC/`:
|
||||
|
||||
1. Read `metadata.yaml` to understand project state and active branches
|
||||
2. Read `main.md` for the global roadmap and objectives
|
||||
3. Read the active branch's latest commits and log entries
|
||||
4. Resume work with full context of what was accomplished and what remains
|
||||
|
||||
## Natural Language Mapping
|
||||
|
||||
| User says | Command |
|
||||
|---|---|
|
||||
| "save/checkpoint/persist this" | COMMIT |
|
||||
| "try a different approach" | BRANCH |
|
||||
| "that experiment worked, integrate it" | MERGE |
|
||||
| "where were we?" / "what's the status?" | CONTEXT --full |
|
||||
| "what happened on branch X?" | CONTEXT --branch X |
|
||||
| "show recent activity" | CONTEXT --log |
|
||||
| "what files do we have?" | CONTEXT --metadata |
|
||||
| "enable/disable auto-commits" | Toggle `proactive_commits` in metadata.yaml |
|
||||
@@ -0,0 +1,120 @@
|
||||
# GCC File Format Reference
|
||||
|
||||
## main.md
|
||||
|
||||
The global roadmap. Updated on every MERGE and periodically on significant COMMITs.
|
||||
|
||||
```markdown
|
||||
# Project Roadmap
|
||||
|
||||
## Objectives
|
||||
- [ ] Objective 1
|
||||
- [x] Objective 2 (completed)
|
||||
|
||||
## Milestones
|
||||
### M1: Feature X implemented
|
||||
- Branch: feature-x
|
||||
- Commits: 3
|
||||
- Status: merged
|
||||
|
||||
### M2: Bug fix Y
|
||||
- Branch: fix-y
|
||||
- Commits: 1
|
||||
- Status: active
|
||||
|
||||
## Active Branches
|
||||
- `experiment-z`: Testing alternative approach for caching
|
||||
```
|
||||
|
||||
## commit.md
|
||||
|
||||
Each commit entry captures the full reasoning context, not just a diff summary.
|
||||
|
||||
```markdown
|
||||
## [C003] Implement retry logic for API calls
|
||||
- **Date**: 2025-01-15T10:30:00Z
|
||||
- **Branch**: feature-resilience
|
||||
- **Branch Purpose**: Add fault tolerance to external API integrations
|
||||
- **Previous Progress**: Identified failure patterns in logs; designed retry strategy with exponential backoff
|
||||
- **This Commit's Contribution**: Implemented `retry_with_backoff(fn, max_retries=3)` in `utils/http.py`. Added unit tests covering timeout, 5xx, and network error scenarios. Validated against staging API.
|
||||
- **Files touched**: utils/http.py, tests/test_http.py
|
||||
```
|
||||
|
||||
## log.md
|
||||
|
||||
Fine-grained OTA (Observation-Thought-Action) trace entries. Keep the last 50 entries maximum.
|
||||
|
||||
```markdown
|
||||
---
|
||||
**[OTA-042]** 2025-01-15T10:15:00Z | Branch: feature-resilience
|
||||
- **Observation**: API calls to /users endpoint failing with 503 errors intermittently
|
||||
- **Thought**: Need exponential backoff rather than fixed delay; should cap at 3 retries to avoid infinite loops
|
||||
- **Action**: Implementing retry_with_backoff() in utils/http.py
|
||||
|
||||
---
|
||||
**[OTA-043]** 2025-01-15T10:28:00Z | Branch: feature-resilience
|
||||
- **Observation**: Tests passing for timeout and 5xx scenarios
|
||||
- **Thought**: Ready to commit this milestone - retry logic is complete and validated
|
||||
- **Action**: COMMIT with summary of retry implementation
|
||||
```
|
||||
|
||||
## metadata.yaml
|
||||
|
||||
Structured infrastructure state. Updated on every operation.
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
created: "2025-01-10T08:00:00Z"
|
||||
proactive_commits: true
|
||||
branches:
|
||||
- name: main
|
||||
status: active
|
||||
created: "2025-01-10T08:00:00Z"
|
||||
- name: feature-resilience
|
||||
status: active
|
||||
created: "2025-01-15T09:00:00Z"
|
||||
parent: main
|
||||
- name: experiment-cache
|
||||
status: abandoned
|
||||
created: "2025-01-12T14:00:00Z"
|
||||
reason: "Redis dependency too heavy for this use case"
|
||||
file_tree:
|
||||
- src/main.py
|
||||
- src/utils/http.py
|
||||
- tests/test_http.py
|
||||
dependencies:
|
||||
- requests>=2.28
|
||||
- pytest>=7.0
|
||||
config:
|
||||
language: python
|
||||
framework: fastapi
|
||||
```
|
||||
|
||||
## Branch Directory Structure
|
||||
|
||||
Each branch under `.GCC/branches/<branch-name>/` contains:
|
||||
|
||||
```
|
||||
.GCC/branches/feature-resilience/
|
||||
├── commit.md # Commits specific to this branch
|
||||
├── log.md # OTA traces for this branch
|
||||
└── summary.md # Branch purpose and current status
|
||||
```
|
||||
|
||||
### summary.md (per-branch)
|
||||
|
||||
```markdown
|
||||
# Branch: feature-resilience
|
||||
|
||||
## Purpose
|
||||
Add fault tolerance to external API integrations to handle intermittent 503 errors.
|
||||
|
||||
## Status: active
|
||||
## Parent: main
|
||||
## Created: 2025-01-15T09:00:00Z
|
||||
|
||||
## Key Decisions
|
||||
- Using exponential backoff (not fixed delay)
|
||||
- Max 3 retries to prevent cascade failures
|
||||
- Logging all retry attempts for observability
|
||||
```
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/bin/bash
|
||||
# GCC Initialization Script
|
||||
# Creates the .GCC/ directory structure for agent memory management
|
||||
|
||||
set -e
|
||||
|
||||
GCC_DIR="${1:-.GCC}"
|
||||
|
||||
if [ -d "$GCC_DIR" ]; then
|
||||
echo "GCC already initialized at $GCC_DIR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "$GCC_DIR/branches"
|
||||
|
||||
# Create main.md - the global roadmap
|
||||
cat > "$GCC_DIR/main.md" << 'EOF'
|
||||
# Project Roadmap
|
||||
|
||||
## Objectives
|
||||
- [ ] (Define project objectives here)
|
||||
|
||||
## Milestones
|
||||
(Milestones will be populated as commits are made)
|
||||
|
||||
## Active Branches
|
||||
(No active branches)
|
||||
EOF
|
||||
|
||||
# Create metadata.yaml - infrastructure state
|
||||
cat > "$GCC_DIR/metadata.yaml" << EOF
|
||||
version: 1
|
||||
created: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
proactive_commits: true
|
||||
branches:
|
||||
- name: main
|
||||
status: active
|
||||
created: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
file_tree: []
|
||||
dependencies: []
|
||||
EOF
|
||||
|
||||
# Create initial commit.md
|
||||
INIT_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
cat > "$GCC_DIR/commit.md" << EOF
|
||||
# Commit History
|
||||
|
||||
## [INIT] Project initialized
|
||||
- **Date**: $INIT_DATE
|
||||
- **Branch**: main
|
||||
- **Summary**: GCC memory system initialized.
|
||||
EOF
|
||||
|
||||
# Create initial log.md
|
||||
cat > "$GCC_DIR/log.md" << 'EOF'
|
||||
# OTA Execution Log
|
||||
|
||||
> Most recent entries at the bottom. Keep last 50 entries max.
|
||||
|
||||
---
|
||||
EOF
|
||||
|
||||
echo "GCC initialized successfully at $GCC_DIR/"
|
||||
echo "Structure:"
|
||||
echo " $GCC_DIR/main.md - Global roadmap"
|
||||
echo " $GCC_DIR/metadata.yaml - Infrastructure state"
|
||||
echo " $GCC_DIR/commit.md - Commit history"
|
||||
echo " $GCC_DIR/log.md - OTA execution log"
|
||||
echo " $GCC_DIR/branches/ - Isolated workspaces"
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
name: star-history-chart
|
||||
description: Add a self-hosted "Stargazers over time" chart to any GitHub repo's README. GitHub now restricts the stargazers endpoint to a repo's own admins/collaborators, so third-party live services (star-history free tier, starchart.cc) return "Requires authentication" for everyone. This generates a static, theme-aware SVG in-repo and auto-refreshes it weekly with a GitHub Action using the repo's own GITHUB_TOKEN. Use when the star chart in a README is broken, shows "Requires authentication", or you want a star history that never breaks.
|
||||
---
|
||||
|
||||
# Star History Chart (self-hosted, never breaks)
|
||||
|
||||
Add a "Stargazers over time" chart that renders from a static SVG committed to
|
||||
the repo and refreshes itself weekly — no external chart service, no broken
|
||||
images.
|
||||
|
||||
## Why this exists
|
||||
|
||||
GitHub now restricts the `/stargazers` endpoint to a repository's own admins and
|
||||
collaborators. Unauthenticated requests return `{"message":"Requires
|
||||
authentication"}`, which breaks every third-party live-chart service
|
||||
(star-history.com free tier, starchart.cc, etc.) for **all** repos. The only
|
||||
reliable fix is to generate the chart yourself with an authenticated token and
|
||||
commit a static image. Inside GitHub Actions, the repo's own `GITHUB_TOKEN` can
|
||||
read its own stargazers, so the whole thing runs with zero secrets to configure.
|
||||
|
||||
## What this skill sets up
|
||||
|
||||
1. `scripts/generate_star_history.py` — fetches stargazers (authenticated),
|
||||
renders a clean, light/dark-adaptive SVG.
|
||||
2. `.github/workflows/star-history.yml` — weekly cron + manual trigger that
|
||||
regenerates and commits `docs/star-history.svg`.
|
||||
3. A README section pointing at the local SVG.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Copy the script and workflow into the repo
|
||||
|
||||
```bash
|
||||
mkdir -p scripts .github/workflows docs
|
||||
cp skills/git/star-history-chart/scripts/generate_star_history.py scripts/generate_star_history.py
|
||||
cp skills/git/star-history-chart/assets/star-history.yml .github/workflows/star-history.yml
|
||||
```
|
||||
|
||||
> The script needs the `requests` package: `pip install requests`.
|
||||
> It resolves the repo from `STAR_HISTORY_REPO`, then `GITHUB_REPOSITORY`
|
||||
> (set automatically in Actions), then the `origin` git remote — so no edits
|
||||
> are required for it to work in a different repo.
|
||||
|
||||
### Step 2: Generate the SVG once, locally
|
||||
|
||||
Use a token that can read the repo's stargazers (as owner/collaborator). The
|
||||
GitHub CLI provides one:
|
||||
|
||||
```bash
|
||||
GITHUB_TOKEN=$(gh auth token) python scripts/generate_star_history.py
|
||||
```
|
||||
|
||||
This writes `docs/star-history.svg`. For a repo with many thousands of stars the
|
||||
first run paginates the whole stargazer list and can take a couple of minutes.
|
||||
|
||||
Verify it rendered (optional, macOS): `qlmanage -t -s 800 -o . docs/star-history.svg`
|
||||
|
||||
### Step 3: Add it to the README
|
||||
|
||||
Add or replace the star chart section. Point the image at the **local** SVG.
|
||||
Set the link target to wherever you want clicks to go (the repo, a docs page, or
|
||||
your own site):
|
||||
|
||||
```markdown
|
||||
## Stargazers over time
|
||||
[](https://github.com/OWNER/REPO/stargazers)
|
||||
```
|
||||
|
||||
If replacing a broken `star-history.com` / `starchart.cc` embed, swap only the
|
||||
image URL to `docs/star-history.svg` and keep or update the link target.
|
||||
|
||||
### Step 4: Commit
|
||||
|
||||
```bash
|
||||
git add scripts/generate_star_history.py .github/workflows/star-history.yml docs/star-history.svg README.md
|
||||
git commit -m "feat(readme): self-hosted stargazers chart with weekly auto-refresh"
|
||||
git push
|
||||
```
|
||||
|
||||
### Step 5: (Optional) Trigger the auto-refresh now
|
||||
|
||||
The workflow runs every Monday at 04:00 UTC. To refresh immediately without
|
||||
waiting: **GitHub → Actions → "Update Star History" → Run workflow**.
|
||||
|
||||
## Customization
|
||||
|
||||
- **Output path** — set `STAR_HISTORY_OUTPUT` (default `docs/star-history.svg`).
|
||||
- **Different repo** — set `STAR_HISTORY_REPO=owner/name`.
|
||||
- **Colors / size** — edit the `.line`, `.area`, `.dot` CSS and `WIDTH`/`HEIGHT`
|
||||
constants near the top of `generate_star_history.py`. The chart is
|
||||
theme-aware via a `prefers-color-scheme: dark` block, so it looks right in
|
||||
both GitHub light and dark modes.
|
||||
- **Refresh cadence** — edit the `cron` expression in the workflow.
|
||||
|
||||
## Notes
|
||||
|
||||
- No secrets to add: the workflow uses the automatic `GITHUB_TOKEN`.
|
||||
- Private repos work too, as long as the token can read the repo.
|
||||
- The script uses only `requests` plus the Python standard library.
|
||||
@@ -0,0 +1,55 @@
|
||||
name: Update Star History
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Every Monday at 4:00 AM UTC
|
||||
- cron: '0 4 * * 1'
|
||||
workflow_dispatch: # Allow manual runs from the GitHub UI
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-star-history:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
pip install requests
|
||||
|
||||
- name: Generate star-history.svg
|
||||
run: python scripts/generate_star_history.py
|
||||
env:
|
||||
# The repo's own GITHUB_TOKEN can read its own stargazers, which
|
||||
# GitHub now restricts to the repository's admins and collaborators.
|
||||
# GITHUB_REPOSITORY is set automatically by Actions.
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check for changes
|
||||
id: check_changes
|
||||
run: |
|
||||
git diff --quiet docs/star-history.svg || echo "changed=true" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Commit and push changes
|
||||
if: steps.check_changes.outputs.changed == 'true'
|
||||
run: |
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git add docs/star-history.svg
|
||||
git commit -m "chore: Update star history chart
|
||||
|
||||
🤖 Generated with [GitHub Actions](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})"
|
||||
git push
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate a self-hosted "Stargazers over time" SVG chart for any repo.
|
||||
|
||||
GitHub now restricts the stargazers endpoint to a repository's own admins and
|
||||
collaborators, so third-party live-chart services (star-history free tier,
|
||||
starchart.cc, ...) return "Requires authentication" for everyone. This script
|
||||
fetches the star data with an authenticated token (in CI, the repo's own
|
||||
GITHUB_TOKEN works because it can read the repo's own stargazers) and renders a
|
||||
static, theme-aware SVG committed to the repository.
|
||||
|
||||
Repo resolution order:
|
||||
1. STAR_HISTORY_REPO env var ("owner/name")
|
||||
2. GITHUB_REPOSITORY env var (set automatically inside GitHub Actions)
|
||||
3. `git remote get-url origin` parsed into "owner/name"
|
||||
|
||||
Usage:
|
||||
GITHUB_TOKEN=xxx python scripts/generate_star_history.py
|
||||
# local test:
|
||||
GITHUB_TOKEN=$(gh auth token) python scripts/generate_star_history.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import requests
|
||||
|
||||
OUTPUT = os.environ.get("STAR_HISTORY_OUTPUT", "docs/star-history.svg")
|
||||
PER_PAGE = 100
|
||||
MAX_PAGES = 400 # GitHub caps stargazers pagination at 400 pages (40k stars)
|
||||
|
||||
WIDTH, HEIGHT = 800, 400
|
||||
PAD_L, PAD_R, PAD_T, PAD_B = 70, 55, 50, 55
|
||||
|
||||
|
||||
def resolve_repo():
|
||||
"""Return "owner/name" for the repository to chart."""
|
||||
repo = os.environ.get("STAR_HISTORY_REPO") or os.environ.get("GITHUB_REPOSITORY")
|
||||
if repo:
|
||||
return repo.strip()
|
||||
try:
|
||||
url = subprocess.check_output(
|
||||
["git", "remote", "get-url", "origin"], text=True
|
||||
).strip()
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
raise SystemExit(
|
||||
"Could not determine the repo. Set STAR_HISTORY_REPO=owner/name."
|
||||
)
|
||||
# git@github.com:owner/name.git or https://github.com/owner/name.git
|
||||
match = re.search(r"github\.com[:/]([^/]+/[^/]+?)(?:\.git)?/?$", url)
|
||||
if not match:
|
||||
raise SystemExit(f"Unrecognized GitHub remote URL: {url}")
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def fetch_stargazers(token, repo):
|
||||
"""Return a sorted list of datetime objects, one per star."""
|
||||
session = requests.Session()
|
||||
session.headers.update({
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Accept": "application/vnd.github.star+json",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
"User-Agent": "star-history-chart-skill",
|
||||
})
|
||||
|
||||
dates = []
|
||||
for page in range(1, MAX_PAGES + 1):
|
||||
resp = session.get(
|
||||
f"https://api.github.com/repos/{repo}/stargazers",
|
||||
params={"per_page": PER_PAGE, "page": page},
|
||||
timeout=30,
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
raise SystemExit(
|
||||
f"GitHub API error {resp.status_code} on page {page}: {resp.text[:200]}"
|
||||
)
|
||||
batch = resp.json()
|
||||
if not batch:
|
||||
break
|
||||
for entry in batch:
|
||||
starred_at = entry.get("starred_at")
|
||||
if starred_at:
|
||||
dates.append(
|
||||
datetime.strptime(starred_at, "%Y-%m-%dT%H:%M:%SZ").replace(
|
||||
tzinfo=timezone.utc
|
||||
)
|
||||
)
|
||||
if len(batch) < PER_PAGE:
|
||||
break
|
||||
|
||||
dates.sort()
|
||||
return dates
|
||||
|
||||
|
||||
def build_series(dates, max_points=100):
|
||||
"""Cumulative (timestamp, count) points, downsampled to max_points."""
|
||||
total = len(dates)
|
||||
if total == 0:
|
||||
return []
|
||||
points = [(d, i + 1) for i, d in enumerate(dates)]
|
||||
if total <= max_points:
|
||||
return points
|
||||
step = total / max_points
|
||||
sampled = [points[int(i * step)] for i in range(max_points)]
|
||||
sampled.append(points[-1]) # always include the latest point
|
||||
return sampled
|
||||
|
||||
|
||||
def esc(text):
|
||||
return (
|
||||
str(text)
|
||||
.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
)
|
||||
|
||||
|
||||
def render_svg(series, repo):
|
||||
if not series:
|
||||
raise SystemExit("No stargazer data to render.")
|
||||
|
||||
t_min = series[0][0].timestamp()
|
||||
t_max = series[-1][0].timestamp()
|
||||
c_max = series[-1][1]
|
||||
t_span = max(t_max - t_min, 1)
|
||||
|
||||
plot_w = WIDTH - PAD_L - PAD_R
|
||||
plot_h = HEIGHT - PAD_T - PAD_B
|
||||
|
||||
def x(ts):
|
||||
return PAD_L + (ts - t_min) / t_span * plot_w
|
||||
|
||||
def y(count):
|
||||
return PAD_T + plot_h - (count / c_max) * plot_h
|
||||
|
||||
pts = [(x(ts.timestamp()), y(c)) for ts, c in series]
|
||||
line = " ".join(f"{px:.1f},{py:.1f}" for px, py in pts)
|
||||
area = (
|
||||
f"{PAD_L:.1f},{PAD_T + plot_h:.1f} "
|
||||
+ line
|
||||
+ f" {pts[-1][0]:.1f},{PAD_T + plot_h:.1f}"
|
||||
)
|
||||
|
||||
# Y axis ticks (5 gridlines)
|
||||
y_ticks = []
|
||||
for i in range(5):
|
||||
val = round(c_max * i / 4)
|
||||
y_ticks.append((y(val), val))
|
||||
|
||||
# X axis ticks (dates)
|
||||
x_ticks = []
|
||||
for i in range(5):
|
||||
ts = t_min + t_span * i / 4
|
||||
label = datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%b %Y")
|
||||
x_ticks.append((x(ts), label))
|
||||
|
||||
grid_lines = ""
|
||||
for gy, val in y_ticks:
|
||||
grid_lines += (
|
||||
f'<line x1="{PAD_L}" y1="{gy:.1f}" x2="{WIDTH - PAD_R}" y2="{gy:.1f}" '
|
||||
f'class="grid"/>\n'
|
||||
f'<text x="{PAD_L - 10}" y="{gy + 4:.1f}" text-anchor="end" '
|
||||
f'class="tick">{val:,}</text>\n'
|
||||
)
|
||||
x_labels = ""
|
||||
for gx, label in x_ticks:
|
||||
x_labels += (
|
||||
f'<text x="{gx:.1f}" y="{HEIGHT - PAD_B + 22}" text-anchor="middle" '
|
||||
f'class="tick">{esc(label)}</text>\n'
|
||||
)
|
||||
|
||||
updated = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
|
||||
|
||||
return f'''<svg xmlns="http://www.w3.org/2000/svg" width="{WIDTH}" height="{HEIGHT}" viewBox="0 0 {WIDTH} {HEIGHT}" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif">
|
||||
<style>
|
||||
.bg {{ fill: #ffffff; }}
|
||||
.title {{ fill: #1f2328; font-size: 17px; font-weight: 600; }}
|
||||
.subtitle {{ fill: #656d76; font-size: 11px; }}
|
||||
.grid {{ stroke: #d0d7de; stroke-width: 1; stroke-dasharray: 3 3; }}
|
||||
.axis {{ stroke: #656d76; stroke-width: 1.5; }}
|
||||
.tick {{ fill: #656d76; font-size: 11px; }}
|
||||
.area {{ fill: #ffd33d; opacity: 0.18; }}
|
||||
.line {{ stroke: #f0b400; stroke-width: 2.5; fill: none; stroke-linejoin: round; stroke-linecap: round; }}
|
||||
.dot {{ fill: #f0b400; }}
|
||||
@media (prefers-color-scheme: dark) {{
|
||||
.bg {{ fill: #0d1117; }}
|
||||
.title {{ fill: #e6edf3; }}
|
||||
.subtitle {{ fill: #8b949e; }}
|
||||
.grid {{ stroke: #30363d; }}
|
||||
.axis {{ stroke: #8b949e; }}
|
||||
.tick {{ fill: #8b949e; }}
|
||||
}}
|
||||
</style>
|
||||
<rect class="bg" width="{WIDTH}" height="{HEIGHT}" rx="6"/>
|
||||
<text x="{PAD_L}" y="26" class="title">Stargazers over time</text>
|
||||
<text x="{PAD_L}" y="42" class="subtitle">{esc(repo)} · {c_max:,} stars · updated {updated}</text>
|
||||
{grid_lines}
|
||||
<line x1="{PAD_L}" y1="{PAD_T}" x2="{PAD_L}" y2="{PAD_T + plot_h}" class="axis"/>
|
||||
<line x1="{PAD_L}" y1="{PAD_T + plot_h}" x2="{WIDTH - PAD_R}" y2="{PAD_T + plot_h}" class="axis"/>
|
||||
<polygon class="area" points="{area}"/>
|
||||
<polyline class="line" points="{line}"/>
|
||||
<circle class="dot" cx="{pts[-1][0]:.1f}" cy="{pts[-1][1]:.1f}" r="4"/>
|
||||
{x_labels}
|
||||
</svg>
|
||||
'''
|
||||
|
||||
|
||||
def main():
|
||||
token = os.environ.get("GITHUB_TOKEN")
|
||||
if not token:
|
||||
raise SystemExit("GITHUB_TOKEN is required (repo read access to stargazers).")
|
||||
|
||||
repo = resolve_repo()
|
||||
print(f"Fetching stargazers for {repo} ...", file=sys.stderr)
|
||||
dates = fetch_stargazers(token, repo)
|
||||
print(f"Got {len(dates)} stars.", file=sys.stderr)
|
||||
|
||||
series = build_series(dates)
|
||||
svg = render_svg(series, repo)
|
||||
|
||||
out_dir = os.path.dirname(OUTPUT)
|
||||
if out_dir:
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
with open(OUTPUT, "w", encoding="utf-8") as fh:
|
||||
fh.write(svg)
|
||||
print(f"Wrote {OUTPUT} ({len(svg)} bytes).", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user