repo-tracking
Lightweight last-touch / freshness tracking — for portfolio projects, deploys, heartbeats, and anything else with a stable id.
Drop a GitHub Action (or call the HTTP API) when something moves. Your app reads the store and shows “Updated 3 days ago”, builds a team activity wall, or alerts on staleness.
Storage always lives on your infra. This package defines the schema and a small TouchStore interface so JSON, Firestore, or any key/document DB can back it.
Pieces
| Piece | What it is |
|---|---|
| GitHub Action | Connector you add to source repos |
| Store | JSON file, Firestore, or your own TouchStore adapter |
| npm package | Helpers, React <LastTouched />, signed ingest handler |
1. Quick start (public JSON)
Create public/repo-tracking.json:
{
"version": 1,
"projects": {}
}
Add .github/workflows/repo-tracking.yml (see examples/connector.yml):
name: Report last touch
on:
push:
branches: [main]
jobs:
track:
runs-on: ubuntu-latest
steps:
- uses: yaboijams/repo-tracking@v0.1.0
with:
project-id: get-it-done
mode: github-file
github-token: ${{ secrets.REPO_TRACKING_TOKEN }}
store-owner: yaboijams
store-repo: web-portfolio
store-path: public/repo-tracking.json
store-branch: master
Create a fine-scoped PAT (contents:write on the store repo only), save it as REPO_TRACKING_TOKEN in each source repo.
JSON mode is ideal for small public sets (portfolios). Concurrent writers to one blob can race; prefer Firestore (or any per-id store) when many repos update at once.
2. HTTP mode + any database
Point the Action at your API; verify the HMAC and write through a TouchStore:
with:
project-id: get-it-done
mode: http
http-url: https://your.site/api/repo-tracking
secret: ${{ secrets.REPO_TRACKING_SECRET }}
import { createTouchHandler } from "repo-tracking/server";
import { createMemoryStore } from "repo-tracking/store";
// or: createJsonFileStore / createFirestoreStore
const handle = createTouchHandler({
store: createMemoryStore(),
secret: process.env.REPO_TRACKING_SECRET!,
});
export async function POST(request: Request) {
const body = await request.text();
const result = await handle({ method: request.method, body, headers: request.headers });
return Response.json(result.body, { status: result.status });
}
Firestore
npm install firebase-admin
See examples/firebase-api.ts. One document per id (default collection repo-tracking) so concurrent touches don’t clobber unrelated projects.
import { getFirestore } from "firebase-admin/firestore";
import { createFirestoreStore } from "repo-tracking/store/firestore";
const store = createFirestoreStore({
firestore: getFirestore(),
collection: "repo-tracking",
});
Bring your own DB
Implement TouchStore — anything key/document oriented works (Postgres, Redis, Mongo, Dynamo, …):
import type { TouchStore } from "repo-tracking/store";
const store: TouchStore = {
async get(id) { /* ... */ },
async touch(update) { /* upsert by update.id; newer touchedAt wins */ },
async list(query) { /* optional filters: kind, tag, staleBefore */ },
};
3. Display in any app
npm install repo-tracking
import { RepoTrackingProvider, LastTouched } from "repo-tracking/react";
export function Projects({ projects }: { projects: { trackingId: string; title: string }[] }) {
return (
<RepoTrackingProvider storeUrl="/repo-tracking.json">
{projects.map((p) => (
<div key={p.trackingId}>
<h3>{p.title}</h3>
<LastTouched id={p.trackingId} className="text-sm opacity-70" />
</div>
))}
</RepoTrackingProvider>
);
}
Core helpers (no React):
import { formatTouchedAt, getProjectEntry } from "repo-tracking";
import { fetchStore, fetchEntry } from "repo-tracking/client";
// Full JSON blob
await fetchStore("/repo-tracking.json", { cache: "no-store" });
// Single-entry API (e.g. Firestore-backed GET ?id=…)
await fetchEntry("https://your.site/api/repo-tracking?id=get-it-done");
For freshness-sensitive UIs, prefer cache: "no-store" or short Cache-Control (see the Firebase example).
Store schema
JSON blob:
{
"version": 1,
"projects": {
"get-it-done": {
"touchedAt": "2026-07-19T07:00:00.000Z",
"sha": "abc123",
"ref": "refs/heads/main",
"repo": "yaboijams/Donezo",
"url": "https://github.com/yaboijams/Donezo",
"version": "1.2.3",
"kind": "commit",
"tags": ["portfolio"],
"meta": { "env": "prod" }
}
}
}
Optional fields version, kind, tags, and meta support package releases, deploys, heartbeats, and team walls beyond portfolio labels.
Security
- A public JSON/API read is not a vault — only store non-sensitive fields (timestamps, ids, shas, public urls).
- Writes must be authenticated: HMAC secret for HTTP mode, or a tightly scoped GitHub PAT for
github-filemode. - Do not put tokens or private content in the store.
Action inputs
| Input | Required | Description |
|---|---|---|
project-id |
yes | Stable id used by consumers |
mode |
no | github-file (default) or http |
github-token |
github-file | PAT with write access to the store repo |
store-owner / store-repo |
github-file | Where the JSON lives |
store-path |
no | Default public/repo-tracking.json |
store-branch |
no | Branch to commit to |
http-url / secret |
http | Endpoint + HMAC secret |
ref-filter |
no | Only report when GITHUB_REF matches |
version |
no | Package/release version (e.g. npm semver) |
Releasing
Releases use semantic-release on main.
- npm granular token (read/write + bypass 2FA)
- GitHub Actions secret
NPM_SECRET(mapped toNPM_TOKENin the workflow) - Conventional Commits:
fix:patch,feat:minor,BREAKING CHANGE/feat!:major
License
MIT