|
| 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 |
0 commit comments