Skip to content

Commit 88142da

Browse files
Initial release of tool-github-export
Export GitHub repository metadata from multiple owners (orgs + users) via the GraphQL v4 API into versioned JSON artifacts: - data/repos.public.json committed to this repo for public consumers - data/repos.private.json pushed to a configurable private data repo - stable category field derived from name prefixes; topics as tags - per-repo overrides (display_name, featured, category, exclude) - JSON Schema contract + validation command - scheduled GitHub Action (every 6h + workflow_dispatch) Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
0 parents  commit 88142da

26 files changed

Lines changed: 7765 additions & 0 deletions

‎.github/workflows/ci.yml‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: ci
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths-ignore:
7+
- "data/**"
8+
pull_request:
9+
10+
jobs:
11+
test:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v7
15+
16+
- uses: astral-sh/setup-uv@v10
17+
with:
18+
enable-cache: true
19+
20+
- name: Install dependencies
21+
run: uv sync
22+
23+
- name: Lint
24+
run: uv run ruff check .
25+
26+
- name: Test
27+
run: uv run pytest -v

‎.github/workflows/export.yml‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: export
2+
3+
on:
4+
schedule:
5+
# Every 6 hours, offset from the top of the hour.
6+
- cron: "13 */6 * * *"
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: write
11+
12+
concurrency:
13+
group: export
14+
cancel-in-progress: false
15+
16+
jobs:
17+
export:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- uses: actions/checkout@v7
21+
22+
- uses: astral-sh/setup-uv@v10
23+
with:
24+
enable-cache: true
25+
26+
- name: Install dependencies
27+
run: uv sync
28+
29+
- name: Run export
30+
env:
31+
GH_EXPORT_TOKEN: ${{ secrets.GH_EXPORT_TOKEN }}
32+
run: uv run github-export export --config export.config.yaml
33+
34+
- name: Validate output
35+
run: |
36+
uv run github-export validate data/repos.public.json
37+
uv run github-export validate data/repos.private.json
38+
39+
- name: Commit public export
40+
run: |
41+
git config user.name "github-actions[bot]"
42+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
43+
git add data/repos.public.json
44+
git diff --staged --quiet || git commit -m "chore(data): update repos.public.json"
45+
git push
46+
47+
- name: Publish private export
48+
if: vars.PRIVATE_DATA_REPO != ''
49+
env:
50+
GH_EXPORT_TOKEN: ${{ secrets.GH_EXPORT_TOKEN }}
51+
PRIVATE_DATA_REPO: ${{ vars.PRIVATE_DATA_REPO }}
52+
run: |
53+
git clone "https://x-access-token:${GH_EXPORT_TOKEN}@github.com/${PRIVATE_DATA_REPO}.git" /tmp/private-data
54+
cp data/repos.private.json /tmp/private-data/repos.private.json
55+
cd /tmp/private-data
56+
git config user.name "github-actions[bot]"
57+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
58+
git add repos.private.json
59+
git diff --staged --quiet || git commit -m "chore(data): update repos.private.json"
60+
git push

‎.gitignore‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*.egg-info/
5+
.venv/
6+
dist/
7+
build/
8+
9+
# Tooling
10+
.ruff_cache/
11+
.pytest_cache/
12+
.coverage
13+
htmlcov/
14+
15+
# Env / secrets
16+
.env
17+
*.local
18+
19+
# OS / editors
20+
.DS_Store
21+
.vscode/
22+
.idea/
23+
24+
# Never commit the private export to this public repo
25+
data/repos.private.json

‎LICENSE‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 DevArts Lab
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

‎README.md‎

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# tool-github-export
2+
3+
Export GitHub repository metadata from multiple owners (orgs + users) into versioned JSON artifacts that any project can consume over HTTP.
4+
5+
This repo is the single source of truth for "which repos exist, what are they, and how should they be categorized" across DevArts Lab properties: the githublabs.com directory, portfolio components, the resume builder, and anything else that needs a project list.
6+
7+
## How it works
8+
9+
```
10+
export.config.yaml --> github-export (GraphQL v4) --> data/repos.public.json (committed here, public)
11+
| --> data/repos.private.json (pushed to a private repo)
12+
v
13+
categories | overrides | filters
14+
```
15+
16+
A scheduled GitHub Action runs the exporter every 6 hours (or on demand via `workflow_dispatch`), validates the output against the JSON schema, and commits `data/repos.public.json` back to this repo. `repos.private.json` is pushed to a separate private repo (see [Private output](#private-output)).
17+
18+
## Outputs
19+
20+
| File | Visibility | Where it goes |
21+
|---|---|---|
22+
| `repos.public.json` | public repos only, minus `exclude` overrides | committed to `data/` in this repo |
23+
| `repos.private.json` | all repos the token can see (public + private) | pushed to the private repo named by the `PRIVATE_DATA_REPO` variable |
24+
25+
### Consumer URLs (public export)
26+
27+
| Endpoint | URL | Notes |
28+
|---|---|---|
29+
| raw | `https://raw.githubusercontent.com/DevArtsLab/tool-github-export/main/data/repos.public.json` | always fresh, rate-limited lightly |
30+
| jsDelivr CDN | `https://cdn.jsdelivr.net/gh/DevArtsLab/tool-github-export@main/data/repos.public.json` | cached, fast, CORS-friendly; pin `@<commit>` for deterministic reads |
31+
| schema | `https://raw.githubusercontent.com/DevArtsLab/tool-github-export/main/schemas/repos.schema.json` | JSON Schema 2020-12 |
32+
33+
### Consuming it
34+
35+
```bash
36+
# curl
37+
curl -s https://cdn.jsdelivr.net/gh/DevArtsLab/tool-github-export@main/data/repos.public.json
38+
```
39+
40+
```ts
41+
// TypeScript / frontend
42+
const res = await fetch(
43+
"https://cdn.jsdelivr.net/gh/DevArtsLab/tool-github-export@main/data/repos.public.json"
44+
);
45+
const { repositories, stats, generated_at } = await res.json();
46+
const tools = repositories.filter((r) => r.category === "tool");
47+
```
48+
49+
```python
50+
# Python
51+
import httpx
52+
data = httpx.get(
53+
"https://cdn.jsdelivr.net/gh/DevArtsLab/tool-github-export@main/data/repos.public.json"
54+
).json()
55+
```
56+
57+
See `examples/consumer.html` for a working browser page (serve it with `uv run python -m http.server`).
58+
59+
## The contract
60+
61+
Top-level fields: `schema_version`, `generated_at`, `generator`, `sources`, `stats` (incl. `by_category`), `repositories[]`.
62+
63+
Each repository carries: identity (`name`, `full_name`, `owner`, `owner_type`, `display_name`), links (`url`, `homepage`, `image_url`), classification (`category`, `tags`, `featured`, `visibility`), code (`primary_language`, `languages[]` with byte percentages, `license`, `default_branch`, `size_kb`), activity (`created_at`, `updated_at`, `pushed_at`, `latest_release`), stats (`stars`, `forks`, `watchers`, `open_issues`), and flags (`is_fork`, `is_archived`, `is_template`, `forked_from`).
64+
65+
**`category` is the stable filter field.** It is derived from the repo name prefix via `categories` in `export.config.yaml` (`tool-` -> `tool`, `devarts-`/`devartslab-` -> `org-infra`, everything else -> `project`). Consumers should filter on `category`, not parse names. `tags` remains the freeform field (GitHub topics + override tags).
66+
67+
**Versioning:** `schema_version` follows semver. Additive fields bump the minor version; renames/removals bump the major version and are announced in release notes.
68+
69+
## Configuration (`export.config.yaml`)
70+
71+
```yaml
72+
sources: # owners to scan: {type: user|organization, login: ...}
73+
filters: # include_forks / include_archived / include_templates
74+
categories: # "prefix-": category (first match wins, case-insensitive)
75+
default_category: project
76+
outputs: # dir + filenames
77+
overrides: # per-repo, keyed by "owner/repo"
78+
```
79+
80+
Per-repo `overrides`:
81+
82+
```yaml
83+
overrides:
84+
"DevArtsLab/tool-github-export":
85+
display_name: "GitHub Export" # pretty name for directories/resumes
86+
description: "..." # replaces the GitHub description
87+
category: tool # overrides prefix-derived category
88+
featured: true # consumers can surface this
89+
tags_add: ["cli"] # merged into tags
90+
exclude: true # kept out of repos.public.json only
91+
```
92+
93+
## Running it
94+
95+
Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/).
96+
97+
```bash
98+
uv sync
99+
uv run github-export export # writes data/repos.{public,private}.json
100+
uv run github-export export --public-only # public artifact only
101+
uv run github-export validate data/repos.public.json
102+
uv run github-export schema > my-schema.json
103+
uv run pytest # tests
104+
uv run ruff check . # lint
105+
```
106+
107+
Token resolution order: `--token` flag -> `GH_EXPORT_TOKEN` / `GH_TOKEN` / `GITHUB_TOKEN` env -> `gh auth token` fallback.
108+
109+
## GitHub Action
110+
111+
`.github/workflows/export.yml` runs every 6 hours + on demand:
112+
113+
1. `github-export export` writes `data/repos.public.json` + `data/repos.private.json`
114+
2. both files are validated against `schemas/repos.schema.json`
115+
3. `repos.public.json` is committed back to `main`
116+
4. `repos.private.json` is pushed to the private data repo (if configured)
117+
118+
### Required secrets / variables
119+
120+
| Name | Kind | Value |
121+
|---|---|---|
122+
| `GH_EXPORT_TOKEN` | secret | PAT that can read target repos. Classic PAT with `repo` + `read:org` covers both private org and personal repos. Public-only exports need just `public_repo` + `read:org`. |
123+
| `PRIVATE_DATA_REPO` | variable | e.g. `DevArtsLab/tool-github-export-data`. If unset, the private file is generated and validated but not published. |
124+
125+
The `GITHUB_TOKEN` provided by Actions cannot be used here: it is scoped to this repository and cannot list org/user repositories.
126+
127+
## Private output
128+
129+
Private repos and repos marked `exclude: true` never reach `repos.public.json`. The private export is pushed to a separate **private** repo so it can be consumed by trusted consumers (e.g. the resume builder) via the GitHub API with a token, without exposing private repo names publicly.
130+
131+
## Roadmap
132+
133+
- Optional AI enrichment block (`ai` field per repo: summary, tech stack, highlights) via GitHub Models in the Action
134+
- Phase 2 serving: Cloudflare Worker + R2 for `data.githublabs.com`-style endpoints with query filters
135+
- `repos.featured.json` convenience view
136+
137+
## License
138+
139+
MIT

‎data/.gitkeep‎

Whitespace-only changes.

0 commit comments

Comments
 (0)