diff --git a/.env.example b/.env.example index 8f30a62..4e32269 100644 --- a/.env.example +++ b/.env.example @@ -1,31 +1,35 @@ -# Copy this file to .env.local for local work. Platform-provided variables and -# compatibility aliases are intentionally omitted. - -# Supabase transaction-pooler URL at runtime and a direct or session-pooler URL -# for migrations and cron configuration. +# Copy to .env.local. All values are intentionally blank; basic dev/build needs none. +# Database: runtime pooler URL, and optional direct/session URL for operations. DATABASE_URL= DATABASE_URL_UNPOOLED= -# Optional fallback for public repository discovery and pinned GitHub embeds. +# GitHub activity: JSON object mapping configured logins to personal access tokens. +# Public authors and IDs live in src/content/site.ts. Keep tokens only in this environment. +GITHUB_TOKENS= +# Optional public repository/embed access; GH_TOKEN is the GitHub CLI alias. GITHUB_TOKEN= +GH_TOKEN= -# Account-specific tokens used to read each authenticated user event feed and -# repository commits, including private repositories the account can access. -GITHUB_F0RR0_TOKEN= -GITHUB_YUPPIESTECHDEV_TOKEN= - -# Authenticates Supabase Cron requests. -# Use at least 32 random characters. +# Independent secrets, each at least 32 random characters. CRON_SECRET= - -# Signs activity pagination cursors independently of cron authentication. -# Use at least 32 random characters. GITHUB_ACTIVITY_CURSOR_SECRET= - -# Verifies GitHub webhook signatures. Use at least 32 random characters and -# configure the same value on the webhook sender. GITHUB_WEBHOOK_SECRET= -# Optional. Generates outcome summaries for public work units. Missing -# credentials, exhausted budgets, and provider failures remain facts-only. +# Optional outcome summaries (activity works without this). OPENAI_API_KEY= +# Optional public PostHog capture key and region (us or eu; defaults to us). +NEXT_PUBLIC_POSTHOG_KEY= +NEXT_PUBLIC_POSTHOG_REGION= + +# Optional local server port and public fallback port (default 3000). +PORT= +NEXT_PUBLIC_PORT= + +# Supplied by Next/Vercel. Leave blank locally; do not manage a separate site URL. +NODE_ENV= +VERCEL= +VERCEL_ENV= +VERCEL_PROJECT_PRODUCTION_URL= +VERCEL_URL= +# Vercel also supplies NEXT_PUBLIC_VERCEL_ENV and NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL at build time. +# Codex account credentials belong in Supabase Vault; see docs/codex-stats.md. diff --git a/.github/workflows/github-activity-backfill.yml b/.github/workflows/github-activity-backfill.yml index 7fb92df..ac7e0d6 100644 --- a/.github/workflows/github-activity-backfill.yml +++ b/.github/workflows/github-activity-backfill.yml @@ -14,12 +14,8 @@ on: account: description: Account whose token and accessible repositories are scanned required: true - default: f0rr0 - type: choice - options: - - f0rr0 - - yuppiestechdev - - all + default: all + type: string repository_id: description: Numeric repository ID; blank scans repositories active in the window required: false @@ -57,8 +53,7 @@ jobs: ACCOUNT: ${{ inputs.account }} DATABASE_URL: ${{ secrets.ACTIVITY_DATABASE_URL }} END_DATE: ${{ inputs.end_date }} - GITHUB_F0RR0_TOKEN: ${{ secrets.ACTIVITY_F0RR0_TOKEN }} - GITHUB_YUPPIESTECHDEV_TOKEN: ${{ secrets.ACTIVITY_YUPPIESTECHDEV_TOKEN }} + GITHUB_TOKENS: ${{ secrets.ACTIVITY_GITHUB_TOKENS }} MAXIMUM_MINUTES: ${{ inputs.maximum_minutes }} REPOSITORY_ID: ${{ inputs.repository_id }} START_DATE: ${{ inputs.start_date }} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 12bc5a3..40925d3 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -3,6 +3,9 @@ name: validate on: pull_request: +permissions: + contents: read + jobs: validate: name: validate diff --git a/.worktreeinclude b/.worktreeinclude index 728495d..e04c51c 100644 --- a/.worktreeinclude +++ b/.worktreeinclude @@ -1,2 +1,3 @@ -# Copy the complete local runtime configuration into Codex-managed worktrees. +# Copy development-only local configuration into managed worktrees. +# Keep production credentials in deployment secret storage, not .env.local. .env.local diff --git a/BLOG.md b/BLOG.md index a28dd3c..dcdc561 100644 --- a/BLOG.md +++ b/BLOG.md @@ -11,7 +11,7 @@ prompts, and desktop/mobile/social crops. Keep research reports, per-post image prompts and production briefs transient; do not commit them to the repository. Reserve `docs/` for current implementation and usage documentation. -Public identity URLs always use `https://f0rr0.dev`; preview deployments return +Public identity URLs use Vercel’s `VERCEL_PROJECT_PRODUCTION_URL`; preview deployments return `noindex`. The sitemap lists intended search destinations, while alternate Markdown and profile exports remain discoverable through links and `llms.txt`. diff --git a/README.md b/README.md index 11b3db0..7cd34d5 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,13 @@ bun run dev ``` The website works without secrets; the persisted commit feed stays empty until -Postgres is configured. See [the commit sync guide](docs/github-commits.md) for -database, Supabase Cron, account polling, and webhook setup. +PostgreSQL is configured. Set `DATABASE_URL` for application queries and, when +using a runtime transaction pooler, `DATABASE_URL_UNPOOLED` to a direct or +session-pooler connection for migrations and cron setup. Supply these in +`.env.local` for local use and in Vercel environment settings for deployments; +the application does not provision a database or synchronize connection settings. +See [the commit sync guide](docs/github-commits.md) for Supabase Cron, account +polling, and webhook setup. The separate [Codex stats guide](docs/codex-stats.md) covers its encrypted account snapshots and scheduled sync. @@ -28,10 +33,79 @@ bun test bun run build ``` -Vercel production builds apply pending migrations before building the site, -using the database connection already synchronized by Supabase. Preview and -local builds skip migrations. Apply them manually in other environments with: +`bun run build` only builds the application. To use the existing production +migrations and Supabase scheduling, set Vercel's **Build Command** to: ```sh -bun run db:migrate +bun scripts/migrate-production-database.ts && bun run build && bun scripts/configure-supabase-cron.ts --production-build ``` + +Those operational scripts act only on Vercel production deployments. The +migration runs before the build because pages may read the database while +building. Cron configuration follows a successful build; it is not a +post-deployment hook. Local operations remain `bun run db:migrate` and +`bun run supabase:cron`. + +## Updating an existing deployment + +Move the existing per-account token values into one `GITHUB_TOKENS` JSON object, +keyed by the logins in `src/content/site.ts`. The same tokens can be reused; no +new tokens or account IDs are required. Set this variable in Vercel and set the +`ACTIVITY_GITHUB_TOKENS` repository secret for the manual backfill Action (GitHub +reserves secret names beginning with `GITHUB_`). Keep the old token +variables until the new deployment has been verified, then remove them. + +Keep the existing database, webhook, cron, cursor-signing and OpenAI credentials. +To retain analytics, set `NEXT_PUBLIC_POSTHOG_KEY` to the existing project's public +capture key; `NEXT_PUBLIC_POSTHOG_REGION` defaults to `us`. + +For the database-backed installation, set the Build Command above before deploying +this change. It applies migration `0022`, builds against the updated schema, then +updates cron configuration. Migration `0022` only replaces seven username +allowlists with valid-login checks; it adds no tables or columns and rewrites no +stored history. Leave older applied migrations intact. + +## Customize once + +- `src/content/resume.ts`: identity, social profiles, career, education and PDF paths. +- `src/content/home.ts`: introduction and featured work. +- `src/content/site.ts`: tracked GitHub authors, language and work-log timezone. +- `.env.example`: every supported environment variable, with blank values. + +Vercel's `VERCEL_PROJECT_PRODUCTION_URL` supplies the canonical domain, including +for preview metadata. Enable **Automatically expose System Environment Variables** +in the project settings. Local URLs use localhost and the configured port. +There is no separately maintained site URL or blog asset base URL: exported +Markdown uses the image URLs already emitted by the MDX compiler. + +GitHub activity uses configured public authors and separate credentials. See the +[GitHub activity guide](docs/github-commits.md) for token format and service setup, +and the [analytics guide](docs/analytics.md) for optional PostHog configuration. + +Each installation needs its own database. Cron/Vault names are installation-wide; +sharing one database between independent sites is unsupported. Set `vercel.json` +regions to match your database location (the existing deployment uses Tokyo). +Personal content and the authoring defaults under `.rulesync/` can remain or be +edited independently of the application configuration. + +`.worktreeinclude` automatically copies `.env.local` into local worktrees. Use +only development-scoped credentials there; keep production credentials in the +deployment secret store. The copied file remains ignored by Git. + +Docker is required for PostgreSQL integration tests; Bun reports those tests as +skipped when Docker is unavailable. Typst tooling is needed to regenerate the +résumé PDF. Versions are pinned in `mise.toml`, `package.json` and `bun.lock`; +CI installs with the frozen lockfile. + +GitHub native secret scanning and push protection are enabled for this repository. +Enable those repository settings when creating a fork. The historical Firebase +project configuration still needs an owner-side restrictions/retirement review. +Report credential exposure privately through GitHub's security reporting feature +when enabled. Otherwise, use the maintainer contact in `src/content/resume.ts`; +never paste credentials into a public issue. + +## Reuse status + +A code license and the reuse policy for personal writing/images still need to be +chosen. Public source availability alone is not a grant of reuse rights. Preserve +upstream copyright/license notices for vendored fonts, assets and authoring skills. diff --git a/bun.lock b/bun.lock index 853addd..f817bb4 100644 --- a/bun.lock +++ b/bun.lock @@ -6,61 +6,61 @@ "name": "f0rr0.dev", "dependencies": { "@ai-sdk/openai": "4.0.41", - "@base-ui/react": "^1.6.0", - "@mdx-js/loader": "^3.1.1", - "@mdx-js/react": "^3.1.1", + "@base-ui/react": "1.6.0", + "@mdx-js/loader": "3.1.1", + "@mdx-js/react": "3.1.1", "@mermaid-js/layout-elk": "0.2.3", - "@next/mdx": "^16.3.4", - "@remark-embedder/core": "^3.0.3", - "@t3-oss/env-nextjs": "^0.13.11", - "@tailwindcss/typography": "^0.5.20", - "@tanstack/react-query": "^5.102.8", + "@next/mdx": "16.3.4", + "@remark-embedder/core": "3.0.3", + "@t3-oss/env-nextjs": "0.13.11", + "@tailwindcss/typography": "0.5.20", + "@tanstack/react-query": "5.102.8", "ai": "7.0.63", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", "drizzle-orm": "0.45.2", - "feed": "^5.2.1", + "feed": "5.2.1", "gpt-tokenizer": "4.0.0", - "lucide-react": "^1.21.0", - "mermaid": "^11.17.2", - "motion": "^13.2.0", - "next": "^16.3.4", - "next-themes": "^0.4.6", + "lucide-react": "1.21.0", + "mermaid": "11.17.2", + "motion": "13.2.0", + "next": "16.3.4", + "next-themes": "0.4.6", "postgres": "3.4.9", "posthog-js": "1.428.7", - "react": "^19.2.8", - "react-dom": "^19.2.8", - "reading-time": "^1.5.0", - "rehype-autolink-headings": "^7.1.0", - "rehype-pretty-code": "^0.14.5", - "rehype-slug": "^6.0.0", - "remark-gfm": "^4.0.1", + "react": "19.2.8", + "react-dom": "19.2.8", + "reading-time": "1.5.0", + "rehype-autolink-headings": "7.1.0", + "rehype-pretty-code": "0.14.5", + "rehype-slug": "6.0.0", + "remark-gfm": "4.0.1", "server-only": "0.0.1", - "shadcn": "^4.11.0", - "shiki": "^4.4.3", - "sonner": "^2.0.8", - "tailwind-merge": "^3.6.0", - "unist-util-visit": "^5.1.0", - "zod": "^4.4.3", + "shadcn": "4.11.0", + "shiki": "4.4.3", + "sonner": "2.0.8", + "tailwind-merge": "3.6.0", + "unist-util-visit": "5.1.0", + "zod": "4.4.3", }, "devDependencies": { - "@tailwindcss/postcss": "^4.3.1", - "@types/bun": "^1.4.1", - "@types/mdx": "^2.0.14", + "@tailwindcss/postcss": "4.3.1", + "@types/bun": "1.4.1", + "@types/mdx": "2.0.14", "@types/node": "24.13.3", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", - "@typescript/native-preview": "^7.0.0-dev.20260624.1", + "@types/react": "19.2.17", + "@types/react-dom": "19.2.3", + "@typescript/native-preview": "7.0.0-dev.20260624.1", "babel-plugin-react-compiler": "1.0.0", "drizzle-kit": "0.31.10", - "oxfmt": "^0.56.0", - "oxlint": "^1.71.0", - "oxlint-tsgolint": "^0.23.0", - "schema-dts": "^2.0.0", - "tailwindcss": "^4.3.1", - "tw-animate-css": "^1.4.0", - "typescript": "^7.0.1-rc", - "ultracite": "^7.8.3", + "oxfmt": "0.56.0", + "oxlint": "1.71.0", + "oxlint-tsgolint": "0.23.0", + "schema-dts": "2.0.0", + "tailwindcss": "4.3.1", + "tw-animate-css": "1.4.0", + "typescript": "7.0.1-rc", + "ultracite": "7.8.3", }, }, }, @@ -305,8 +305,6 @@ "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], - "@neondatabase/serverless": ["@neondatabase/serverless@1.1.0", "", {}, "sha512-r3ZZhRjEcfEdKIZnoB1RusNgvHuaBRqfCzV4Gi+5A9yUX0S4HTws/ASWqt13wL4y4I+0rqsWGdA2w7EQXHi3+Q=="], - "@next/env": ["@next/env@16.3.4", "", {}, "sha512-cjWZnUUa6jZq2kFaNe/ZyJdZonOZ/QoN0Zka2nz/FLOrfx14pQuM9c5RaSVkWMqgdt4ksgPAMWPyHSs/CyV48Q=="], "@next/mdx": ["@next/mdx@16.3.4", "", { "dependencies": { "source-map": "^0.7.0" }, "peerDependencies": { "@mdx-js/loader": ">=0.15.0", "@mdx-js/react": ">=0.15.0" }, "optionalPeers": ["@mdx-js/loader", "@mdx-js/react"] }, "sha512-XEmW3ccWWNybofVOmgIEVhXtmNXM0u423iqkVXwPf29fht/tdpUiMDuMHQ4hMerE1477nOJrv5kM/F6qMcEMwA=="], diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..965d318 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,5 @@ +[install] +exact = true + +[test] +preload = ["./tests/setup.ts"] diff --git a/docs/analytics.md b/docs/analytics.md index 650690b..636b866 100644 --- a/docs/analytics.md +++ b/docs/analytics.md @@ -4,8 +4,8 @@ The site uses PostHog US Cloud for pageviews, acquisition attribution, and expli ## Configuration -- `src/instrumentation-client.ts` initializes the SDK only in production on `f0rr0.dev`. The public project token and API path are configured in source; no PostHog environment variables are needed. -- `src/proxy.ts` forwards `/_r7k2/*` to fixed US ingestion and asset hosts. It strips Cookie, Authorization, and Referer headers. Collector trailing slashes are preserved; ordinary page trailing slashes receive a 308 redirect. +- `src/instrumentation-client.ts` initializes the SDK only on the canonical production host when `NEXT_PUBLIC_POSTHOG_KEY` is configured. `NEXT_PUBLIC_POSTHOG_REGION` selects `us` (default) or `eu` for both the SDK and proxy; preview and local deployments do not capture events. +- `src/proxy.ts` forwards `/_r7k2/*` to the selected region’s ingestion and asset hosts. It strips Cookie, Authorization, and Referer headers. Collector trailing slashes are preserved; ordinary page trailing slashes receive a 308 redirect. - The SDK uses always-cookieless mode, memory persistence, and no person profiles. Autocapture, replay, surveys, heatmaps, automatic exceptions, performance collection, and feature flags are disabled. - Do Not Track and Global Privacy Control signals do not change capture behavior. - `src/lib/analytics.ts` contains the typed event contract, link classification, and property redaction. URL queries and fragments, referrer paths, ad click IDs, and search terms are removed. Campaign values must be short public slugs. diff --git a/docs/github-commits.md b/docs/github-commits.md index 0d0e107..552764c 100644 --- a/docs/github-commits.md +++ b/docs/github-commits.md @@ -90,12 +90,11 @@ after activation even if an older worker cleared a deployment-time token. ## Runtime configuration -Required server-side values are: +GitHub activity is optional. Its server-side configuration is: ```dotenv DATABASE_URL=postgresql://... -GITHUB_F0RR0_TOKEN=github_pat_... -GITHUB_YUPPIESTECHDEV_TOKEN=github_pat_... +GITHUB_TOKENS={"alice":""} GITHUB_WEBHOOK_SECRET= CRON_SECRET= GITHUB_ACTIVITY_CURSOR_SECRET= @@ -104,9 +103,50 @@ GITHUB_ACTIVITY_CURSOR_SECRET= `OPENAI_API_KEY` is optional. Without it, factual work units continue to publish and summary claims remain untouched. `DATABASE_URL_UNPOOLED` is the optional direct/session-pooler override used by migrations and Supabase Cron -configuration. `GITHUB_TOKEN` is an optional additional read token. Secrets and +configuration. `GITHUB_TOKEN` (or `GH_TOKEN`) is optional for public discovery and code embeds. Secrets and private evidence stay server-side. +Configure tracked authors once in `src/content/site.ts` as `{ login, id }` records. +The first account supplies the primary public GitHub profile. IDs are GitHub's +permanent numeric user IDs (stored as strings). GitHub social URLs and author +lookups derive from these records. + +`GITHUB_TOKENS` is a JSON object mapping those logins to personal access tokens. +Blank input or `{}` means no account credentials. Keys are case-insensitive; +unknown accounts, case-colliding keys and empty token values are rejected without +logging tokens. Each account can have one token; omit its key to disable its +account-specific polling and inventory. The existing `/user` check verifies the +configured login and ID before acquisition. GitHub App installation tokens cannot +substitute for a personal account identity in these jobs. + +Repository reads try the account's token first, then other configured credentials. +Changing tokens never changes the selected authors, rewrites stored identities or +prunes published history. Missing credentials and lost access defer fetching. +Intentional changes to the public author list request a work-unit rebuild and +filter issue visibility; raw evidence is retained. Username changes require an +explicit configuration/checkpoint maintenance operation, not automatic database +renaming. Keep the numeric ID unchanged when an existing account is renamed. + +The forward migration only replaces the seven personal account-name constraints +with generic login shape checks. It adds no identity columns and preserves +existing checkpoints. Applied migrations remain unchanged. + +Webhooks require their own `GITHUB_WEBHOOK_SECRET`; removing a token does not +revoke a webhook. Summaries require only their own `OPENAI_API_KEY` and stored facts. +Cron setup schedules polling and refs when tokens are configured; the existing +worker remains scheduled for database-only publication, and summaries are scheduled +when their provider key is configured. Codex scheduling follows enabled database +accounts. Rerun cron setup after enabling or disabling a service. Local cron setup +requires the Vercel production hostname in `VERCEL_PROJECT_PRODUCTION_URL`. +The manual backfill Action uses repository secrets `ACTIVITY_DATABASE_URL` and +`ACTIVITY_GITHUB_TOKENS` (mapped to the runtime variable `GITHUB_TOKENS`; GitHub +reserves the `GITHUB_` secret prefix). Its account input defaults to all configured authors. Every +selected backfill author needs a credential; use `--account` to select a subset. + +Public output includes private-activity counts, timestamps and line/file facts, +with repository names masked; private repository IDs/avatar URLs can also appear. +Enable ingestion only if that publication policy fits the installation. + Routine entry points are: - `POST /api/github/webhook` diff --git a/drizzle/0022_portable_github_accounts.sql b/drizzle/0022_portable_github_accounts.sql new file mode 100644 index 0000000..a51d327 --- /dev/null +++ b/drizzle/0022_portable_github_accounts.sql @@ -0,0 +1,14 @@ +ALTER TABLE "github_account_checkpoints" DROP CONSTRAINT "github_account_checkpoints_tracked_account";--> statement-breakpoint +ALTER TABLE "github_commits" DROP CONSTRAINT "github_commits_tracked_author";--> statement-breakpoint +ALTER TABLE "github_issues" DROP CONSTRAINT "github_issues_tracked_account";--> statement-breakpoint +ALTER TABLE "github_pull_request_signals" DROP CONSTRAINT "github_pull_request_signals_account";--> statement-breakpoint +ALTER TABLE "github_pull_requests" DROP CONSTRAINT "github_pull_requests_tracked_account";--> statement-breakpoint +ALTER TABLE "github_push_observations" DROP CONSTRAINT "github_push_observations_tracked_account";--> statement-breakpoint +ALTER TABLE "github_webhook_deliveries" DROP CONSTRAINT "github_webhook_deliveries_tracked_account";--> statement-breakpoint +ALTER TABLE "github_account_checkpoints" ADD CONSTRAINT "github_account_checkpoints_tracked_account" CHECK ("github_account_checkpoints"."account" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$');--> statement-breakpoint +ALTER TABLE "github_commits" ADD CONSTRAINT "github_commits_tracked_author" CHECK ("github_commits"."author_login" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$');--> statement-breakpoint +ALTER TABLE "github_issues" ADD CONSTRAINT "github_issues_tracked_account" CHECK ("github_issues"."account" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$');--> statement-breakpoint +ALTER TABLE "github_pull_request_signals" ADD CONSTRAINT "github_pull_request_signals_account" CHECK ("github_pull_request_signals"."account" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$');--> statement-breakpoint +ALTER TABLE "github_pull_requests" ADD CONSTRAINT "github_pull_requests_tracked_account" CHECK ("github_pull_requests"."account" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$');--> statement-breakpoint +ALTER TABLE "github_push_observations" ADD CONSTRAINT "github_push_observations_tracked_account" CHECK ("github_push_observations"."account" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$');--> statement-breakpoint +ALTER TABLE "github_webhook_deliveries" ADD CONSTRAINT "github_webhook_deliveries_tracked_account" CHECK ("github_webhook_deliveries"."account" IS NULL OR "github_webhook_deliveries"."account" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$'); diff --git a/drizzle/meta/0022_snapshot.json b/drizzle/meta/0022_snapshot.json new file mode 100644 index 0000000..3f40985 --- /dev/null +++ b/drizzle/meta/0022_snapshot.json @@ -0,0 +1,3871 @@ +{ + "id": "9948f913-1fd3-4315-a57d-c9f1bbc15e7f", + "prevId": "6070194c-d84b-488f-9507-6aa51ce8bb1f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.codex_accounts": { + "name": "codex_accounts", + "schema": "", + "columns": { + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": true, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "snapshot_at": { + "name": "snapshot_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "codex_accounts_id_shape": { + "name": "codex_accounts_id_shape", + "value": "\"codex_accounts\".\"id\" ~ '^[a-z0-9][a-z0-9_-]{0,63}$'" + }, + "codex_accounts_snapshot_pair": { + "name": "codex_accounts_snapshot_pair", + "value": "(\"codex_accounts\".\"snapshot\" IS NULL) = (\"codex_accounts\".\"snapshot_at\" IS NULL)" + }, + "codex_accounts_snapshot_object": { + "name": "codex_accounts_snapshot_object", + "value": "\"codex_accounts\".\"snapshot\" IS NULL OR jsonb_typeof(\"codex_accounts\".\"snapshot\") = 'object'" + } + }, + "isRLSEnabled": true + }, + "public.github_account_checkpoints": { + "name": "github_account_checkpoints", + "schema": "", + "columns": { + "account": { + "name": "account", + "type": "varchar(39)", + "primaryKey": true, + "notNull": true + }, + "events_etag": { + "name": "events_etag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "events_last_attempted_at": { + "name": "events_last_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "events_last_succeeded_at": { + "name": "events_last_succeeded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "events_next_poll_at": { + "name": "events_next_poll_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "gap_detected_at": { + "name": "gap_detected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "gap_expected_event_id": { + "name": "gap_expected_event_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "gap_oldest_available_event_id": { + "name": "gap_oldest_available_event_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "gap_state": { + "name": "gap_state", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true, + "default": "'clear'" + }, + "latest_event_id": { + "name": "latest_event_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "paused": { + "name": "paused", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pull_request_backfill_digest": { + "name": "pull_request_backfill_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "head_ref_cursor_repository_id": { + "name": "head_ref_cursor_repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "head_ref_cycle_started_at": { + "name": "head_ref_cycle_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "head_ref_last_attempted_at": { + "name": "head_ref_last_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "head_ref_last_succeeded_at": { + "name": "head_ref_last_succeeded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "head_ref_lease_token": { + "name": "head_ref_lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "head_ref_lease_until": { + "name": "head_ref_lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "head_ref_next_page": { + "name": "head_ref_next_page", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "head_ref_scan_started_at": { + "name": "head_ref_scan_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ref_backfill_since_at": { + "name": "ref_backfill_since_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tag_ref_cursor_repository_id": { + "name": "tag_ref_cursor_repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "tag_ref_cycle_started_at": { + "name": "tag_ref_cycle_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tag_ref_last_attempted_at": { + "name": "tag_ref_last_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tag_ref_last_succeeded_at": { + "name": "tag_ref_last_succeeded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tag_ref_lease_token": { + "name": "tag_ref_lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tag_ref_lease_until": { + "name": "tag_ref_lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tag_ref_next_page": { + "name": "tag_ref_next_page", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tag_ref_scan_started_at": { + "name": "tag_ref_scan_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_account_checkpoints_tracked_account": { + "name": "github_account_checkpoints_tracked_account", + "value": "\"github_account_checkpoints\".\"account\" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$'" + }, + "github_account_checkpoints_event_id_shape": { + "name": "github_account_checkpoints_event_id_shape", + "value": "(\"github_account_checkpoints\".\"latest_event_id\" IS NULL OR \"github_account_checkpoints\".\"latest_event_id\" ~ '^[0-9]{1,64}$') AND (\"github_account_checkpoints\".\"gap_expected_event_id\" IS NULL OR \"github_account_checkpoints\".\"gap_expected_event_id\" ~ '^[0-9]{1,64}$') AND (\"github_account_checkpoints\".\"gap_oldest_available_event_id\" IS NULL OR \"github_account_checkpoints\".\"gap_oldest_available_event_id\" ~ '^[0-9]{1,64}$')" + }, + "github_account_checkpoints_gap_state": { + "name": "github_account_checkpoints_gap_state", + "value": "\"github_account_checkpoints\".\"gap_state\" IN ('clear', 'detected')" + }, + "github_account_checkpoints_gap_details": { + "name": "github_account_checkpoints_gap_details", + "value": "(\"github_account_checkpoints\".\"gap_state\" = 'detected' AND \"github_account_checkpoints\".\"gap_detected_at\" IS NOT NULL) OR (\"github_account_checkpoints\".\"gap_state\" = 'clear' AND \"github_account_checkpoints\".\"gap_detected_at\" IS NULL AND \"github_account_checkpoints\".\"gap_expected_event_id\" IS NULL AND \"github_account_checkpoints\".\"gap_oldest_available_event_id\" IS NULL)" + }, + "github_account_checkpoints_ref_cursor_shape": { + "name": "github_account_checkpoints_ref_cursor_shape", + "value": "(\"github_account_checkpoints\".\"head_ref_cursor_repository_id\" IS NULL OR \"github_account_checkpoints\".\"head_ref_cursor_repository_id\" ~ '^[0-9]{1,32}$') AND (\"github_account_checkpoints\".\"tag_ref_cursor_repository_id\" IS NULL OR \"github_account_checkpoints\".\"tag_ref_cursor_repository_id\" ~ '^[0-9]{1,32}$')" + }, + "github_account_checkpoints_ref_leases": { + "name": "github_account_checkpoints_ref_leases", + "value": "(\"github_account_checkpoints\".\"head_ref_lease_token\" IS NULL) = (\"github_account_checkpoints\".\"head_ref_lease_until\" IS NULL) AND (\"github_account_checkpoints\".\"tag_ref_lease_token\" IS NULL) = (\"github_account_checkpoints\".\"tag_ref_lease_until\" IS NULL)" + }, + "github_account_checkpoints_ref_scans": { + "name": "github_account_checkpoints_ref_scans", + "value": "(\"github_account_checkpoints\".\"head_ref_next_page\" IS NULL AND \"github_account_checkpoints\".\"head_ref_scan_started_at\" IS NULL OR \"github_account_checkpoints\".\"head_ref_next_page\" >= 2 AND \"github_account_checkpoints\".\"head_ref_scan_started_at\" IS NOT NULL) AND (\"github_account_checkpoints\".\"tag_ref_next_page\" IS NULL AND \"github_account_checkpoints\".\"tag_ref_scan_started_at\" IS NULL OR \"github_account_checkpoints\".\"tag_ref_next_page\" >= 2 AND \"github_account_checkpoints\".\"tag_ref_scan_started_at\" IS NOT NULL)" + }, + "github_account_checkpoints_pr_backfill_digest": { + "name": "github_account_checkpoints_pr_backfill_digest", + "value": "\"github_account_checkpoints\".\"pull_request_backfill_digest\" IS NULL OR \"github_account_checkpoints\".\"pull_request_backfill_digest\" ~ '^[a-f0-9]{64}$'" + } + }, + "isRLSEnabled": true + }, + "public.github_account_repository_catalogs": { + "name": "github_account_repository_catalogs", + "schema": "", + "columns": { + "account_user_id": { + "name": "account_user_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "active_access": { + "name": "active_access", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inventory_generation": { + "name": "inventory_generation", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "gh_account_repo_catalogs_current_idx": { + "name": "gh_account_repo_catalogs_current_idx", + "columns": [ + { + "expression": "account_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "inventory_generation", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active_access", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gh_account_repo_catalogs_account_fk": { + "name": "gh_account_repo_catalogs_account_fk", + "tableFrom": "github_account_repository_catalogs", + "tableTo": "github_repository_inventory_heads", + "columnsFrom": ["account_user_id"], + "columnsTo": ["account_user_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gh_account_repo_catalogs_repository_fk": { + "name": "gh_account_repo_catalogs_repository_fk", + "tableFrom": "github_account_repository_catalogs", + "tableTo": "github_repositories", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "gh_account_repo_catalogs_pk": { + "name": "gh_account_repo_catalogs_pk", + "columns": ["account_user_id", "repository_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "gh_account_repo_catalogs_generation": { + "name": "gh_account_repo_catalogs_generation", + "value": "\"github_account_repository_catalogs\".\"inventory_generation\" > 0" + } + }, + "isRLSEnabled": true + }, + "public.github_commit_pull_request_associations": { + "name": "github_commit_pull_request_associations", + "schema": "", + "columns": { + "commit_repository_id": { + "name": "commit_repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "commit_sha": { + "name": "commit_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "pull_request_node_id": { + "name": "pull_request_node_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "gh_commit_pr_associations_commit_fk": { + "name": "gh_commit_pr_associations_commit_fk", + "tableFrom": "github_commit_pull_request_associations", + "tableTo": "github_commits", + "columnsFrom": ["commit_repository_id", "commit_sha"], + "columnsTo": ["repository_id", "sha"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gh_commit_pr_associations_pr_fk": { + "name": "gh_commit_pr_associations_pr_fk", + "tableFrom": "github_commit_pull_request_associations", + "tableTo": "github_pull_requests", + "columnsFrom": ["pull_request_node_id"], + "columnsTo": ["node_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "gh_commit_pr_associations_pk": { + "name": "gh_commit_pr_associations_pk", + "columns": [ + "commit_repository_id", + "commit_sha", + "pull_request_node_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "gh_commit_pr_associations_sha_shape": { + "name": "gh_commit_pr_associations_sha_shape", + "value": "\"github_commit_pull_request_associations\".\"commit_sha\" ~ '^[a-f0-9]{40}$'" + } + }, + "isRLSEnabled": true + }, + "public.github_commits": { + "name": "github_commits", + "schema": "", + "columns": { + "additions": { + "name": "additions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "author_login": { + "name": "author_login", + "type": "varchar(39)", + "primaryKey": false, + "notNull": true + }, + "authored_at": { + "name": "authored_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "changed_files": { + "name": "changed_files", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "committed_at": { + "name": "committed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "committer_at": { + "name": "committer_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "committer_user_id": { + "name": "committer_user_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "deletions": { + "name": "deletions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enrichment_error": { + "name": "enrichment_error", + "type": "varchar(80)", + "primaryKey": false, + "notNull": false + }, + "enrichment_attempts": { + "name": "enrichment_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "enrichment_lease_token": { + "name": "enrichment_lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "enrichment_lease_until": { + "name": "enrichment_lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "enrichment_state": { + "name": "enrichment_state", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "file_facts": { + "name": "file_facts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "file_facts_digest": { + "name": "file_facts_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN \"file_facts\" IS NULL THEN NULL ELSE encode(sha256(jsonb_send(\"file_facts\")), 'hex') END", + "type": "stored" + } + }, + "file_facts_complete": { + "name": "file_facts_complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "first_observed_at": { + "name": "first_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_shas": { + "name": "parent_shas", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "provider_file_cap_reached": { + "name": "provider_file_cap_reached", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pr_discovery_error": { + "name": "pr_discovery_error", + "type": "varchar(80)", + "primaryKey": false, + "notNull": false + }, + "pr_discovery_attempts": { + "name": "pr_discovery_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "pr_discovery_lease_token": { + "name": "pr_discovery_lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "pr_discovery_lease_until": { + "name": "pr_discovery_lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pr_discovery_state": { + "name": "pr_discovery_state", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "sha": { + "name": "sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "github_commits_committed_at_idx": { + "name": "github_commits_committed_at_idx", + "columns": [ + { + "expression": "committed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_commits_enrichment_pending_idx": { + "name": "github_commits_enrichment_pending_idx", + "columns": [ + { + "expression": "enrichment_state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enrichment_lease_until", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "committed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_commits_pr_discovery_pending_idx": { + "name": "github_commits_pr_discovery_pending_idx", + "columns": [ + { + "expression": "pr_discovery_state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_discovery_lease_until", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "first_observed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_commits_repository_fk": { + "name": "github_commits_repository_fk", + "tableFrom": "github_commits", + "tableTo": "github_repositories", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "github_commits_repository_id_sha_pk": { + "name": "github_commits_repository_id_sha_pk", + "columns": ["repository_id", "sha"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_commits_sha_shape": { + "name": "github_commits_sha_shape", + "value": "\"github_commits\".\"sha\" ~ '^[a-f0-9]{40}$'" + }, + "github_commits_parent_shas_array": { + "name": "github_commits_parent_shas_array", + "value": "\"github_commits\".\"parent_shas\" IS NULL OR jsonb_typeof(\"github_commits\".\"parent_shas\") = 'array'" + }, + "github_commits_file_facts_array": { + "name": "github_commits_file_facts_array", + "value": "\"github_commits\".\"file_facts\" IS NULL OR jsonb_typeof(\"github_commits\".\"file_facts\") = 'array'" + }, + "github_commits_file_facts_completeness": { + "name": "github_commits_file_facts_completeness", + "value": "NOT \"github_commits\".\"file_facts_complete\" OR (\"github_commits\".\"file_facts\" IS NOT NULL AND NOT \"github_commits\".\"provider_file_cap_reached\")" + }, + "github_commits_enrichment_state": { + "name": "github_commits_enrichment_state", + "value": "\"github_commits\".\"enrichment_state\" IN ('pending', 'processing', 'complete', 'unavailable')" + }, + "github_commits_enrichment_lease": { + "name": "github_commits_enrichment_lease", + "value": "(\"github_commits\".\"enrichment_state\" = 'processing') = (\"github_commits\".\"enrichment_lease_token\" IS NOT NULL) AND (\"github_commits\".\"enrichment_state\" <> 'processing' OR \"github_commits\".\"enrichment_lease_until\" IS NOT NULL) AND (\"github_commits\".\"enrichment_state\" NOT IN ('complete', 'unavailable') OR \"github_commits\".\"enrichment_lease_until\" IS NULL)" + }, + "github_commits_pr_discovery_state": { + "name": "github_commits_pr_discovery_state", + "value": "\"github_commits\".\"pr_discovery_state\" IN ('pending', 'processing', 'complete', 'unavailable')" + }, + "github_commits_pr_discovery_lease": { + "name": "github_commits_pr_discovery_lease", + "value": "(\"github_commits\".\"pr_discovery_state\" = 'processing') = (\"github_commits\".\"pr_discovery_lease_token\" IS NOT NULL) AND (\"github_commits\".\"pr_discovery_state\" <> 'processing' OR \"github_commits\".\"pr_discovery_lease_until\" IS NOT NULL) AND (\"github_commits\".\"pr_discovery_state\" NOT IN ('complete', 'unavailable') OR \"github_commits\".\"pr_discovery_lease_until\" IS NULL)" + }, + "github_commits_tracked_author": { + "name": "github_commits_tracked_author", + "value": "\"github_commits\".\"author_login\" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$'" + }, + "github_commits_nonnegative_activity_counts": { + "name": "github_commits_nonnegative_activity_counts", + "value": "(\"github_commits\".\"changed_files\" IS NULL OR \"github_commits\".\"changed_files\" >= 0) AND (\"github_commits\".\"additions\" IS NULL OR \"github_commits\".\"additions\" >= 0) AND (\"github_commits\".\"deletions\" IS NULL OR \"github_commits\".\"deletions\" >= 0)" + }, + "github_commits_nonnegative_attempts": { + "name": "github_commits_nonnegative_attempts", + "value": "\"github_commits\".\"enrichment_attempts\" >= 0 AND \"github_commits\".\"pr_discovery_attempts\" >= 0" + } + }, + "isRLSEnabled": true + }, + "public.github_issues": { + "name": "github_issues", + "schema": "", + "columns": { + "account": { + "name": "account", + "type": "varchar(39)", + "primaryKey": false, + "notNull": true + }, + "author_login": { + "name": "author_login", + "type": "varchar(39)", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "first_observed_at": { + "name": "first_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "node_id": { + "name": "node_id", + "type": "varchar(128)", + "primaryKey": true, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "title_snapshot": { + "name": "title_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url_snapshot": { + "name": "url_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "github_issues_repository_number_unique": { + "name": "github_issues_repository_number_unique", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_issues_author_idx": { + "name": "github_issues_author_idx", + "columns": [ + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_issues_repository_id_github_repositories_id_fk": { + "name": "github_issues_repository_id_github_repositories_id_fk", + "tableFrom": "github_issues", + "tableTo": "github_repositories", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_issues_tracked_account": { + "name": "github_issues_tracked_account", + "value": "\"github_issues\".\"account\" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$'" + }, + "github_issues_positive_number": { + "name": "github_issues_positive_number", + "value": "\"github_issues\".\"number\" > 0" + } + }, + "isRLSEnabled": true + }, + "public.github_public_feed_head": { + "name": "github_public_feed_head", + "schema": "", + "columns": { + "feed_revision": { + "name": "feed_revision", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "head_content_revision": { + "name": "head_content_revision", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "id": { + "name": "id", + "type": "boolean", + "primaryKey": true, + "notNull": true, + "default": true + }, + "last_published_at": { + "name": "last_published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ordering_revision": { + "name": "ordering_revision", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "projection_request_token": { + "name": "projection_request_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "summary_policy_digest": { + "name": "summary_policy_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "summarizing": { + "name": "summarizing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "gh_public_feed_head_singleton": { + "name": "gh_public_feed_head_singleton", + "value": "\"github_public_feed_head\".\"id\"" + }, + "gh_public_feed_head_revisions": { + "name": "gh_public_feed_head_revisions", + "value": "\"github_public_feed_head\".\"feed_revision\" >= 0 AND \"github_public_feed_head\".\"head_content_revision\" >= 0 AND \"github_public_feed_head\".\"ordering_revision\" >= 0" + }, + "gh_public_feed_head_summary_policy_digest": { + "name": "gh_public_feed_head_summary_policy_digest", + "value": "\"github_public_feed_head\".\"summary_policy_digest\" IS NULL OR \"github_public_feed_head\".\"summary_policy_digest\" ~ '^[a-f0-9]{64}$'" + } + }, + "isRLSEnabled": true + }, + "public.github_pull_request_memberships": { + "name": "github_pull_request_memberships", + "schema": "", + "columns": { + "commit_repository_id": { + "name": "commit_repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "commit_sha": { + "name": "commit_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "is_head": { + "name": "is_head", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "version_id": { + "name": "version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "github_pull_request_memberships_position_unique": { + "name": "github_pull_request_memberships_position_unique", + "columns": [ + { + "expression": "version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_pull_request_memberships_commit_lookup_idx": { + "name": "github_pull_request_memberships_commit_lookup_idx", + "columns": [ + { + "expression": "commit_repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "commit_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gh_pr_memberships_version_fk": { + "name": "gh_pr_memberships_version_fk", + "tableFrom": "github_pull_request_memberships", + "tableTo": "github_pull_request_versions", + "columnsFrom": ["version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "gh_pr_memberships_pk": { + "name": "gh_pr_memberships_pk", + "columns": ["version_id", "commit_repository_id", "commit_sha"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_pull_request_memberships_sha_shape": { + "name": "github_pull_request_memberships_sha_shape", + "value": "\"github_pull_request_memberships\".\"commit_sha\" ~ '^[a-f0-9]{40}$'" + }, + "github_pull_request_memberships_nonnegative_position": { + "name": "github_pull_request_memberships_nonnegative_position", + "value": "\"github_pull_request_memberships\".\"position\" >= 0" + } + }, + "isRLSEnabled": true + }, + "public.github_pull_request_signals": { + "name": "github_pull_request_signals", + "schema": "", + "columns": { + "account": { + "name": "account", + "type": "varchar(39)", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "varchar(80)", + "primaryKey": false, + "notNull": false + }, + "event_id": { + "name": "event_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "lease_token": { + "name": "lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "repository_name_snapshot": { + "name": "repository_name_snapshot", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + } + }, + "indexes": { + "github_pull_request_signals_event_unique": { + "name": "github_pull_request_signals_event_unique", + "columns": [ + { + "expression": "account", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_pull_request_signals_pending_idx": { + "name": "github_pull_request_signals_pending_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_until", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "observed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_pull_request_signals_account": { + "name": "github_pull_request_signals_account", + "value": "\"github_pull_request_signals\".\"account\" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$'" + }, + "github_pull_request_signals_event_id": { + "name": "github_pull_request_signals_event_id", + "value": "\"github_pull_request_signals\".\"event_id\" ~ '^[0-9]{1,64}$'" + }, + "github_pull_request_signals_repository_id": { + "name": "github_pull_request_signals_repository_id", + "value": "\"github_pull_request_signals\".\"repository_id\" ~ '^[0-9]{1,32}$'" + }, + "github_pull_request_signals_state": { + "name": "github_pull_request_signals_state", + "value": "\"github_pull_request_signals\".\"state\" IN ('pending', 'processing', 'complete', 'unavailable')" + }, + "github_pull_request_signals_lease": { + "name": "github_pull_request_signals_lease", + "value": "(\"github_pull_request_signals\".\"state\" = 'processing') = (\"github_pull_request_signals\".\"lease_token\" IS NOT NULL) AND (\"github_pull_request_signals\".\"state\" <> 'processing' OR \"github_pull_request_signals\".\"lease_until\" IS NOT NULL)" + }, + "github_pull_request_signals_values": { + "name": "github_pull_request_signals_values", + "value": "\"github_pull_request_signals\".\"number\" > 0 AND \"github_pull_request_signals\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": true + }, + "public.github_pull_request_versions": { + "name": "github_pull_request_versions", + "schema": "", + "columns": { + "base_ref_name": { + "name": "base_ref_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_repository_id": { + "name": "base_repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "base_sha": { + "name": "base_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "commit_count": { + "name": "commit_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "file_facts": { + "name": "file_facts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "file_facts_digest": { + "name": "file_facts_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN \"file_facts\" IS NULL THEN NULL ELSE encode(sha256(jsonb_send(\"file_facts\")), 'hex') END", + "type": "stored" + } + }, + "file_facts_complete": { + "name": "file_facts_complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "head_ref_name": { + "name": "head_ref_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "head_repository_id": { + "name": "head_repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "head_sha": { + "name": "head_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "is_current": { + "name": "is_current", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "membership_complete": { + "name": "membership_complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "merge_snapshot": { + "name": "merge_snapshot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "provider_updated_at": { + "name": "provider_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "pull_request_node_id": { + "name": "pull_request_node_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "github_pull_request_versions_head_unique": { + "name": "github_pull_request_versions_head_unique", + "columns": [ + { + "expression": "pull_request_node_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "head_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_pull_request_versions_current_unique": { + "name": "github_pull_request_versions_current_unique", + "columns": [ + { + "expression": "pull_request_node_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"github_pull_request_versions\".\"is_current\"", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gh_pr_versions_pull_request_fk": { + "name": "gh_pr_versions_pull_request_fk", + "tableFrom": "github_pull_request_versions", + "tableTo": "github_pull_requests", + "columnsFrom": ["pull_request_node_id"], + "columnsTo": ["node_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_pull_request_versions_sha_shapes": { + "name": "github_pull_request_versions_sha_shapes", + "value": "\"github_pull_request_versions\".\"base_sha\" ~ '^[a-f0-9]{40}$' AND \"github_pull_request_versions\".\"head_sha\" ~ '^[a-f0-9]{40}$'" + }, + "github_pull_request_versions_nonnegative_count": { + "name": "github_pull_request_versions_nonnegative_count", + "value": "\"github_pull_request_versions\".\"commit_count\" IS NULL OR \"github_pull_request_versions\".\"commit_count\" >= 0" + }, + "github_pull_request_versions_file_facts_array": { + "name": "github_pull_request_versions_file_facts_array", + "value": "\"github_pull_request_versions\".\"file_facts\" IS NULL OR jsonb_typeof(\"github_pull_request_versions\".\"file_facts\") = 'array'" + }, + "github_pull_request_versions_file_facts_complete": { + "name": "github_pull_request_versions_file_facts_complete", + "value": "NOT \"github_pull_request_versions\".\"file_facts_complete\" OR \"github_pull_request_versions\".\"file_facts\" IS NOT NULL" + } + }, + "isRLSEnabled": true + }, + "public.github_pull_requests": { + "name": "github_pull_requests", + "schema": "", + "columns": { + "account": { + "name": "account", + "type": "varchar(39)", + "primaryKey": false, + "notNull": true + }, + "additions": { + "name": "additions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "author_login": { + "name": "author_login", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "base_ref_name": { + "name": "base_ref_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_repository_id": { + "name": "base_repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "base_sha": { + "name": "base_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_snapshot": { + "name": "body_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changed_files": { + "name": "changed_files", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "commit_count": { + "name": "commit_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "deletions": { + "name": "deletions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "draft": { + "name": "draft", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "first_observed_at": { + "name": "first_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "head_ref_name": { + "name": "head_ref_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "head_repository_id": { + "name": "head_repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "head_sha": { + "name": "head_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": false + }, + "last_reconciled_at": { + "name": "last_reconciled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "merged_at": { + "name": "merged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "merge_sha": { + "name": "merge_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": false + }, + "merge_sha_verified_at": { + "name": "merge_sha_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_reconcile_at": { + "name": "next_reconcile_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "node_id": { + "name": "node_id", + "type": "varchar(128)", + "primaryKey": true, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider_updated_at": { + "name": "provider_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "reconcile_attempts": { + "name": "reconcile_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reconcile_error": { + "name": "reconcile_error", + "type": "varchar(80)", + "primaryKey": false, + "notNull": false + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title_snapshot": { + "name": "title_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "github_pull_requests_repository_number_unique": { + "name": "github_pull_requests_repository_number_unique", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_pull_requests_reconciliation_idx": { + "name": "github_pull_requests_reconciliation_idx", + "columns": [ + { + "expression": "account", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_reconcile_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_pull_requests_author_idx": { + "name": "github_pull_requests_author_idx", + "columns": [ + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_pull_requests_repository_id_github_repositories_id_fk": { + "name": "github_pull_requests_repository_id_github_repositories_id_fk", + "tableFrom": "github_pull_requests", + "tableTo": "github_repositories", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_pull_requests_tracked_account": { + "name": "github_pull_requests_tracked_account", + "value": "\"github_pull_requests\".\"account\" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$'" + }, + "github_pull_requests_state": { + "name": "github_pull_requests_state", + "value": "\"github_pull_requests\".\"state\" IN ('open', 'closed', 'merged')" + }, + "github_pull_requests_terminal_state": { + "name": "github_pull_requests_terminal_state", + "value": "(\"github_pull_requests\".\"state\" = 'open' AND \"github_pull_requests\".\"terminal_at\" IS NULL) OR (\"github_pull_requests\".\"state\" IN ('closed', 'merged') AND \"github_pull_requests\".\"terminal_at\" IS NOT NULL)" + }, + "github_pull_requests_merged_state": { + "name": "github_pull_requests_merged_state", + "value": "(\"github_pull_requests\".\"state\" = 'merged') = (\"github_pull_requests\".\"merged_at\" IS NOT NULL)" + }, + "github_pull_requests_sha_shapes": { + "name": "github_pull_requests_sha_shapes", + "value": "(\"github_pull_requests\".\"base_sha\" IS NULL OR \"github_pull_requests\".\"base_sha\" ~ '^[a-f0-9]{40}$') AND (\"github_pull_requests\".\"head_sha\" IS NULL OR \"github_pull_requests\".\"head_sha\" ~ '^[a-f0-9]{40}$') AND (\"github_pull_requests\".\"merge_sha\" IS NULL OR \"github_pull_requests\".\"merge_sha\" ~ '^[a-f0-9]{40}$')" + }, + "github_pull_requests_verified_merge_sha": { + "name": "github_pull_requests_verified_merge_sha", + "value": "(\"github_pull_requests\".\"merge_sha\" IS NULL AND \"github_pull_requests\".\"merge_sha_verified_at\" IS NULL) OR (\"github_pull_requests\".\"state\" = 'merged' AND \"github_pull_requests\".\"merge_sha_verified_at\" IS NOT NULL)" + }, + "github_pull_requests_positive_number": { + "name": "github_pull_requests_positive_number", + "value": "\"github_pull_requests\".\"number\" > 0" + }, + "github_pull_requests_nonnegative_counts": { + "name": "github_pull_requests_nonnegative_counts", + "value": "(\"github_pull_requests\".\"changed_files\" IS NULL OR \"github_pull_requests\".\"changed_files\" >= 0) AND (\"github_pull_requests\".\"additions\" IS NULL OR \"github_pull_requests\".\"additions\" >= 0) AND (\"github_pull_requests\".\"deletions\" IS NULL OR \"github_pull_requests\".\"deletions\" >= 0) AND (\"github_pull_requests\".\"commit_count\" IS NULL OR \"github_pull_requests\".\"commit_count\" >= 0)" + }, + "github_pull_requests_nonnegative_attempts": { + "name": "github_pull_requests_nonnegative_attempts", + "value": "\"github_pull_requests\".\"reconcile_attempts\" >= 0" + } + }, + "isRLSEnabled": true + }, + "public.github_push_observation_commits": { + "name": "github_push_observation_commits", + "schema": "", + "columns": { + "observation_id": { + "name": "observation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "sha": { + "name": "sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "github_push_observation_commits_position_unique": { + "name": "github_push_observation_commits_position_unique", + "columns": [ + { + "expression": "observation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gh_push_observation_commits_observation_fk": { + "name": "gh_push_observation_commits_observation_fk", + "tableFrom": "github_push_observation_commits", + "tableTo": "github_push_observations", + "columnsFrom": ["observation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "gh_push_observation_commits_pk": { + "name": "gh_push_observation_commits_pk", + "columns": ["observation_id", "repository_id", "sha"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_push_observation_commits_sha_shape": { + "name": "github_push_observation_commits_sha_shape", + "value": "\"github_push_observation_commits\".\"sha\" ~ '^[a-f0-9]{40}$'" + }, + "github_push_observation_commits_nonnegative_position": { + "name": "github_push_observation_commits_nonnegative_position", + "value": "\"github_push_observation_commits\".\"position\" >= 0" + } + }, + "isRLSEnabled": true + }, + "public.github_push_observations": { + "name": "github_push_observations", + "schema": "", + "columns": { + "account": { + "name": "account", + "type": "varchar(39)", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "after_sha": { + "name": "after_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "before_sha": { + "name": "before_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "varchar(80)", + "primaryKey": false, + "notNull": false + }, + "expected_commit_count": { + "name": "expected_commit_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "history_since_at": { + "name": "history_since_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "history_until_at": { + "name": "history_until_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "lease_token": { + "name": "lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "provider_created_at": { + "name": "provider_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ref_name": { + "name": "ref_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "repository_name_snapshot": { + "name": "repository_name_snapshot", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + } + }, + "indexes": { + "github_push_observations_source_unique": { + "name": "github_push_observations_source_unique", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_push_observations_push_unique": { + "name": "github_push_observations_push_unique", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ref_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "before_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "after_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"github_push_observations\".\"source\" <> 'backfill'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_push_observations_pending_idx": { + "name": "github_push_observations_pending_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_until", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "observed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_push_observations_account_idx": { + "name": "github_push_observations_account_idx", + "columns": [ + { + "expression": "account", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "observed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gh_push_observations_repository_fk": { + "name": "gh_push_observations_repository_fk", + "tableFrom": "github_push_observations", + "tableTo": "github_repositories", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_push_observations_tracked_account": { + "name": "github_push_observations_tracked_account", + "value": "\"github_push_observations\".\"account\" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$'" + }, + "github_push_observations_source": { + "name": "github_push_observations_source", + "value": "\"github_push_observations\".\"source\" IN ('webhook', 'events', 'refs', 'backfill')" + }, + "github_push_observations_sha_shape": { + "name": "github_push_observations_sha_shape", + "value": "\"github_push_observations\".\"before_sha\" ~ '^[a-f0-9]{40}$' AND \"github_push_observations\".\"after_sha\" ~ '^[a-f0-9]{40}$'" + }, + "github_push_observations_nonnegative_count": { + "name": "github_push_observations_nonnegative_count", + "value": "\"github_push_observations\".\"attempt_count\" >= 0 AND (\"github_push_observations\".\"expected_commit_count\" IS NULL OR \"github_push_observations\".\"expected_commit_count\" >= 0)" + }, + "github_push_observations_history_bounds": { + "name": "github_push_observations_history_bounds", + "value": "(\"github_push_observations\".\"source\" <> 'backfill' AND \"github_push_observations\".\"history_since_at\" IS NULL AND \"github_push_observations\".\"history_until_at\" IS NULL) OR (\"github_push_observations\".\"source\" = 'backfill' AND \"github_push_observations\".\"history_since_at\" IS NOT NULL AND \"github_push_observations\".\"history_until_at\" IS NOT NULL AND \"github_push_observations\".\"history_since_at\" <= \"github_push_observations\".\"history_until_at\" AND \"github_push_observations\".\"expected_commit_count\" IS NULL AND \"github_push_observations\".\"before_sha\" = repeat('0', 40))" + }, + "github_push_observations_state": { + "name": "github_push_observations_state", + "value": "\"github_push_observations\".\"state\" IN ('pending', 'processing', 'complete', 'deferred', 'unavailable')" + }, + "github_push_observations_lease": { + "name": "github_push_observations_lease", + "value": "(\"github_push_observations\".\"state\" = 'processing') = (\"github_push_observations\".\"lease_token\" IS NOT NULL) AND (\"github_push_observations\".\"state\" <> 'processing' OR \"github_push_observations\".\"lease_until\" IS NOT NULL)" + } + }, + "isRLSEnabled": true + }, + "public.github_ref_generations": { + "name": "github_ref_generations", + "schema": "", + "columns": { + "branch_lineage_id": { + "name": "branch_lineage_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "coverage_since_at": { + "name": "coverage_since_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "ref_name": { + "name": "ref_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "gh_ref_generations_lineage_idx": { + "name": "gh_ref_generations_lineage_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "branch_lineage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gh_ref_generations_repository_fk": { + "name": "gh_ref_generations_repository_fk", + "tableFrom": "github_ref_generations", + "tableTo": "github_repositories", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "gh_ref_generations_pk": { + "name": "gh_ref_generations_pk", + "columns": ["repository_id", "ref_name"] + } + }, + "uniqueConstraints": { + "gh_ref_generations_version_unique": { + "name": "gh_ref_generations_version_unique", + "nullsNotDistinct": false, + "columns": ["repository_id", "ref_name", "generation"] + } + }, + "policies": {}, + "checkConstraints": { + "gh_ref_generations_head_name": { + "name": "gh_ref_generations_head_name", + "value": "\"github_ref_generations\".\"ref_name\" LIKE 'refs/heads/%'" + }, + "gh_ref_generations_sha_shape": { + "name": "gh_ref_generations_sha_shape", + "value": "\"github_ref_generations\".\"head_sha\" ~ '^[a-f0-9]{40}$'" + }, + "gh_ref_generations_positive": { + "name": "gh_ref_generations_positive", + "value": "\"github_ref_generations\".\"generation\" > 0" + } + }, + "isRLSEnabled": true + }, + "public.github_ref_memberships": { + "name": "github_ref_memberships", + "schema": "", + "columns": { + "commit_repository_id": { + "name": "commit_repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "commit_sha": { + "name": "commit_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ref_name": { + "name": "ref_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "gh_ref_memberships_position_unique": { + "name": "gh_ref_memberships_position_unique", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ref_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gh_ref_memberships_commit_idx": { + "name": "gh_ref_memberships_commit_idx", + "columns": [ + { + "expression": "commit_repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "commit_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gh_ref_memberships_generation_fk": { + "name": "gh_ref_memberships_generation_fk", + "tableFrom": "github_ref_memberships", + "tableTo": "github_ref_generations", + "columnsFrom": ["repository_id", "ref_name", "generation"], + "columnsTo": ["repository_id", "ref_name", "generation"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gh_ref_memberships_commit_fk": { + "name": "gh_ref_memberships_commit_fk", + "tableFrom": "github_ref_memberships", + "tableTo": "github_commits", + "columnsFrom": ["commit_repository_id", "commit_sha"], + "columnsTo": ["repository_id", "sha"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "gh_ref_memberships_pk": { + "name": "gh_ref_memberships_pk", + "columns": [ + "repository_id", + "ref_name", + "commit_repository_id", + "commit_sha" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "gh_ref_memberships_values": { + "name": "gh_ref_memberships_values", + "value": "\"github_ref_memberships\".\"generation\" > 0 AND \"github_ref_memberships\".\"position\" >= 0 AND \"github_ref_memberships\".\"commit_sha\" ~ '^[a-f0-9]{40}$'" + } + }, + "isRLSEnabled": true + }, + "public.github_repositories": { + "name": "github_repositories", + "schema": "", + "columns": { + "default_branch": { + "name": "default_branch", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_observed_at": { + "name": "first_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "facts_verified_at": { + "name": "facts_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "inventory_verified_at": { + "name": "inventory_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "full_name": { + "name": "full_name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "homepage_url": { + "name": "homepage_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heads_last_reconciled_at": { + "name": "heads_last_reconciled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "html_url": { + "name": "html_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "varchar(32)", + "primaryKey": true, + "notNull": true + }, + "last_observed_at": { + "name": "last_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "owner_avatar_url": { + "name": "owner_avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "owner_login": { + "name": "owner_login", + "type": "varchar(39)", + "primaryKey": false, + "notNull": false + }, + "owner_type": { + "name": "owner_type", + "type": "varchar(12)", + "primaryKey": false, + "notNull": false + }, + "pushed_at": { + "name": "pushed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "topics": { + "name": "topics", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tags_last_reconciled_at": { + "name": "tags_last_reconciled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "varchar(12)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "github_repositories_full_name_idx": { + "name": "github_repositories_full_name_idx", + "columns": [ + { + "expression": "full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_repositories_full_name": { + "name": "github_repositories_full_name", + "value": "length(btrim(\"github_repositories\".\"full_name\")) > 0" + }, + "github_repositories_owner_type": { + "name": "github_repositories_owner_type", + "value": "\"github_repositories\".\"owner_type\" IS NULL OR \"github_repositories\".\"owner_type\" IN ('Organization', 'User')" + }, + "github_repositories_visibility": { + "name": "github_repositories_visibility", + "value": "\"github_repositories\".\"visibility\" IS NULL OR \"github_repositories\".\"visibility\" IN ('public', 'private', 'internal')" + }, + "github_repositories_topics_array": { + "name": "github_repositories_topics_array", + "value": "\"github_repositories\".\"topics\" IS NULL OR jsonb_typeof(\"github_repositories\".\"topics\") = 'array'" + }, + "github_repositories_observation_order": { + "name": "github_repositories_observation_order", + "value": "\"github_repositories\".\"last_observed_at\" >= \"github_repositories\".\"first_observed_at\"" + } + }, + "isRLSEnabled": true + }, + "public.github_repository_inventory_heads": { + "name": "github_repository_inventory_heads", + "schema": "", + "columns": { + "account_login": { + "name": "account_login", + "type": "varchar(39)", + "primaryKey": false, + "notNull": true + }, + "account_user_id": { + "name": "account_user_id", + "type": "varchar(32)", + "primaryKey": true, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "generation": { + "name": "generation", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "gh_repo_inventory_head_account_id": { + "name": "gh_repo_inventory_head_account_id", + "value": "\"github_repository_inventory_heads\".\"account_user_id\" ~ '^[0-9]{1,32}$'" + }, + "gh_repo_inventory_head_generation": { + "name": "gh_repo_inventory_head_generation", + "value": "(\"github_repository_inventory_heads\".\"generation\" = 0 AND \"github_repository_inventory_heads\".\"completed_at\" IS NULL) OR (\"github_repository_inventory_heads\".\"generation\" > 0 AND \"github_repository_inventory_heads\".\"completed_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": true + }, + "public.github_repository_refs": { + "name": "github_repository_refs", + "schema": "", + "columns": { + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "branch_lineage_id": { + "name": "branch_lineage_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "first_observed_at": { + "name": "first_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "head_sha": { + "name": "head_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true + }, + "last_observed_at": { + "name": "last_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "projection_relevant": { + "name": "projection_relevant", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ref_name": { + "name": "ref_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repair_attempts": { + "name": "repair_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "repair_error": { + "name": "repair_error", + "type": "varchar(80)", + "primaryKey": false, + "notNull": false + }, + "repair_lease_token": { + "name": "repair_lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repair_lease_until": { + "name": "repair_lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "github_repository_refs_active_idx": { + "name": "github_repository_refs_active_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_repository_refs_projection_idx": { + "name": "github_repository_refs_projection_idx", + "columns": [ + { + "expression": "projection_relevant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_observed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_repository_refs_repository_fk": { + "name": "github_repository_refs_repository_fk", + "tableFrom": "github_repository_refs", + "tableTo": "github_repositories", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "github_repository_refs_repository_id_ref_name_pk": { + "name": "github_repository_refs_repository_id_ref_name_pk", + "columns": ["repository_id", "ref_name"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_repository_refs_kind": { + "name": "github_repository_refs_kind", + "value": "\"github_repository_refs\".\"kind\" IN ('head', 'tag')" + }, + "github_repository_refs_name": { + "name": "github_repository_refs_name", + "value": "(\"github_repository_refs\".\"kind\" = 'head' AND \"github_repository_refs\".\"ref_name\" LIKE 'refs/heads/%') OR (\"github_repository_refs\".\"kind\" = 'tag' AND \"github_repository_refs\".\"ref_name\" LIKE 'refs/tags/%')" + }, + "github_repository_refs_lineage": { + "name": "github_repository_refs_lineage", + "value": "(\"github_repository_refs\".\"kind\" = 'head') = (\"github_repository_refs\".\"branch_lineage_id\" IS NOT NULL)" + }, + "github_repository_refs_projection_relevance": { + "name": "github_repository_refs_projection_relevance", + "value": "NOT \"github_repository_refs\".\"projection_relevant\" OR \"github_repository_refs\".\"kind\" = 'head'" + }, + "github_repository_refs_sha_shape": { + "name": "github_repository_refs_sha_shape", + "value": "\"github_repository_refs\".\"head_sha\" ~ '^[a-f0-9]{40}$'" + }, + "github_repository_refs_observation_order": { + "name": "github_repository_refs_observation_order", + "value": "\"github_repository_refs\".\"last_observed_at\" >= \"github_repository_refs\".\"first_observed_at\"" + }, + "github_repository_refs_repair_lease": { + "name": "github_repository_refs_repair_lease", + "value": "(\"github_repository_refs\".\"repair_lease_token\" IS NULL) = (\"github_repository_refs\".\"repair_lease_until\" IS NULL) AND (\"github_repository_refs\".\"repair_lease_token\" IS NULL OR \"github_repository_refs\".\"kind\" = 'head')" + }, + "github_repository_refs_repair_attempts": { + "name": "github_repository_refs_repair_attempts", + "value": "\"github_repository_refs\".\"repair_attempts\" >= 0" + } + }, + "isRLSEnabled": true + }, + "public.github_webhook_deliveries": { + "name": "github_webhook_deliveries", + "schema": "", + "columns": { + "accepted": { + "name": "accepted", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "account": { + "name": "account", + "type": "varchar(39)", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "varchar(40)", + "primaryKey": false, + "notNull": false + }, + "delivery_id": { + "name": "delivery_id", + "type": "varchar(36)", + "primaryKey": true, + "notNull": true + }, + "event": { + "name": "event", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "github_webhook_deliveries_audit_idx": { + "name": "github_webhook_deliveries_audit_idx", + "columns": [ + { + "expression": "event", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "observed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_webhook_deliveries_id_shape": { + "name": "github_webhook_deliveries_id_shape", + "value": "\"github_webhook_deliveries\".\"delivery_id\" ~ '^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$'" + }, + "github_webhook_deliveries_event_shape": { + "name": "github_webhook_deliveries_event_shape", + "value": "\"github_webhook_deliveries\".\"event\" ~ '^[a-z][a-z0-9_]{0,39}$'" + }, + "github_webhook_deliveries_action_shape": { + "name": "github_webhook_deliveries_action_shape", + "value": "\"github_webhook_deliveries\".\"action\" IS NULL OR \"github_webhook_deliveries\".\"action\" ~ '^[a-z][a-z0-9_]{0,39}$'" + }, + "github_webhook_deliveries_repository_id_shape": { + "name": "github_webhook_deliveries_repository_id_shape", + "value": "\"github_webhook_deliveries\".\"repository_id\" IS NULL OR \"github_webhook_deliveries\".\"repository_id\" ~ '^[0-9]{1,32}$'" + }, + "github_webhook_deliveries_tracked_account": { + "name": "github_webhook_deliveries_tracked_account", + "value": "\"github_webhook_deliveries\".\"account\" IS NULL OR \"github_webhook_deliveries\".\"account\" ~ '^[a-z0-9]([a-z0-9-]{0,37}[a-z0-9])?$'" + } + }, + "isRLSEnabled": true + }, + "public.github_work_unit_accepted_summaries": { + "name": "github_work_unit_accepted_summaries", + "schema": "", + "columns": { + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "attribution_mode": { + "name": "attribution_mode", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "identity_key": { + "name": "identity_key", + "type": "varchar(180)", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome_digest": { + "name": "outcome_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "recipe": { + "name": "recipe", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "summary_input_digest": { + "name": "summary_input_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "gh_work_unit_accepted_summaries_identity_idx": { + "name": "gh_work_unit_accepted_summaries_identity_idx", + "columns": [ + { + "expression": "identity_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attribution_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recipe", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "accepted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gh_work_unit_accepted_summaries_outcome_idx": { + "name": "gh_work_unit_accepted_summaries_outcome_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "outcome_digest", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attribution_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recipe", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "accepted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "gh_work_unit_accepted_summaries_pk": { + "name": "gh_work_unit_accepted_summaries_pk", + "columns": ["identity_key", "summary_input_digest", "recipe"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "gh_work_unit_accepted_summaries_digests": { + "name": "gh_work_unit_accepted_summaries_digests", + "value": "\"github_work_unit_accepted_summaries\".\"outcome_digest\" ~ '^[a-f0-9]{64}$' AND \"github_work_unit_accepted_summaries\".\"summary_input_digest\" ~ '^[a-f0-9]{64}$'" + }, + "gh_work_unit_accepted_summaries_attribution": { + "name": "gh_work_unit_accepted_summaries_attribution", + "value": "\"github_work_unit_accepted_summaries\".\"attribution_mode\" IN ('tracked_authored_pr', 'foreign_pr_contribution', 'canonical_owned_composite', 'branch_owned_composite')" + } + }, + "isRLSEnabled": true + }, + "public.github_work_unit_memberships": { + "name": "github_work_unit_memberships", + "schema": "", + "columns": { + "logical_repository_id": { + "name": "logical_repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "logical_sha": { + "name": "logical_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "work_unit_id": { + "name": "work_unit_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "gh_work_unit_memberships_position_unique": { + "name": "gh_work_unit_memberships_position_unique", + "columns": [ + { + "expression": "work_unit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gh_work_unit_memberships_commit_unique": { + "name": "gh_work_unit_memberships_commit_unique", + "columns": [ + { + "expression": "logical_repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "logical_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gh_work_unit_memberships_unit_fk": { + "name": "gh_work_unit_memberships_unit_fk", + "tableFrom": "github_work_unit_memberships", + "tableTo": "github_work_units", + "columnsFrom": ["work_unit_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gh_work_unit_memberships_commit_fk": { + "name": "gh_work_unit_memberships_commit_fk", + "tableFrom": "github_work_unit_memberships", + "tableTo": "github_commits", + "columnsFrom": ["logical_repository_id", "logical_sha"], + "columnsTo": ["repository_id", "sha"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "gh_work_unit_memberships_pk": { + "name": "gh_work_unit_memberships_pk", + "columns": ["work_unit_id", "logical_repository_id", "logical_sha"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "gh_work_unit_memberships_values": { + "name": "gh_work_unit_memberships_values", + "value": "\"github_work_unit_memberships\".\"position\" >= 0 AND \"github_work_unit_memberships\".\"logical_sha\" ~ '^[a-f0-9]{40}$'" + } + }, + "isRLSEnabled": true + }, + "public.github_work_unit_summary_attempts": { + "name": "github_work_unit_summary_attempts", + "schema": "", + "columns": { + "identity_key": { + "name": "identity_key", + "type": "varchar(180)", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "error_code": { + "name": "error_code", + "type": "varchar(80)", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attribution_mode": { + "name": "attribution_mode", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "debounce_until": { + "name": "debounce_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_started_at": { + "name": "request_started_at", + "type": "timestamp with time zone[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::timestamptz[]" + }, + "last_started_at": { + "name": "last_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "lease_token": { + "name": "lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome_digest": { + "name": "outcome_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "recipe": { + "name": "recipe", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "request_payload": { + "name": "request_payload", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "started_requests": { + "name": "started_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "state": { + "name": "state", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "summary_input_digest": { + "name": "summary_input_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "work_unit_id": { + "name": "work_unit_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "gh_work_unit_summary_input_unique": { + "name": "gh_work_unit_summary_input_unique", + "columns": [ + { + "expression": "work_unit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "summary_input_digest", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recipe", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gh_work_unit_summary_claim_idx": { + "name": "gh_work_unit_summary_claim_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "debounce_until", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gh_work_unit_summary_attempts_identity_idx": { + "name": "gh_work_unit_summary_attempts_identity_idx", + "columns": [ + { + "expression": "identity_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "gh_work_unit_summary_attempts_pk": { + "name": "gh_work_unit_summary_attempts_pk", + "columns": ["work_unit_id", "revision"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "gh_work_unit_summary_state": { + "name": "gh_work_unit_summary_state", + "value": "\"github_work_unit_summary_attempts\".\"state\" IN ('pending', 'processing', 'retryable', 'accepted', 'terminal')" + }, + "gh_work_unit_summary_digests": { + "name": "gh_work_unit_summary_digests", + "value": "\"github_work_unit_summary_attempts\".\"outcome_digest\" ~ '^[a-f0-9]{64}$' AND \"github_work_unit_summary_attempts\".\"summary_input_digest\" ~ '^[a-f0-9]{64}$'" + }, + "gh_work_unit_summary_attribution": { + "name": "gh_work_unit_summary_attribution", + "value": "\"github_work_unit_summary_attempts\".\"attribution_mode\" IN ('tracked_authored_pr', 'foreign_pr_contribution', 'canonical_owned_composite', 'branch_owned_composite')" + }, + "gh_work_unit_summary_revisions": { + "name": "gh_work_unit_summary_revisions", + "value": "\"github_work_unit_summary_attempts\".\"revision\" > 0 AND \"github_work_unit_summary_attempts\".\"started_requests\" BETWEEN 0 AND 2" + }, + "gh_work_unit_summary_lease": { + "name": "gh_work_unit_summary_lease", + "value": "(\"github_work_unit_summary_attempts\".\"lease_token\" IS NULL) = (\"github_work_unit_summary_attempts\".\"lease_until\" IS NULL) AND (\"github_work_unit_summary_attempts\".\"state\" = 'processing') = (\"github_work_unit_summary_attempts\".\"lease_token\" IS NOT NULL) AND (\"github_work_unit_summary_attempts\".\"state\" <> 'processing' OR (\"github_work_unit_summary_attempts\".\"started_requests\" > 0 AND \"github_work_unit_summary_attempts\".\"request_payload\" IS NOT NULL))" + }, + "gh_work_unit_summary_terminal_payload": { + "name": "gh_work_unit_summary_terminal_payload", + "value": "\"github_work_unit_summary_attempts\".\"state\" NOT IN ('accepted', 'terminal') OR \"github_work_unit_summary_attempts\".\"request_payload\" IS NULL" + }, + "gh_work_unit_summary_accepted_output": { + "name": "gh_work_unit_summary_accepted_output", + "value": "(\"github_work_unit_summary_attempts\".\"state\" = 'accepted' AND \"github_work_unit_summary_attempts\".\"outcome\" IS NOT NULL AND \"github_work_unit_summary_attempts\".\"accepted_at\" IS NOT NULL AND \"github_work_unit_summary_attempts\".\"completed_at\" IS NOT NULL) OR (\"github_work_unit_summary_attempts\".\"state\" <> 'accepted' AND \"github_work_unit_summary_attempts\".\"outcome\" IS NULL AND \"github_work_unit_summary_attempts\".\"accepted_at\" IS NULL AND (\"github_work_unit_summary_attempts\".\"state\" <> 'terminal' OR \"github_work_unit_summary_attempts\".\"completed_at\" IS NOT NULL))" + }, + "gh_work_unit_summary_started": { + "name": "gh_work_unit_summary_started", + "value": "(\"github_work_unit_summary_attempts\".\"started_requests\" = 0) = (\"github_work_unit_summary_attempts\".\"last_started_at\" IS NULL) AND (\"github_work_unit_summary_attempts\".\"state\" <> 'pending' OR \"github_work_unit_summary_attempts\".\"request_payload\" IS NOT NULL)" + }, + "gh_work_unit_summary_request_cap": { + "name": "gh_work_unit_summary_request_cap", + "value": "\"github_work_unit_summary_attempts\".\"request_payload\" IS NULL OR octet_length(\"github_work_unit_summary_attempts\".\"request_payload\") <= 393216" + }, + "gh_work_unit_summary_request_times": { + "name": "gh_work_unit_summary_request_times", + "value": "cardinality(\"github_work_unit_summary_attempts\".\"request_started_at\") = \"github_work_unit_summary_attempts\".\"started_requests\"" + }, + "gh_work_unit_summary_metrics": { + "name": "gh_work_unit_summary_metrics", + "value": "(\"github_work_unit_summary_attempts\".\"input_tokens\" IS NULL OR \"github_work_unit_summary_attempts\".\"input_tokens\" >= 0) AND (\"github_work_unit_summary_attempts\".\"output_tokens\" IS NULL OR \"github_work_unit_summary_attempts\".\"output_tokens\" >= 0) AND (\"github_work_unit_summary_attempts\".\"latency_ms\" IS NULL OR \"github_work_unit_summary_attempts\".\"latency_ms\" >= 0)" + } + }, + "isRLSEnabled": true + }, + "public.github_work_unit_summary_daily_usage": { + "name": "github_work_unit_summary_daily_usage", + "schema": "", + "columns": { + "day": { + "name": "day", + "type": "date", + "primaryKey": true, + "notNull": true + }, + "started_requests": { + "name": "started_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.github_work_units": { + "name": "github_work_units", + "schema": "", + "columns": { + "activity_anchor_at": { + "name": "activity_anchor_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "activity_at": { + "name": "activity_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "activity_day": { + "name": "activity_day", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "additions": { + "name": "additions", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "attribution_mode": { + "name": "attribution_mode", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "branch_lineage_id": { + "name": "branch_lineage_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "content_observed_at": { + "name": "content_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "deletions": { + "name": "deletions", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "facts_digest": { + "name": "facts_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "file_count": { + "name": "file_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "first_activity_at": { + "name": "first_activity_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "identity_key": { + "name": "identity_key", + "type": "varchar(180)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "languages": { + "name": "languages", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_activity_at": { + "name": "last_activity_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "member_count": { + "name": "member_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "membership_digest": { + "name": "membership_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "newest_commit_repository_id": { + "name": "newest_commit_repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "newest_commit_sha": { + "name": "newest_commit_sha", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "outcome_digest": { + "name": "outcome_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "pull_request_node_id": { + "name": "pull_request_node_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false + }, + "repository_id": { + "name": "repository_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "summary_evaluation_digest": { + "name": "summary_evaluation_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "summary_evaluated_digest": { + "name": "summary_evaluated_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "summary_input_digest": { + "name": "summary_input_digest", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "gh_work_units_identity_unique": { + "name": "gh_work_units_identity_unique", + "columns": [ + { + "expression": "identity_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gh_work_units_pr_unique": { + "name": "gh_work_units_pr_unique", + "columns": [ + { + "expression": "pull_request_node_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"github_work_units\".\"kind\" = 'pull_request'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "gh_work_units_canonical_day_unique": { + "name": "gh_work_units_canonical_day_unique", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "activity_day", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"github_work_units\".\"kind\" = 'canonical_day'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "gh_work_units_branch_unique": { + "name": "gh_work_units_branch_unique", + "columns": [ + { + "expression": "branch_lineage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"github_work_units\".\"kind\" = 'branch'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "gh_work_units_feed_idx": { + "name": "gh_work_units_feed_idx", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "activity_day", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "activity_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gh_work_units_repository_fk": { + "name": "gh_work_units_repository_fk", + "tableFrom": "github_work_units", + "tableTo": "github_repositories", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gh_work_units_pull_request_fk": { + "name": "gh_work_units_pull_request_fk", + "tableFrom": "github_work_units", + "tableTo": "github_pull_requests", + "columnsFrom": ["pull_request_node_id"], + "columnsTo": ["node_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gh_work_units_newest_commit_fk": { + "name": "gh_work_units_newest_commit_fk", + "tableFrom": "github_work_units", + "tableTo": "github_commits", + "columnsFrom": ["newest_commit_repository_id", "newest_commit_sha"], + "columnsTo": ["repository_id", "sha"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "gh_work_units_kind": { + "name": "gh_work_units_kind", + "value": "\"github_work_units\".\"kind\" IN ('pull_request', 'canonical_day', 'branch')" + }, + "gh_work_units_owner_shape": { + "name": "gh_work_units_owner_shape", + "value": "(\"github_work_units\".\"kind\" = 'pull_request' AND \"github_work_units\".\"pull_request_node_id\" IS NOT NULL AND \"github_work_units\".\"branch_lineage_id\" IS NULL) OR (\"github_work_units\".\"kind\" = 'canonical_day' AND \"github_work_units\".\"pull_request_node_id\" IS NULL AND \"github_work_units\".\"branch_lineage_id\" IS NULL) OR (\"github_work_units\".\"kind\" = 'branch' AND \"github_work_units\".\"pull_request_node_id\" IS NULL AND \"github_work_units\".\"branch_lineage_id\" IS NOT NULL)" + }, + "gh_work_units_identity": { + "name": "gh_work_units_identity", + "value": "(\"github_work_units\".\"kind\" = 'pull_request' AND \"github_work_units\".\"identity_key\" = 'pr:' || \"github_work_units\".\"pull_request_node_id\") OR (\"github_work_units\".\"kind\" = 'canonical_day' AND \"github_work_units\".\"identity_key\" = 'canonical:' || \"github_work_units\".\"repository_id\" || ':' || \"github_work_units\".\"activity_day\"::text) OR (\"github_work_units\".\"kind\" = 'branch' AND \"github_work_units\".\"identity_key\" = 'branch:' || \"github_work_units\".\"branch_lineage_id\"::text)" + }, + "gh_work_units_attribution_mode": { + "name": "gh_work_units_attribution_mode", + "value": "\"github_work_units\".\"attribution_mode\" IN ('tracked_authored_pr', 'foreign_pr_contribution', 'canonical_owned_composite', 'branch_owned_composite')" + }, + "gh_work_units_kind_attribution": { + "name": "gh_work_units_kind_attribution", + "value": "(\"github_work_units\".\"kind\" = 'pull_request' AND \"github_work_units\".\"attribution_mode\" IN ('tracked_authored_pr', 'foreign_pr_contribution')) OR (\"github_work_units\".\"kind\" = 'canonical_day' AND \"github_work_units\".\"attribution_mode\" = 'canonical_owned_composite') OR (\"github_work_units\".\"kind\" = 'branch' AND \"github_work_units\".\"attribution_mode\" = 'branch_owned_composite')" + }, + "gh_work_units_visibility": { + "name": "gh_work_units_visibility", + "value": "\"github_work_units\".\"visibility\" IN ('public', 'private')" + }, + "gh_work_units_nonnegative_facts": { + "name": "gh_work_units_nonnegative_facts", + "value": "\"github_work_units\".\"member_count\" > 0 AND \"github_work_units\".\"file_count\" >= 0 AND \"github_work_units\".\"additions\" >= 0 AND \"github_work_units\".\"deletions\" >= 0 AND \"github_work_units\".\"revision\" > 0" + }, + "gh_work_units_activity_order": { + "name": "gh_work_units_activity_order", + "value": "\"github_work_units\".\"first_activity_at\" <= \"github_work_units\".\"last_activity_at\" AND \"github_work_units\".\"activity_day\" = (\"github_work_units\".\"activity_at\" AT TIME ZONE 'UTC')::date" + }, + "gh_work_units_digest_shapes": { + "name": "gh_work_units_digest_shapes", + "value": "\"github_work_units\".\"facts_digest\" ~ '^[a-f0-9]{64}$' AND \"github_work_units\".\"membership_digest\" ~ '^[a-f0-9]{64}$' AND (\"github_work_units\".\"outcome_digest\" IS NULL OR \"github_work_units\".\"outcome_digest\" ~ '^[a-f0-9]{64}$') AND (\"github_work_units\".\"summary_evaluation_digest\" IS NULL OR \"github_work_units\".\"summary_evaluation_digest\" ~ '^[a-f0-9]{64}$') AND (\"github_work_units\".\"summary_evaluated_digest\" IS NULL OR \"github_work_units\".\"summary_evaluated_digest\" ~ '^[a-f0-9]{64}$') AND (\"github_work_units\".\"summary_input_digest\" IS NULL OR \"github_work_units\".\"summary_input_digest\" ~ '^[a-f0-9]{64}$')" + }, + "gh_work_units_languages_array": { + "name": "gh_work_units_languages_array", + "value": "\"github_work_units\".\"languages\" IS NULL OR jsonb_typeof(\"github_work_units\".\"languages\") = 'array'" + } + }, + "isRLSEnabled": true + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 4d76000..1ec284a 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -155,6 +155,13 @@ "when": 1788813490378, "tag": "0021_windy_midnight", "breakpoints": true + }, + { + "idx": 22, + "version": "7", + "when": 1788907570264, + "tag": "0022_portable_github_accounts", + "breakpoints": true } ] } diff --git a/package.json b/package.json index 05988b0..9e2887e 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "next dev", - "build": "bun scripts/migrate-production-database.ts && next build && bun scripts/configure-supabase-cron.ts --production-build", + "build": "next build", "start": "next start", "test": "bun test ./tests", "typecheck": "tsgo --noEmit", @@ -28,61 +28,61 @@ }, "dependencies": { "@ai-sdk/openai": "4.0.41", - "@base-ui/react": "^1.6.0", - "@mdx-js/loader": "^3.1.1", - "@mdx-js/react": "^3.1.1", + "@base-ui/react": "1.6.0", + "@mdx-js/loader": "3.1.1", + "@mdx-js/react": "3.1.1", "@mermaid-js/layout-elk": "0.2.3", - "@next/mdx": "^16.3.4", - "@remark-embedder/core": "^3.0.3", - "@t3-oss/env-nextjs": "^0.13.11", - "@tailwindcss/typography": "^0.5.20", - "@tanstack/react-query": "^5.102.8", + "@next/mdx": "16.3.4", + "@remark-embedder/core": "3.0.3", + "@t3-oss/env-nextjs": "0.13.11", + "@tailwindcss/typography": "0.5.20", + "@tanstack/react-query": "5.102.8", "ai": "7.0.63", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", "drizzle-orm": "0.45.2", - "feed": "^5.2.1", + "feed": "5.2.1", "gpt-tokenizer": "4.0.0", - "lucide-react": "^1.21.0", - "mermaid": "^11.17.2", - "motion": "^13.2.0", - "next": "^16.3.4", - "next-themes": "^0.4.6", + "lucide-react": "1.21.0", + "mermaid": "11.17.2", + "motion": "13.2.0", + "next": "16.3.4", + "next-themes": "0.4.6", "postgres": "3.4.9", "posthog-js": "1.428.7", - "react": "^19.2.8", - "react-dom": "^19.2.8", - "reading-time": "^1.5.0", - "rehype-autolink-headings": "^7.1.0", - "rehype-pretty-code": "^0.14.5", - "rehype-slug": "^6.0.0", - "remark-gfm": "^4.0.1", + "react": "19.2.8", + "react-dom": "19.2.8", + "reading-time": "1.5.0", + "rehype-autolink-headings": "7.1.0", + "rehype-pretty-code": "0.14.5", + "rehype-slug": "6.0.0", + "remark-gfm": "4.0.1", "server-only": "0.0.1", - "shadcn": "^4.11.0", - "shiki": "^4.4.3", - "sonner": "^2.0.8", - "tailwind-merge": "^3.6.0", - "unist-util-visit": "^5.1.0", - "zod": "^4.4.3" + "shadcn": "4.11.0", + "shiki": "4.4.3", + "sonner": "2.0.8", + "tailwind-merge": "3.6.0", + "unist-util-visit": "5.1.0", + "zod": "4.4.3" }, "devDependencies": { - "@tailwindcss/postcss": "^4.3.1", - "@types/bun": "^1.4.1", - "@types/mdx": "^2.0.14", + "@tailwindcss/postcss": "4.3.1", + "@types/bun": "1.4.1", + "@types/mdx": "2.0.14", "@types/node": "24.13.3", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", - "@typescript/native-preview": "^7.0.0-dev.20260624.1", + "@types/react": "19.2.17", + "@types/react-dom": "19.2.3", + "@typescript/native-preview": "7.0.0-dev.20260624.1", "babel-plugin-react-compiler": "1.0.0", "drizzle-kit": "0.31.10", - "oxfmt": "^0.56.0", - "oxlint": "^1.71.0", - "oxlint-tsgolint": "^0.23.0", - "schema-dts": "^2.0.0", - "tailwindcss": "^4.3.1", - "tw-animate-css": "^1.4.0", - "typescript": "^7.0.1-rc", - "ultracite": "^7.8.3" + "oxfmt": "0.56.0", + "oxlint": "1.71.0", + "oxlint-tsgolint": "0.23.0", + "schema-dts": "2.0.0", + "tailwindcss": "4.3.1", + "tw-animate-css": "1.4.0", + "typescript": "7.0.1-rc", + "ultracite": "7.8.3" }, "engines": { "node": "24.x" diff --git a/scripts/backfill-github-activity.ts b/scripts/backfill-github-activity.ts index c625807..ed64f2a 100644 --- a/scripts/backfill-github-activity.ts +++ b/scripts/backfill-github-activity.ts @@ -1,3 +1,5 @@ +import { tokenForGitHubAccount } from "@/lib/github-accounts"; + import { closeDatabase } from "../src/db/client"; import { env } from "../src/env"; import { runGitHubActivityWorker } from "../src/lib/github-activity-worker"; @@ -39,11 +41,7 @@ interface BackfillArguments { startDate: string; } -interface BackfillEnvironment { - DATABASE_URL?: string; - GITHUB_F0RR0_TOKEN?: string; - GITHUB_YUPPIESTECHDEV_TOKEN?: string; -} +type BackfillEnvironment = Pick; type GitHubBackfillProgressStage = | "identity" @@ -123,7 +121,7 @@ export const backfillArgumentsFrom = ( ); } return { - account: requiredArgument(values, "account"), + account: values.get("account")?.trim() ?? "all", endDate: requiredArgument(values, "end-date"), maximumMinutes, repositoryId: values.get("repository-id")?.trim() ?? "", @@ -138,32 +136,9 @@ export const requireBackfillEnvironment = ( if (environment.DATABASE_URL === undefined) { throw new Error("DATABASE_URL is not configured."); } - if ( - request.accounts.includes("f0rr0") && - environment.GITHUB_F0RR0_TOKEN === undefined - ) { - throw new Error("GITHUB_F0RR0_TOKEN is not configured."); - } - if ( - request.accounts.includes("yuppiestechdev") && - environment.GITHUB_YUPPIESTECHDEV_TOKEN === undefined - ) { - throw new Error("GITHUB_YUPPIESTECHDEV_TOKEN is not configured."); - } -}; - -const tokenFor = ( - account: TrackedGitHubAccount, - environment: BackfillEnvironment -) => { - const token = - account === "f0rr0" - ? environment.GITHUB_F0RR0_TOKEN - : environment.GITHUB_YUPPIESTECHDEV_TOKEN; - if (token === undefined) { - throw new Error(`The GitHub token for ${account} is not configured.`); + for (const account of request.accounts) { + tokenForGitHubAccount(account, environment); } - return token; }; interface GitHubBackfillFactualDrainDependencies { @@ -371,7 +346,7 @@ export const runGitHubBackfillDiscovery = async ( ): Promise => { const identities = await Promise.all( input.request.accounts.map(async (account) => { - const token = tokenFor(account, input.environment); + const token = tokenForGitHubAccount(account, input.environment); input.onProgress?.({ account, phase: "started", diff --git a/scripts/configure-supabase-cron.ts b/scripts/configure-supabase-cron.ts index 7c785e0..7ad034a 100644 --- a/scripts/configure-supabase-cron.ts +++ b/scripts/configure-supabase-cron.ts @@ -1,6 +1,7 @@ import postgres from "postgres"; import { env } from "../src/env"; +import { githubTokensFrom } from "../src/lib/github-accounts"; import { GITHUB_CRON_EXECUTION_DURATION_MS, GITHUB_EVENTS_CRON_JOB, @@ -10,7 +11,7 @@ import { GITHUB_WORKER_HTTP_TIMEOUT_MS, GITHUB_WORKER_CRON_JOB, } from "../src/lib/github-cron-config"; -import { CANONICAL_SITE_URL } from "../src/lib/site-url"; +import { productionSiteOrigin } from "../src/lib/site-url"; import { shouldApplyProductionMigrations } from "./migrate-production-database"; const SECRET_DESCRIPTION = "Vercel cron configuration"; @@ -25,16 +26,20 @@ const LEGACY_SUMMARY_JOB_NAME = "github-summary-worker-every-five-minutes"; const LEGACY_JOB_NAME = "github-sync-every-three-hours"; const LEGACY_REFS_JOB_NAME = "github-refs-every-fifteen-minutes"; const LEGACY_TAG_REFS_JOB_NAME = "github-tag-refs-every-fifteen-minutes"; +// Keep the historical lock key so overlapping old/new deployments still coordinate. const CRON_CONFIGURATION_LOCK_NAME = "f0rr0.dev:supabase-cron"; -interface SupabaseCronEnvironment { - CRON_SECRET?: string; - DATABASE_URL?: string; - DATABASE_URL_UNPOOLED?: string; - VERCEL?: string; - VERCEL_ENV?: string; - VERCEL_PROJECT_PRODUCTION_URL?: string; -} +type SupabaseCronEnvironment = Pick< + typeof env, + | "CRON_SECRET" + | "GITHUB_TOKENS" + | "OPENAI_API_KEY" + | "DATABASE_URL" + | "DATABASE_URL_UNPOOLED" + | "VERCEL" + | "VERCEL_ENV" + | "VERCEL_PROJECT_PRODUCTION_URL" +>; const requiredEnvironmentValue = ( name: string, @@ -78,14 +83,8 @@ export const supabaseCronUrlsFrom = (configuredSiteUrl: string) => { }; }; -export const supabaseCronSiteUrlFrom = ( - environment: SupabaseCronEnvironment -) => { - const productionHostname = environment.VERCEL_PROJECT_PRODUCTION_URL?.trim(); - return productionHostname === undefined || productionHostname.length === 0 - ? CANONICAL_SITE_URL - : `https://${productionHostname}`; -}; +export const supabaseCronSiteUrlFrom = (environment: SupabaseCronEnvironment) => + productionSiteOrigin(environment.VERCEL_PROJECT_PRODUCTION_URL); const upsertVaultSecret = async ( sql: postgres.TransactionSql, @@ -142,6 +141,53 @@ const cronHttpPostCommand = ( ) as request_id `; +export const supabaseCronJobsFrom = ( + environment: SupabaseCronEnvironment, + codexEnabled: boolean +) => { + const githubEnabled = + Object.keys(githubTokensFrom(environment.GITHUB_TOKENS)).length > 0; + const urls = supabaseCronUrlsFrom(supabaseCronSiteUrlFrom(environment)); + return [ + { + ...GITHUB_EVENTS_CRON_JOB, + urlName: URL_NAME, + url: urls.events, + timeout: GITHUB_CRON_EXECUTION_DURATION_MS, + enabled: githubEnabled, + }, + { + ...GITHUB_HEAD_REFS_CRON_JOB, + urlName: HEAD_REFS_URL_NAME, + url: urls.headRefs, + timeout: GITHUB_CRON_EXECUTION_DURATION_MS, + enabled: githubEnabled, + }, + { + ...GITHUB_WORKER_CRON_JOB, + urlName: WORKER_URL_NAME, + url: urls.worker, + timeout: GITHUB_WORKER_HTTP_TIMEOUT_MS, + enabled: true, + }, + { + ...GITHUB_SUMMARY_CRON_JOB, + urlName: SUMMARY_URL_NAME, + url: urls.summary, + timeout: GITHUB_WORKER_HTTP_TIMEOUT_MS, + enabled: Boolean(environment.OPENAI_API_KEY?.trim()), + }, + { + name: CODEX_STATS_JOB_NAME, + schedule: "7,22,37,52 * * * *", + urlName: CODEX_STATS_URL_NAME, + url: urls.codexStats, + timeout: GITHUB_WORKER_HTTP_TIMEOUT_MS, + enabled: codexEnabled, + }, + ]; +}; + export const configureSupabaseCron = async ( environment: SupabaseCronEnvironment = env ) => { @@ -153,7 +199,8 @@ export const configureSupabaseCron = async ( if (cronSecret.length < 32) { throw new Error("CRON_SECRET must contain at least 32 characters."); } - const urls = supabaseCronUrlsFrom(supabaseCronSiteUrlFrom(environment)); + // Validate configuration before opening a connection. + supabaseCronJobsFrom(environment, false); const sql = postgres(databaseUrl, { connect_timeout: 10, idle_timeout: 20, @@ -174,26 +221,14 @@ export const configureSupabaseCron = async ( await transaction`create extension if not exists pg_net with schema extensions`; await transaction`create extension if not exists supabase_vault with schema vault`; - await upsertVaultSecret(transaction, { - name: URL_NAME, - value: urls.events, - }); - await upsertVaultSecret(transaction, { - name: HEAD_REFS_URL_NAME, - value: urls.headRefs, - }); - await upsertVaultSecret(transaction, { - name: SUMMARY_URL_NAME, - value: urls.summary, - }); - await upsertVaultSecret(transaction, { - name: WORKER_URL_NAME, - value: urls.worker, - }); - await upsertVaultSecret(transaction, { - name: CODEX_STATS_URL_NAME, - value: urls.codexStats, - }); + const [codex] = await transaction<{ enabled: boolean }[]>` + select exists(select 1 from codex_accounts where enabled) as enabled + `; + const configuredJobs = supabaseCronJobsFrom( + environment, + + codex?.enabled + ); await upsertVaultSecret(transaction, { name: SECRET_NAME, value: cronSecret, @@ -202,68 +237,34 @@ export const configureSupabaseCron = async ( await transaction` select cron.unschedule(jobid) from cron.job - where jobname in ( - ${LEGACY_JOB_NAME}, - ${LEGACY_REFS_JOB_NAME}, - ${LEGACY_TAG_REFS_JOB_NAME}, - ${LEGACY_SUMMARY_JOB_NAME}, - ${GITHUB_EVENTS_CRON_JOB.name}, - ${GITHUB_HEAD_REFS_CRON_JOB.name}, - ${GITHUB_SUMMARY_CRON_JOB.name}, - ${GITHUB_WORKER_CRON_JOB.name}, - ${CODEX_STATS_JOB_NAME} - ) + where jobname = any(${[ + LEGACY_JOB_NAME, + LEGACY_REFS_JOB_NAME, + LEGACY_TAG_REFS_JOB_NAME, + LEGACY_SUMMARY_JOB_NAME, + ...configuredJobs.map(({ name }) => name), + ]}::text[]) `; - const [eventsJob] = await transaction<{ jobId: number }[]>` - select cron.schedule( - ${GITHUB_EVENTS_CRON_JOB.name}, - ${GITHUB_EVENTS_CRON_JOB.schedule}, - ${cronHttpPostCommand(URL_NAME)} - ) as "jobId" - `; - const [headRefsJob] = await transaction<{ jobId: number }[]>` - select cron.schedule( - ${GITHUB_HEAD_REFS_CRON_JOB.name}, - ${GITHUB_HEAD_REFS_CRON_JOB.schedule}, - ${cronHttpPostCommand(HEAD_REFS_URL_NAME)} - ) as "jobId" - `; - const [workerJob] = await transaction<{ jobId: number }[]>` - select cron.schedule( - ${GITHUB_WORKER_CRON_JOB.name}, - ${GITHUB_WORKER_CRON_JOB.schedule}, - ${cronHttpPostCommand(WORKER_URL_NAME, GITHUB_WORKER_HTTP_TIMEOUT_MS)} - ) as "jobId" - `; - const [summaryJob] = await transaction<{ jobId: number }[]>` - select cron.schedule( - ${GITHUB_SUMMARY_CRON_JOB.name}, - ${GITHUB_SUMMARY_CRON_JOB.schedule}, - ${cronHttpPostCommand(SUMMARY_URL_NAME, GITHUB_WORKER_HTTP_TIMEOUT_MS)} - ) as "jobId" - `; - const [codexStatsJob] = await transaction<{ jobId: number }[]>` - select cron.schedule( - ${CODEX_STATS_JOB_NAME}, - '7,22,37,52 * * * *', - ${cronHttpPostCommand(CODEX_STATS_URL_NAME, GITHUB_WORKER_HTTP_TIMEOUT_MS)} - ) as "jobId" - `; - if ( - codexStatsJob === undefined || - eventsJob === undefined || - headRefsJob === undefined || - summaryJob === undefined || - workerJob === undefined - ) { - throw new Error("Supabase did not return every scheduled cron job."); + const jobs: { name: string; jobId: number }[] = []; + for (const job of configuredJobs.filter((value) => value.enabled)) { + await upsertVaultSecret(transaction, { + name: job.urlName, + value: job.url, + }); + const [scheduled] = await transaction<{ jobId: number }[]>` + select cron.schedule(${job.name}, ${job.schedule}, ${cronHttpPostCommand(job.urlName, job.timeout)}) as "jobId" + `; + if (scheduled === undefined) { + throw new Error("Supabase did not return the scheduled cron job."); + } + jobs.push({ name: job.name, jobId: scheduled.jobId }); } - return { codexStatsJob, eventsJob, headRefsJob, summaryJob, workerJob }; + return jobs; }); process.stdout.write( - `Configured Supabase cron jobs ${String(jobs.eventsJob.jobId)}, ${String(jobs.headRefsJob.jobId)}, ${String(jobs.workerJob.jobId)}, ${String(jobs.summaryJob.jobId)}, and ${String(jobs.codexStatsJob.jobId)}.\n` + `Configured ${String(jobs.length)} Supabase cron jobs.\n` ); return jobs; } finally { diff --git a/scripts/migrate-production-database.ts b/scripts/migrate-production-database.ts index 2c86631..b15d4c2 100644 --- a/scripts/migrate-production-database.ts +++ b/scripts/migrate-production-database.ts @@ -5,14 +5,13 @@ import postgres from "postgres"; import { env } from "../src/env"; +// Keep the historical lock key so overlapping old/new deployments still coordinate. const MIGRATION_LOCK_NAME = "f0rr0.dev:drizzle-migrations"; -interface Environment { - DATABASE_URL?: string; - DATABASE_URL_UNPOOLED?: string; - VERCEL?: string; - VERCEL_ENV?: string; -} +type Environment = Pick< + typeof env, + "DATABASE_URL" | "DATABASE_URL_UNPOOLED" | "VERCEL" | "VERCEL_ENV" +>; export class ProductionMigrationConfigurationError extends Error { constructor(message: string) { diff --git a/src/app/(blog)/writing/page.tsx b/src/app/(blog)/writing/page.tsx index 1002d5f..961b1bc 100644 --- a/src/app/(blog)/writing/page.tsx +++ b/src/app/(blog)/writing/page.tsx @@ -22,7 +22,7 @@ export const metadata: Metadata = { images: [siteConfig.author.image], locale: siteConfig.locale, siteName: siteConfig.name, - title: "Sid Jain Writing", + title: `${siteConfig.name} Writing`, type: "website", url: publicUrl("/writing"), }, @@ -31,7 +31,7 @@ export const metadata: Metadata = { card: "summary", description, images: [siteConfig.author.image], - title: "Sid Jain Writing", + title: `${siteConfig.name} Writing`, }, }; diff --git a/src/app/(portfolio)/journey/page.tsx b/src/app/(portfolio)/journey/page.tsx index f74e99b..d00afe4 100644 --- a/src/app/(portfolio)/journey/page.tsx +++ b/src/app/(portfolio)/journey/page.tsx @@ -6,7 +6,7 @@ import { JsonLd } from "@/components/json-ld"; import { SiteMain } from "@/components/site-page"; import { SiteShell } from "@/components/site-shell"; import { resumeData } from "@/content/resume"; -import { publicUrl, siteConfig } from "@/lib/site"; +import { publicUrl, resumePdfUrl, siteConfig } from "@/lib/site"; import { buildProfilePageJsonLd } from "@/lib/structured-data"; const resumeDescription = siteConfig.description; @@ -21,7 +21,7 @@ export const metadata: Metadata = { images: [resumeData.person.image], locale: siteConfig.locale, siteName: siteConfig.name, - title: "Sid Jain Journey", + title: `${siteConfig.name} Journey`, type: "profile", url: publicUrl("/journey"), }, @@ -30,7 +30,7 @@ export const metadata: Metadata = { card: "summary", description: resumeDescription, images: [resumeData.person.image], - title: "Sid Jain Journey", + title: `${siteConfig.name} Journey`, }, }; @@ -52,7 +52,7 @@ export default function JourneyPage() { diff --git a/src/app/(portfolio)/work/page.tsx b/src/app/(portfolio)/work/page.tsx index b0f9c60..78b5d21 100644 --- a/src/app/(portfolio)/work/page.tsx +++ b/src/app/(portfolio)/work/page.tsx @@ -6,8 +6,7 @@ import { SiteShell } from "@/components/site-shell"; import { getInitialGitHubActivity } from "@/lib/github-activity-feed"; import { publicUrl, siteConfig } from "@/lib/site"; -const description = - "A day-by-day record of what Sid Jain is building, fixing, and shipping."; +const description = `A day-by-day record of what ${siteConfig.name} is building, fixing, and shipping.`; export const metadata: Metadata = { alternates: { canonical: "/work" }, @@ -17,7 +16,7 @@ export const metadata: Metadata = { images: [siteConfig.author.image], locale: siteConfig.locale, siteName: siteConfig.name, - title: "Sid Jain Work", + title: `${siteConfig.name} Work`, type: "website", url: publicUrl("/work"), }, @@ -26,7 +25,7 @@ export const metadata: Metadata = { card: "summary", description, images: [siteConfig.author.image], - title: "Sid Jain Work", + title: `${siteConfig.name} Work`, }, }; diff --git a/src/app/opengraph-image.tsx b/src/app/opengraph-image.tsx index 898ffa9..56c1fe2 100644 --- a/src/app/opengraph-image.tsx +++ b/src/app/opengraph-image.tsx @@ -1,6 +1,7 @@ import { ImageResponse } from "next/og"; import { resumeData } from "@/content/resume"; +import { siteConfig } from "@/lib/site"; export const alt = `${resumeData.person.name} — ${resumeData.person.role}`; export const size = { @@ -36,7 +37,7 @@ export default function Image() { > {resumeData.person.name} - f0rr0.dev + {new URL(siteConfig.url).hostname}
diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 7f0b517..f433859 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -2,7 +2,7 @@ import type { MetadataRoute } from "next"; import { resumeData } from "@/content/resume"; import { getBlogPosts } from "@/lib/blog-utils"; -import { publicUrl } from "@/lib/site"; +import { publicUrl, resumePdfUrl } from "@/lib/site"; const newestDate = (dates: Date[]) => dates.toSorted((a, b) => b.getTime() - a.getTime()).at(0); @@ -30,7 +30,7 @@ export default async function sitemap(): Promise { }, { lastModified: resumeUpdatedAt, - url: publicUrl("/resume/sid-jain-resume.pdf"), + url: publicUrl(resumePdfUrl), }, { lastModified: latestPostDate, diff --git a/src/components/github-activity-days.tsx b/src/components/github-activity-days.tsx index 8304439..fd6cc3e 100644 --- a/src/components/github-activity-days.tsx +++ b/src/components/github-activity-days.tsx @@ -299,7 +299,9 @@ function GitHubActivityDay({ > {formatDate(day.day, "weekday")} - IST + + {WORK_LOG_TIME_ZONE} +
; export type BlogPost = BlogPostEntry & { metadata: BlogPostMetadata; + images?: Record; date: Date; updatedAt?: Date; readingTime: string; @@ -228,7 +229,10 @@ export const getBlogPosts = cache(async (): Promise => { const posts = await Promise.all( entries.map(async ({ slug, importPath }) => { - const mod = await importBlogPostModule<{ metadata: unknown }>(importPath); + const mod = await importBlogPostModule<{ + metadata: unknown; + blogImages?: Record; + }>(importPath); const metadata = parseBlogPostMetadata(mod.metadata); const stats = await getPostStats(importPath); const date = toDate(metadata.date, slug); @@ -241,6 +245,7 @@ export const getBlogPosts = cache(async (): Promise => { date, importPath, metadata, + images: mod.blogImages ?? {}, readingTime: stats.readingTime, slug, updatedAt, diff --git a/src/lib/date.ts b/src/lib/date.ts index bd9caec..eacd735 100644 --- a/src/lib/date.ts +++ b/src/lib/date.ts @@ -1,4 +1,6 @@ -export const WORK_LOG_TIME_ZONE = "Asia/Kolkata"; +import { sitePreferences } from "@/content/site"; + +export const WORK_LOG_TIME_ZONE = sitePreferences.workLogTimeZone; export const dateFormats = { date: { dateStyle: "medium" }, @@ -31,7 +33,10 @@ export const formatDate = ( const key = `${format}:${timeZone}`; const formatter = dateFormatters.get(key) ?? - new Intl.DateTimeFormat("en-US", { ...dateFormats[format], timeZone }); + new Intl.DateTimeFormat(sitePreferences.language, { + ...dateFormats[format], + timeZone, + }); dateFormatters.set(key, formatter); return formatter.format(new Date(value)); }; diff --git a/src/lib/github-accounts.ts b/src/lib/github-accounts.ts new file mode 100644 index 0000000..b4246ae --- /dev/null +++ b/src/lib/github-accounts.ts @@ -0,0 +1,60 @@ +import { z } from "zod"; + +import { env } from "@/env"; +import { TRACKED_GITHUB_ACCOUNTS } from "@/lib/github-commits-core"; + +const tokensSchema = z.record(z.string(), z.string().trim().min(1)); + +export const githubTokensFrom = ( + value?: string, + accounts: readonly string[] = TRACKED_GITHUB_ACCOUNTS +): Record => { + if (value === undefined || value.trim() === "") { + return {}; + } + try { + const tokens: Record = {}; + for (const [key, token] of Object.entries( + tokensSchema.parse(JSON.parse(value)) + )) { + const login = key.toLowerCase(); + if (!accounts.includes(login) || Object.hasOwn(tokens, login)) { + throw new TypeError("Unknown or duplicate account."); + } + tokens[login] = token; + } + return tokens; + } catch { + // Parser errors can include credentials; never expose their original details. + throw new TypeError( + "GITHUB_TOKENS must be a JSON object mapping configured GitHub logins to nonempty tokens, without duplicate logins." + ); + } +}; + +export const tokenForGitHubAccount = ( + login: string, + environment: Pick = env +) => { + const tokens = githubTokensFrom(environment.GITHUB_TOKENS); + const token = Object.hasOwn(tokens, login) ? tokens[login] : undefined; + if (token === undefined) { + throw new Error(`No GitHub token is configured for ${login}.`); + } + return token; +}; + +export const tokensForGitHubAccount = (login?: string) => { + const tokens = githubTokensFrom(env.GITHUB_TOKENS); + return [ + ...new Set( + [ + ...(login === undefined ? [] : [tokens[login]]), + ...Object.values(tokens), + env.GITHUB_TOKEN ?? env.GH_TOKEN, + ].flatMap((value) => + value === undefined || value.trim() === "" ? [] : [value.trim()] + ) + ), + ]; +}; diff --git a/src/lib/github-activity-processor.ts b/src/lib/github-activity-processor.ts index 0956d3a..f4d5ead 100644 --- a/src/lib/github-activity-processor.ts +++ b/src/lib/github-activity-processor.ts @@ -1,4 +1,4 @@ -import { env } from "@/env"; +import { tokensForGitHubAccount } from "@/lib/github-accounts"; import { fetchGitHub, GitHubResponseError, @@ -132,6 +132,7 @@ export class ActivityProcessingError extends Error { } export type GitHubGraphQlResponseErrorKind = + | "access_denied" | "invalid_response" | "partial_response" | "rate_limited" @@ -251,10 +252,7 @@ const graphQlErrorIsRateLimited = ( const graphQlErrorIsPermanent = (errors: readonly JsonObject[]) => { const permanentSignals = [ "BAD_USER_INPUT", - "FORBIDDEN", "GRAPHQL_VALIDATION_FAILED", - "NOT_FOUND", - "UNAUTHORIZED", "UNDEFINED_FIELD", ]; const signals = graphQlErrorSignals(errors, false); @@ -368,30 +366,18 @@ const repositoryReferenceFrom = (row: { return repository; }; -const tokenCandidatesFor = (account: TrackedGitHubAccount) => { - const accountToken = - account === "f0rr0" - ? env.GITHUB_F0RR0_TOKEN - : env.GITHUB_YUPPIESTECHDEV_TOKEN; - const otherToken = - account === "f0rr0" - ? env.GITHUB_YUPPIESTECHDEV_TOKEN - : env.GITHUB_F0RR0_TOKEN; - return [ - ...new Set( - [accountToken, otherToken, env.GITHUB_TOKEN].flatMap((value) => { - const token = value?.trim(); - return token === undefined || token.length === 0 ? [] : [token]; - }) - ), - ]; -}; +const githubSourceAccessIsDenied = (error: unknown) => + (error instanceof GitHubResponseError && + [401, 403, 404].includes(error.status) && + !error.retryable) || + (error instanceof GitHubGraphQlResponseError && + error.kind === "access_denied"); const withGitHubTokenCandidate = async ( account: TrackedGitHubAccount, fetcher: (token: string) => Promise ) => { - const tokens = tokenCandidatesFor(account); + const tokens = tokensForGitHubAccount(account); if (tokens.length === 0) { throw new ActivityProcessingError( "source_auth_missing", @@ -404,10 +390,7 @@ const withGitHubTokenCandidate = async ( return await fetcher(token); } catch (error) { lastError = error; - if ( - !(error instanceof GitHubResponseError) || - ![401, 403, 404].includes(error.status) - ) { + if (!githubSourceAccessIsDenied(error)) { throw error; } } @@ -487,6 +470,17 @@ export const githubGraphQlPayloadFrom = async (response: Response) => { retryable: true, }); } + if ( + graphQlErrorSignals(errors, false).some((signal) => + ["FORBIDDEN", "NOT_FOUND", "UNAUTHORIZED"].some((access) => + signal.includes(access) + ) + ) + ) { + throw new GitHubGraphQlResponseError("access_denied", { + retryable: true, + }); + } const retryable = !graphQlErrorIsPermanent(errors); const hasPartialData = Object.hasOwn(value, "data") && value.data !== null; throw new GitHubGraphQlResponseError( @@ -1647,7 +1641,7 @@ export const fetchGitHubAssociatedPullRequests = async ( row: GitHubActivityCommitReference, options: GitHubProviderRequestOptions = {} ) => { - const tokens = tokenCandidatesFor(row.author); + const tokens = tokensForGitHubAccount(row.author); if (tokens.length === 0) { throw new ActivityProcessingError( "source_auth_missing", @@ -1670,10 +1664,7 @@ export const fetchGitHubAssociatedPullRequests = async ( } } catch (error) { lastError = error; - const hiddenFromToken = - error instanceof GitHubResponseError && - [401, 403, 404].includes(error.status) && - !error.retryable; + const hiddenFromToken = githubSourceAccessIsDenied(error); if (!hiddenFromToken) { throw error; } diff --git a/src/lib/github-activity-store.ts b/src/lib/github-activity-store.ts index cf01611..fb999bc 100644 --- a/src/lib/github-activity-store.ts +++ b/src/lib/github-activity-store.ts @@ -23,6 +23,7 @@ import type { PublicGitHubActivityRepository, PublicGitHubWorkUnitKind, } from "@/lib/github-activity-types"; +import { TRACKED_GITHUB_USER_IDS } from "@/lib/github-commits-core"; import { decodeGitHubWorkUnitSummary, GITHUB_WORK_UNIT_SUMMARY_RECIPE, @@ -264,6 +265,10 @@ const readAvailableDays = async ( .where( and( beforeIssue, + inArray( + githubIssues.authorUserId, + Object.values(TRACKED_GITHUB_USER_IDS) + ), inArray(githubRepositories.visibility, [ "public", "private", @@ -371,6 +376,10 @@ const readPublicRows = async ( .where( and( inArray(issueDay, selectedDays), + inArray( + githubIssues.authorUserId, + Object.values(TRACKED_GITHUB_USER_IDS) + ), inArray(githubRepositories.visibility, [ "public", "private", diff --git a/src/lib/github-activity-worker-store.ts b/src/lib/github-activity-worker-store.ts index a6a4af2..eed0955 100644 --- a/src/lib/github-activity-worker-store.ts +++ b/src/lib/github-activity-worker-store.ts @@ -1133,28 +1133,21 @@ export const completeGitHubPullRequestDiscovery = async ( now ); } - await transaction - .delete(githubCommitPullRequestAssociations) - .where( - and( - eq( - githubCommitPullRequestAssociations.commitRepositoryId, - commit.repositoryId - ), - eq(githubCommitPullRequestAssociations.commitSha, commit.sha) - ) - ); + // Discovery is limited by credential visibility. Absence does not revoke prior evidence. const associatedPullRequestNodeIds = [ ...new Set(pullRequests.map(({ nodeId }) => nodeId)), ]; if (associatedPullRequestNodeIds.length > 0) { - await transaction.insert(githubCommitPullRequestAssociations).values( - associatedPullRequestNodeIds.map((pullRequestNodeId) => ({ - commitRepositoryId: commit.repositoryId, - commitSha: commit.sha, - pullRequestNodeId, - })) - ); + await transaction + .insert(githubCommitPullRequestAssociations) + .values( + associatedPullRequestNodeIds.map((pullRequestNodeId) => ({ + commitRepositoryId: commit.repositoryId, + commitSha: commit.sha, + pullRequestNodeId, + })) + ) + .onConflictDoNothing(); } const [completed] = await transaction .update(githubCommits) @@ -1270,7 +1263,7 @@ export const claimDueGitHubPullRequests = async ( .returning({ nodeId: githubPullRequests.nodeId }); if (updated !== undefined) { claimed.push({ - account: candidate.account as TrackedGitHubAccount, + account: candidate.account, attemptCount: candidate.attemptCount + 1, createdAt: candidate.createdAt, lastReconciledAt: candidate.lastReconciledAt, diff --git a/src/lib/github-activity-worker.ts b/src/lib/github-activity-worker.ts index c8acab0..cdc8300 100644 --- a/src/lib/github-activity-worker.ts +++ b/src/lib/github-activity-worker.ts @@ -1,3 +1,4 @@ +import { tokensForGitHubAccount } from "@/lib/github-accounts"; import { ActivityProcessingError, fetchGitHubActivityCommitSource, @@ -78,10 +79,9 @@ import { reconcileGitHubWorkUnitSummaryStatus } from "@/lib/github-work-unit-sum const DEFAULT_WORKER_MAXIMUM_DURATION_MS = 90_000; const MAXIMUM_PUBLICATION_RESERVE_MS = 30_000; const MINIMUM_FACTUAL_PROCESSING_MS = 8000; -const TERMINAL_GITHUB_STATUSES = new Set([403, 404, 410, 422]); +const TERMINAL_GITHUB_STATUSES = new Set([410, 422]); const TERMINAL_ACTIVITY_PROCESSING_CODES = new Set([ "membership_incomplete", - "source_auth_missing", "source_incomplete", "source_invalid", "source_unavailable", @@ -161,7 +161,10 @@ const processRefRepairs = async ( const claimed = await claimGitHubRefRepairs({ limit, now: new Date() }); result.claimed = claimed.length; for (const repair of claimed) { - if (context.deadlineReached()) { + if ( + context.deadlineReached() || + (repair.active && !context.hasCredentials) + ) { await releaseGitHubRefRepair(repair); result.deferred += 1; continue; @@ -240,6 +243,7 @@ const observedSinceLastReconciliation = (due: DueGitHubPullRequest) => due.versionObservedAt > due.lastReconciledAt); interface WorkerContext { + hasCredentials: boolean; activeAccounts: readonly TrackedGitHubAccount[]; deadlineAt: number; deadlineReached: () => boolean; @@ -672,6 +676,7 @@ export const runGitHubActivityWorker = async ( const refs = emptyStageResult(); const activeAccounts = await activeTrackedAccounts(requestedAccounts); const context: WorkerContext = { + hasCredentials: tokensForGitHubAccount().length > 0, activeAccounts, deadlineAt: startedAt + processingDurationMs, deadlineReached, @@ -679,22 +684,26 @@ export const runGitHubActivityWorker = async ( }; const overallDeadlineReached = () => workerDeadlineReached(startedAt, maximumDurationMs); - await processObservations(context, observationLimit, observations); - await processPullRequestSignals( - context, - pullRequestSignalLimit, - pullRequestSignals - ); + if (context.hasCredentials) { + await processObservations(context, observationLimit, observations); + await processPullRequestSignals( + context, + pullRequestSignalLimit, + pullRequestSignals + ); + } if (options.includeRefs !== false) { await processRefRepairs(context, refLimit, refs); } - await processCommits(context, commitLimit, commits); - await processPullRequestDiscovery( - context, - pullRequestDiscoveryLimit, - pullRequestDiscovery - ); - await processPullRequests(context, pullRequestLimit, pullRequests); + if (context.hasCredentials) { + await processCommits(context, commitLimit, commits); + await processPullRequestDiscovery( + context, + pullRequestDiscoveryLimit, + pullRequestDiscovery + ); + await processPullRequests(context, pullRequestLimit, pullRequests); + } const projection = options.includeProjection !== false && (await ensureGitHubWorkUnitProjectionRequest()) !== null diff --git a/src/lib/github-api.ts b/src/lib/github-api.ts index 4f2b536..b1232a8 100644 --- a/src/lib/github-api.ts +++ b/src/lib/github-api.ts @@ -148,7 +148,7 @@ const isRateLimited = async (response: Response) => (await hasSecondaryRateLimitEvidence(response)))); const readDefaultGitHubToken = () => { - const token = env.GITHUB_TOKEN?.trim() ?? env.GITHUB_F0RR0_TOKEN?.trim(); + const token = env.GITHUB_TOKEN?.trim() ?? env.GH_TOKEN?.trim(); return token === undefined || token.length === 0 ? null : token; }; @@ -286,7 +286,7 @@ export const fetchGitHub = async ( ? {} : { "If-None-Match": options.ifNoneMatch }), ...(method === "POST" ? { "Content-Type": "application/json" } : {}), - "User-Agent": "f0rr0.dev", + "User-Agent": "Figment", "X-GitHub-Api-Version": GITHUB_API_VERSION, }, method, diff --git a/src/lib/github-commits-core.ts b/src/lib/github-commits-core.ts index e390ad3..b55d61d 100644 --- a/src/lib/github-commits-core.ts +++ b/src/lib/github-commits-core.ts @@ -1,3 +1,9 @@ +import { githubAccounts } from "@/content/site"; + +export const TRACKED_GITHUB_ACCOUNTS = githubAccounts.map(({ login }) => login); +export const TRACKED_GITHUB_USER_IDS: Readonly> = + Object.fromEntries(githubAccounts.map(({ login, id }) => [login, id])); + const COMMIT_SHA = /^[a-f0-9]{40}$/; const EVENT_ID = /^\d{1,64}$/; const GITHUB_DELIVERY_ID = @@ -8,14 +14,7 @@ const REPOSITORY_FULL_NAME = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?\/[A-Za-z0-9._-]{1,100}$/; const ZERO_SHA = "0".repeat(40); -export const TRACKED_GITHUB_ACCOUNTS = ["f0rr0", "yuppiestechdev"] as const; - -export type TrackedGitHubAccount = (typeof TRACKED_GITHUB_ACCOUNTS)[number]; - -export const TRACKED_GITHUB_USER_IDS = { - f0rr0: "8574219", - yuppiestechdev: "99666891", -} as const satisfies Record; +export type TrackedGitHubAccount = string; type JsonObject = Record; diff --git a/src/lib/github-commits.ts b/src/lib/github-commits.ts index b2fdd0e..7b7a3d3 100644 --- a/src/lib/github-commits.ts +++ b/src/lib/github-commits.ts @@ -1,5 +1,6 @@ import { DatabaseConfigurationError, isDatabaseConfigured } from "@/db/client"; import { env } from "@/env"; +import { githubTokensFrom, tokenForGitHubAccount } from "@/lib/github-accounts"; import { fetchGitHub, githubApiUrl, @@ -10,7 +11,6 @@ import { import { authenticatedGitHubAccountFrom, githubEventFrom, - TRACKED_GITHUB_ACCOUNTS, } from "@/lib/github-commits-core"; import type { GitHubEvent, @@ -26,10 +26,6 @@ import { import type { GitHubRepositoryRefKind } from "@/lib/github-commits-store"; import { reconcileGitHubRepositoryRefBatch } from "@/lib/github-ref-reconciliation-batch"; -const ACCOUNT_TOKEN_VARIABLES = { - f0rr0: "GITHUB_F0RR0_TOKEN", - yuppiestechdev: "GITHUB_YUPPIESTECHDEV_TOKEN", -} as const satisfies Record; const CHECKPOINT_ATTEMPTS = 3; const EVENT_PAGES = 3; const GITHUB_PAGE_SIZE = 100; @@ -93,22 +89,6 @@ export interface GitHubRefReconciliationResult { repositories: number; } -class GitHubSyncConfigurationError extends Error { - constructor(variable: string) { - super(`${variable} is not configured.`); - this.name = "GitHubSyncConfigurationError"; - } -} - -const tokenFor = (account: TrackedGitHubAccount) => { - const variable = ACCOUNT_TOKEN_VARIABLES[account]; - const token = env[variable]?.trim(); - if (token === undefined || token.length === 0) { - throw new GitHubSyncConfigurationError(variable); - } - return token; -}; - interface GitHubCronRequestOptions { deadlineAt?: number; } @@ -119,7 +99,8 @@ const settleTrackedGitHubAccounts = async ( failedAccounts: readonly FailedGitHubAccount[]; results: readonly Result[]; }> => { - const settled = await Promise.allSettled(TRACKED_GITHUB_ACCOUNTS.map(action)); + const accounts = Object.keys(githubTokensFrom(env.GITHUB_TOKENS)); + const settled = await Promise.allSettled(accounts.map(action)); const results: Result[] = []; const failedAccounts: FailedGitHubAccount[] = []; @@ -128,7 +109,7 @@ const settleTrackedGitHubAccounts = async ( results.push(outcome.value); continue; } - const account = TRACKED_GITHUB_ACCOUNTS[index]; + const account = accounts[index]; if (account === undefined) { throw new Error("A GitHub account result has no tracked account."); } @@ -162,10 +143,15 @@ export const assertGitHubTokenIdentity = async ( options: GitHubCronRequestOptions = {} ) => { const { payload } = await fetchJson(githubApiUrl("/user"), token, options); - if (authenticatedGitHubAccountFrom(payload) !== account) { - throw new Error( - `${ACCOUNT_TOKEN_VARIABLES[account]} is not authenticated as ${account}.` - ); + if ( + authenticatedGitHubAccountFrom(payload) !== account || + typeof payload !== "object" || + payload === null || + !("login" in payload) || + typeof payload.login !== "string" || + payload.login.toLowerCase() !== account + ) { + throw new Error(`The GitHub token is not authenticated as ${account}.`); } }; @@ -314,7 +300,7 @@ export const syncGitHubAccount = async ( }; } if (token === null) { - token = tokenFor(account); + token = tokenForGitHubAccount(account); await assertGitHubTokenIdentity(account, token, options); } const collected = await collectGitHubEvents( @@ -417,7 +403,7 @@ export const reconcileGitHubAccountRefs = async ( repositories: 0, }; } - const token = tokenFor(account); + const token = tokenForGitHubAccount(account); await assertGitHubTokenIdentity(account, token, options); return { account, diff --git a/src/lib/github-profile.ts b/src/lib/github-profile.ts index 49ce7e5..2bacc7b 100644 --- a/src/lib/github-profile.ts +++ b/src/lib/github-profile.ts @@ -1,6 +1,7 @@ -import "server-only"; import { unstable_cache } from "next/cache"; +import "server-only"; +import { primaryGitHubProfile } from "@/content/resume"; import { fetchGitHub, githubApiUrl } from "@/lib/github-api"; import { createUnavailableGitHubProfile, @@ -10,7 +11,7 @@ import type { GitHubProfile } from "@/lib/github-profile-core"; export type { GitHubProfile, GitHubProject } from "@/lib/github-profile-core"; -const GITHUB_LOGIN = "f0rr0"; +const GITHUB_LOGIN = primaryGitHubProfile.username; const GITHUB_CACHE_SECONDS = 60 * 60 * 12; const fetchGitHubProfile = async (): Promise => { diff --git a/src/lib/github-work-unit-projection-state.ts b/src/lib/github-work-unit-projection-state.ts index 54c2df9..37d1dda 100644 --- a/src/lib/github-work-unit-projection-state.ts +++ b/src/lib/github-work-unit-projection-state.ts @@ -4,6 +4,7 @@ import { and, eq, sql } from "drizzle-orm"; import { getDatabase } from "@/db/client"; import { githubPublicFeedHead } from "@/db/schema"; +import { TRACKED_GITHUB_USER_IDS } from "@/lib/github-commits-core"; import { GITHUB_WORK_UNIT_SUMMARY_POLICY_DIGEST } from "@/lib/github-work-unit-summary"; type Database = ReturnType; @@ -18,6 +19,7 @@ const PROJECTION_POLICY = const PIPELINE_POLICY_DIGEST = createHash("sha256") .update( JSON.stringify({ + authors: Object.values(TRACKED_GITHUB_USER_IDS).toSorted(), projection: PROJECTION_POLICY, summary: GITHUB_WORK_UNIT_SUMMARY_POLICY_DIGEST, }) @@ -69,11 +71,18 @@ export const ensureGitHubWorkUnitProjectionRequest = async () => export const completeGitHubWorkUnitProjectionRequest = async ( token: string ) => { + // Author changes can alter issue-only pages even when no work unit changes. + const policyChanged = sql`${githubPublicFeedHead.summaryPolicyDigest} IS DISTINCT FROM ${PIPELINE_POLICY_DIGEST}`; + const revisionIncrement = sql`CASE WHEN ${policyChanged} THEN 1 ELSE 0 END`; const [cleared] = await getDatabase() .update(githubPublicFeedHead) .set({ projectionRequestToken: null, summaryPolicyDigest: PIPELINE_POLICY_DIGEST, + feedRevision: sql`${githubPublicFeedHead.feedRevision} + ${revisionIncrement}`, + headContentRevision: sql`${githubPublicFeedHead.headContentRevision} + ${revisionIncrement}`, + orderingRevision: sql`${githubPublicFeedHead.orderingRevision} + ${revisionIncrement}`, + lastPublishedAt: sql`CASE WHEN ${policyChanged} THEN now() ELSE ${githubPublicFeedHead.lastPublishedAt} END`, }) .where( and( diff --git a/src/lib/github-work-unit-store.ts b/src/lib/github-work-unit-store.ts index 7f65282..5088382 100644 --- a/src/lib/github-work-unit-store.ts +++ b/src/lib/github-work-unit-store.ts @@ -62,10 +62,7 @@ const SUMMARY_EVALUATION_LIMIT = 8; const SUMMARY_DEBOUNCE_MS = 5 * 60 * 1000; const DIGEST = /^[a-f0-9]{64}$/u; const SHA = /^[a-f0-9]{40}$/u; - -const trackedAuthorUserIds = new Set( - Object.values(TRACKED_GITHUB_USER_IDS) -); +const trackedAuthorUserIds = new Set(Object.values(TRACKED_GITHUB_USER_IDS)); type GitHubWorkUnitDatabase = ReturnType; type GitHubWorkUnitTransaction = Parameters< @@ -1278,7 +1275,14 @@ const loadProjectionSnapshot = async ( eq(githubIssues.repositoryId, githubRepositories.id) ) .where( - inArray(githubRepositories.visibility, ["public", "private", "internal"]) + and( + inArray(githubIssues.authorUserId, [...trackedAuthorUserIds]), + inArray(githubRepositories.visibility, [ + "public", + "private", + "internal", + ]) + ) ); const issueDays = issueRows.map((issue) => issueDayFrom(issue.createdAt)); return { diff --git a/src/lib/github-work-unit-summary-store.ts b/src/lib/github-work-unit-summary-store.ts index 2df1da2..c80ed1e 100644 --- a/src/lib/github-work-unit-summary-store.ts +++ b/src/lib/github-work-unit-summary-store.ts @@ -26,6 +26,7 @@ import { } from "@/db/schema"; import { env } from "@/env"; import { PUBLIC_GITHUB_ACTIVITY_DAY_PAGE_SIZE } from "@/lib/github-activity-store"; +import { TRACKED_GITHUB_USER_IDS } from "@/lib/github-commits-core"; import { GITHUB_SUMMARY_REQUEST_BUDGET } from "@/lib/github-cron-config"; import { acquireGitHubWorkUnitProjectionLock } from "@/lib/github-work-unit-projection-state"; import { @@ -644,11 +645,17 @@ async function readInitialPageDays(transaction: SummaryTransaction) { eq(githubIssues.repositoryId, githubRepositories.id) ) .where( - inArray(githubRepositories.visibility, [ - "public", - "private", - "internal", - ]) + and( + inArray( + githubIssues.authorUserId, + Object.values(TRACKED_GITHUB_USER_IDS) + ), + inArray(githubRepositories.visibility, [ + "public", + "private", + "internal", + ]) + ) ) .orderBy(desc(issueDay)) .limit(PUBLIC_GITHUB_ACTIVITY_DAY_PAGE_SIZE), diff --git a/src/lib/remark-static-image-imports.mjs b/src/lib/remark-static-image-imports.mjs index 7363583..80b81c9 100644 --- a/src/lib/remark-static-image-imports.mjs +++ b/src/lib/remark-static-image-imports.mjs @@ -153,6 +153,57 @@ const transformJsxImage = (node, imports, importAliases, counterRef) => { srcAttribute.value = createIdentifierExpression(identifier); }; +const createImageManifest = (aliases) => ({ + type: "mdxjsEsm", + value: "export const blogImages = {};", + data: { + estree: { + type: "Program", + sourceType: "module", + body: [ + { + type: "ExportNamedDeclaration", + specifiers: [], + source: null, + declaration: { + type: "VariableDeclaration", + kind: "const", + declarations: [ + { + type: "VariableDeclarator", + id: { type: "Identifier", name: "blogImages" }, + init: { + type: "ObjectExpression", + properties: [...aliases].map(([url, identifier]) => ({ + type: "Property", + kind: "init", + method: false, + shorthand: false, + computed: false, + key: { + type: "Literal", + value: url.startsWith("@/../public") + ? url.slice("@/../public".length) + : url, + }, + value: { + type: "MemberExpression", + computed: false, + optional: false, + object: { type: "Identifier", name: identifier }, + property: { type: "Identifier", name: "src" }, + }, + })), + }, + }, + ], + }, + }, + ], + }, + }, +}); + const remarkStaticImageImports = () => (tree) => { const imports = []; const importAliases = new Map(); @@ -188,7 +239,11 @@ const remarkStaticImageImports = () => (tree) => { }); if (imports.length > 0 && Array.isArray(tree.children)) { - tree.children = [...imports, ...tree.children]; + tree.children = [ + ...imports, + createImageManifest(importAliases), + ...tree.children, + ]; } }; diff --git a/src/lib/resume.ts b/src/lib/resume.ts index 5025033..3804b61 100644 --- a/src/lib/resume.ts +++ b/src/lib/resume.ts @@ -1,11 +1,12 @@ import { resumeCompanyStageLabels, resumeData, + socialProfiles, resumeRoleMarkerLabels, } from "@/content/resume"; import type { PublicReference, ResumeRole } from "@/content/resume"; import type { BlogPost } from "@/lib/blog-utils"; -import { publicUrl } from "@/lib/site"; +import { publicUrl, resumePdfUrl } from "@/lib/site"; export interface AskAgentAction { description: string; @@ -112,7 +113,7 @@ const buildAskAboutMePrompt = () => { `Start at ${contextUrl} and follow the relevant links for context about ${resumeData.person.name}.`, "This is an informational research chat, not a code-editing task.", `I want to ask questions about ${resumeData.person.name}'s work, technical depth, projects, and fit for roles such as ${resumeData.person.targetPositioning}.`, - "Use his résumé and linked work as sources for your answers.", + "Use the résumé and linked work as sources for your answers.", ].join(" "); }; @@ -145,29 +146,9 @@ export const buildJsonResume = () => ({ email: resumeData.person.email, image: publicUrl(resumeData.person.image), label: currentRole?.title ?? resumeData.person.role, - location: { - city: "Mumbai", - countryCode: "IN", - region: "Maharashtra", - }, + location: resumeData.person.address, name: resumeData.person.name, - profiles: [ - { - network: "LinkedIn", - url: "https://linkedin.com/in/f0rr0", - username: "f0rr0", - }, - { - network: "GitHub", - url: "https://github.com/f0rr0", - username: "f0rr0", - }, - { - network: "GitHub", - url: "https://github.com/yuppiestechdev", - username: "yuppiestechdev", - }, - ], + profiles: socialProfiles, summary: resumeData.summary, url: publicUrl("/"), }, @@ -276,12 +257,9 @@ export const buildLlmsTxt = ( - [Detailed career context](${localProfileUrl("/llms-full.txt")}): Full work history, engineering decisions, leadership scope, client engagements, and source links. Read for technical interviews or role-fit questions. - [Journey](${localProfileUrl("/journey")}): Human-readable experience and education. -## Namefi Work +## Selected References -${resumeData.machineReadable.publicReferences - .filter((reference) => new URL(reference.href).hostname === "namefi.io") - .map(markdownLink) - .join("\n")} +${resumeData.machineReadable.publicReferences.map(markdownLink).join("\n")} ## Code and Technical Writing @@ -293,7 +271,7 @@ ${resumeData.links.map((link) => `- [${link.label}](${link.href})`).join("\n")} ## Optional -- [PDF résumé](${localProfileUrl("/resume/sid-jain-resume.pdf")}): Downloadable résumé. +- [PDF résumé](${localProfileUrl(resumePdfUrl)}): Downloadable résumé. - [Work](${localProfileUrl("/work")}): Recent code activity. - [Writing](${localProfileUrl("/writing")}): All published articles; each article is also available at /writing/{slug}.md. - [RSS](${localProfileUrl("/rss.xml")}): Article feed. @@ -319,29 +297,19 @@ export const buildLlmsFullTxt = (blogPosts: BlogPost[] = []) => { note: "Structured experience and skills.", }, { - href: localProfileUrl("/resume/sid-jain-resume.pdf"), + href: localProfileUrl(resumePdfUrl), label: "PDF Resume", note: "Downloadable résumé.", }, - { - href: "https://linkedin.com/in/f0rr0", - label: "LinkedIn", - note: "Professional profile.", - }, - { - href: "https://github.com/f0rr0", - label: "GitHub: f0rr0", - note: "Open-source projects.", - }, - { - href: "https://github.com/yuppiestechdev", - label: "GitHub: yuppiestechdev", - note: "Additional engineering work.", - }, + ...socialProfiles.map(({ network, username, url }) => ({ + href: url, + label: `${network}: ${username}`, + note: "Public profile.", + })), { href: `mailto:${resumeData.person.email}`, label: "Email", - note: "Contact Sid.", + note: `Contact ${resumeData.person.name}.`, }, ]; diff --git a/src/lib/site-url.ts b/src/lib/site-url.ts index ebc1962..7c84363 100644 --- a/src/lib/site-url.ts +++ b/src/lib/site-url.ts @@ -1 +1,46 @@ -export const CANONICAL_SITE_URL = "https://f0rr0.dev"; +import { env } from "../env"; + +export const productionSiteOrigin = (hostname: string | undefined) => { + const host = hostname?.trim(); + if (host === undefined || host.length === 0) { + throw new Error( + "VERCEL_PROJECT_PRODUCTION_URL is required for production URLs." + ); + } + const url = new URL(`https://${host}`); + if ( + url.host !== host || + url.username || + url.password || + url.pathname !== "/" || + url.search || + url.hash || + url.hostname === "localhost" || + url.hostname.endsWith(".localhost") + ) { + throw new Error( + "VERCEL_PROJECT_PRODUCTION_URL must be a production hostname." + ); + } + return url.origin; +}; + +export const siteOriginFrom = (environment: Partial) => { + if ( + (environment.VERCEL_PROJECT_PRODUCTION_URL?.trim().length ?? 0) > 0 || + environment.VERCEL === "1" + ) { + return productionSiteOrigin(environment.VERCEL_PROJECT_PRODUCTION_URL); + } + return new URL( + `http://localhost:${(environment.PORT?.trim() ?? "") || (environment.NEXT_PUBLIC_PORT?.trim() ?? "") || "3000"}` + ).origin; +}; + +// Vercel provides the public hostname at build time; standalone scripts use its server variable. +export const CANONICAL_SITE_URL = + env.NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL === undefined + ? typeof window === "undefined" + ? siteOriginFrom(env) + : window.location.origin + : productionSiteOrigin(env.NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL); diff --git a/src/lib/site.ts b/src/lib/site.ts index c7f3b54..c2ce916 100644 --- a/src/lib/site.ts +++ b/src/lib/site.ts @@ -1,21 +1,24 @@ -import { resumeData } from "@/content/resume"; +import { primaryGitHubProfile, resumeData } from "@/content/resume"; +import { sitePreferences } from "@/content/site"; import { CANONICAL_SITE_URL } from "@/lib/site-url"; export const siteConfig = { author: { bio: resumeData.summary, - handle: "f0rr0", - image: "/resume/sid-jain-profile.png", + handle: primaryGitHubProfile.username, + image: resumeData.person.image, name: resumeData.person.name, role: resumeData.person.role, }, description: `${resumeData.person.name}. ${resumeData.summary}`, - language: "en-US", - locale: "en_US", - name: "Sid Jain", - shortName: "F0RR0", + language: sitePreferences.language, + locale: sitePreferences.language.replace("-", "_"), + name: resumeData.person.name, + shortName: primaryGitHubProfile.username.toUpperCase(), url: CANONICAL_SITE_URL, }; export const publicUrl = (path: string) => new URL(path, CANONICAL_SITE_URL).toString(); + +export const resumePdfUrl = resumeData.pdf.outputPath.replace(/^public\//, "/"); diff --git a/src/lib/structured-data.ts b/src/lib/structured-data.ts index 09afcef..cdcca21 100644 --- a/src/lib/structured-data.ts +++ b/src/lib/structured-data.ts @@ -10,18 +10,13 @@ import type { WithContext, } from "schema-dts"; -import { resumeData } from "@/content/resume"; +import { resumeData, socialProfiles } from "@/content/resume"; import type { BlogPost } from "@/lib/blog-utils"; import { publicUrl, siteConfig } from "@/lib/site"; -const linkedInUrl = "https://linkedin.com/in/f0rr0"; -const githubUrl = "https://github.com/f0rr0"; -const yuppiesGithubUrl = "https://github.com/yuppiestechdev"; - -const personId = () => publicUrl("/#sid-jain"); +const personId = () => publicUrl("/#person"); const websiteId = () => publicUrl("/#website"); - -const sameAs = [linkedInUrl, githubUrl, yuppiesGithubUrl]; +const sameAs = socialProfiles.map(({ url }) => url); const [currentExperience] = resumeData.experience; const [currentRole] = currentExperience?.roles ?? []; const currentCompanyReference = @@ -33,18 +28,11 @@ const buildPersonNode = (): Person => ({ "@id": personId(), "@type": "Person", alternateName: resumeData.person.alternateNames, - alumniOf: [ - { - "@type": "CollegeOrUniversity", - name: "University of California, Los Angeles", - sameAs: "https://www.ucla.edu/", - }, - { - "@type": "EducationalOrganization", - name: "Delhi Public School, R. K. Puram", - sameAs: "https://dpsrkp.net/", - }, - ], + alumniOf: resumeData.education.map((school) => ({ + "@type": "EducationalOrganization" as const, + name: school.company, + sameAs: school.url, + })), description: resumeData.summary, email: `mailto:${resumeData.person.email}`, image: publicUrl(resumeData.person.image), @@ -68,14 +56,14 @@ const buildPersonNode = (): Person => ({ const buildWebsiteNode = (): WebSite => ({ "@id": websiteId(), "@type": "WebSite", - alternateName: "F0RR0", + alternateName: siteConfig.shortName, author: { "@id": personId(), "@type": "Person", name: resumeData.person.name, }, description: siteConfig.description, - inLanguage: "en-US", + inLanguage: siteConfig.language, name: resumeData.person.name, publisher: { "@id": personId(), @@ -103,7 +91,7 @@ export const buildProfilePageJsonLd = (): WithContext => ({ url: publicUrl("/"), }, mainEntity: buildPersonNode(), - name: "Sid Jain Journey", + name: `${siteConfig.name} Journey`, url: publicUrl("/journey"), }); @@ -130,7 +118,7 @@ export const buildBlogPostingJsonLd = ({ datePublished: post.date.toISOString(), description: post.metadata.summary, headline: post.metadata.title, - inLanguage: "en-US", + inLanguage: siteConfig.language, isPartOf: { "@id": websiteId(), "@type": "WebSite", @@ -164,7 +152,7 @@ export const buildBlogCollectionJsonLd = ( "@id": publicUrl("/writing#collection"), "@type": "CollectionPage", description: `Notes on what ${resumeData.person.name} is building across product design, engineering, AI, and creative development.`, - inLanguage: "en-US", + inLanguage: siteConfig.language, isPartOf: { "@id": websiteId(), "@type": "WebSite", @@ -189,6 +177,6 @@ export const buildBlogCollectionJsonLd = ( ), numberOfItems: posts.length, } satisfies ItemList, - name: "Sid Jain Writing", + name: `${siteConfig.name} Writing`, url: publicUrl("/writing"), }); diff --git a/src/proxy.ts b/src/proxy.ts index d08babb..e65345a 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -1,6 +1,8 @@ import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; +import { env } from "@/env"; + export function proxy(request: NextRequest) { const url = new URL(request.url); if (url.pathname !== "/_r7k2" && !url.pathname.startsWith("/_r7k2/")) { @@ -12,10 +14,11 @@ export function proxy(request: NextRequest) { } url.pathname = url.pathname.slice("/_r7k2".length) || "/"; + const region = env.NEXT_PUBLIC_POSTHOG_REGION; url.hostname = url.pathname.startsWith("/static/") || url.pathname.startsWith("/array/") - ? "us-assets.i.posthog.com" - : "us.i.posthog.com"; + ? `${region}-assets.i.posthog.com` + : `${region}.i.posthog.com`; url.protocol = "https:"; url.port = ""; const headers = new Headers(request.headers); diff --git a/tests/blog-markdown.test.ts b/tests/blog-markdown.test.ts index 86d87ec..4242972 100644 --- a/tests/blog-markdown.test.ts +++ b/tests/blog-markdown.test.ts @@ -9,11 +9,17 @@ describe("blog Markdown", () => { test("resolves authored image assets without rewriting fenced examples", () => { const body = '![A chart](./chart.svg)\n\n```md\n![Example](./example.png)\n```\n~~~~md\n![Another](./example.png)\n~~~~\n![Remote](https://example.com/image.png)'; - const output = resolveBlogMarkdownImages(body, "a-post/page.mdx"); - const base = - "https://raw.githubusercontent.com/f0rr0/f0rr0.dev/next/src/content/blog/a-post/"; - expect(output).toContain(`![A chart](${base}chart.svg)`); - expect(output).toContain(`src="${base}screen.png"`); + const output = resolveBlogMarkdownImages( + body, + { + "./chart.svg": "/_next/static/media/chart.hash.svg", + "./screen.png": "/_next/static/media/screen.hash.png", + }, + "https://example.com/writing/a-post" + ); + const base = "https://example.com/_next/static/media/"; + expect(output).toContain(`![A chart](${base}chart.hash.svg)`); + expect(output).toContain(`src="${base}screen.hash.png"`); expect(output).toContain("```md\n![Example](./example.png)\n```"); expect(output).toContain("~~~~md\n![Another](./example.png)\n~~~~"); expect(output).toContain("![Remote](https://example.com/image.png)"); diff --git a/tests/github-accounts.test.ts b/tests/github-accounts.test.ts new file mode 100644 index 0000000..04f63cf --- /dev/null +++ b/tests/github-accounts.test.ts @@ -0,0 +1,99 @@ +import { afterEach, expect, test } from "bun:test"; + +import { + githubTokensFrom, + tokenForGitHubAccount, + tokensForGitHubAccount, +} from "../src/lib/github-accounts"; +import { assertGitHubTokenIdentity } from "../src/lib/github-commits"; +import { + TRACKED_GITHUB_ACCOUNTS, + TRACKED_GITHUB_USER_IDS, +} from "../src/lib/github-commits-core"; +import { env, mockFetch } from "./helpers"; + +const originalFetch = globalThis.fetch; +const originalEnvironment = { + GITHUB_TOKENS: env.GITHUB_TOKENS, + GITHUB_TOKEN: env.GITHUB_TOKEN, + GH_TOKEN: env.GH_TOKEN, +}; +afterEach(() => { + globalThis.fetch = originalFetch; + Object.assign(env, originalEnvironment); +}); + +test("parses arbitrary configured account keys and rejects malformed or ambiguous input without exposing tokens", () => { + expect(githubTokensFrom()).toEqual({}); + expect(githubTokensFrom(" ")).toEqual({}); + expect( + githubTokensFrom('{"Alice":" one ","bob":"two"}', ["alice", "bob"]) + ).toEqual({ alice: "one", bob: "two" }); + for (const value of [ + '{"unknown":"private-value"}', + '{"alice":"private-value","ALICE":"second"}', + '{"alice":""}', + '["private-value"]', + "null", + '"private-value"', + "private-value", + ]) { + expect(() => githubTokensFrom(value, ["alice"])).toThrow(TypeError); + try { + githubTokensFrom(value, ["alice"]); + } catch (error) { + expect(String(error)).not.toContain("private-value"); + } + } +}); + +test("credential rotation, removal and order never change authors or require network calls", () => { + globalThis.fetch = mockFetch(() => { + throw new Error("Unexpected discovery request"); + }); + const authors = TRACKED_GITHUB_USER_IDS; + const accounts = TRACKED_GITHUB_ACCOUNTS; + delete env.GITHUB_TOKEN; + delete env.GH_TOKEN; + for (const tokens of [ + { f0rr0: "first", yuppiestechdev: "second" }, + { yuppiestechdev: "second", f0rr0: "rotated" }, + { yuppiestechdev: "second" }, + {}, + ]) { + env.GITHUB_TOKENS = JSON.stringify(tokens); + expect(TRACKED_GITHUB_USER_IDS).toEqual(authors); + expect(TRACKED_GITHUB_ACCOUNTS).toEqual(accounts); + expect(tokensForGitHubAccount()).toEqual(Object.values(tokens)); + } + expect(() => tokenForGitHubAccount("f0rr0")).toThrow("No GitHub token"); + env.GITHUB_TOKENS = JSON.stringify({ + f0rr0: "first", + yuppiestechdev: "second", + }); + env.GH_TOKEN = "first"; + expect(tokensForGitHubAccount("yuppiestechdev")).toEqual(["second", "first"]); + expect(tokenForGitHubAccount("f0rr0")).toBe("first"); +}); + +test("existing verification checks both stable identity and configured login in one request", async () => { + let calls = 0; + let identity = { id: 8_574_219, login: "F0rr0" }; + globalThis.fetch = mockFetch(async (input) => { + calls += 1; + expect(new URL(input instanceof Request ? input.url : input).pathname).toBe( + "/user" + ); + return Response.json(identity); + }); + await assertGitHubTokenIdentity("f0rr0", "test-token"); + expect(calls).toBe(1); + identity = { id: 99_666_891, login: "f0rr0" }; + expect(assertGitHubTokenIdentity("f0rr0", "test-token")).rejects.toThrow( + "not authenticated" + ); + identity = { id: 8_574_219, login: "renamed" }; + expect(assertGitHubTokenIdentity("f0rr0", "test-token")).rejects.toThrow( + "not authenticated" + ); +}); diff --git a/tests/github-activity-days.test.tsx b/tests/github-activity-days.test.tsx index 5b1b772..2f029fe 100644 --- a/tests/github-activity-days.test.tsx +++ b/tests/github-activity-days.test.tsx @@ -3,6 +3,7 @@ import { expect, test } from "bun:test"; import { renderToStaticMarkup } from "react-dom/server"; import { GitHubActivityDays } from "../src/components/github-activity-days"; +import { WORK_LOG_TIME_ZONE } from "../src/lib/date"; import { buildPublicGitHubActivityDays } from "../src/lib/github-activity-feed-core"; test("the initial homepage HTML includes all IST work and totals across UTC midnight", () => { @@ -45,6 +46,6 @@ test("the initial homepage HTML includes all IST work and totals across UTC midn expect(html).toContain("5 commits across 2 repos"); expect(html).toContain("+4,474"); expect(html).toContain("−3,427"); - expect(html).toContain("IST"); + expect(html).toContain(WORK_LOG_TIME_ZONE); expect(html).not.toContain("yesterday"); }); diff --git a/tests/github-activity-feed-postgres.test.ts b/tests/github-activity-feed-postgres.test.ts index 30c6a4e..3ef30a5 100644 --- a/tests/github-activity-feed-postgres.test.ts +++ b/tests/github-activity-feed-postgres.test.ts @@ -230,13 +230,13 @@ describe.skipIf(!dockerAvailable)("GitHub work-unit feed projection", () => { account, author_user_id, created_at, node_id, number, repository_id, title_snapshot, url_snapshot ) values - ('f0rr0', '1', '2026-08-29T12:00:00Z', 'ISSUE_public_feed_1', 7, + ('f0rr0', '8574219', '2026-08-29T12:00:00Z', 'ISSUE_public_feed_1', 7, '102', 'Track deterministic activity', 'https://github.com/untrusted/ignored/issues/700'), - ('f0rr0', '1', '2026-08-30T08:00:00Z', 'ISSUE_private_feed_1', 8, + ('f0rr0', '8574219', '2026-08-30T08:00:00Z', 'ISSUE_private_feed_1', 8, '201', 'private issue title sentinel', 'https://github.com/secret-owner/private-repository-sentinel/issues/8'), - ('f0rr0', '1', '2026-08-31T08:00:00Z', 'ISSUE_unknown_feed_1', 9, + ('f0rr0', '8574219', '2026-08-31T08:00:00Z', 'ISSUE_unknown_feed_1', 9, '301', 'unknown issue title sentinel', 'https://github.com/unknown-owner/unknown-repository-sentinel/issues/9') `; @@ -609,4 +609,15 @@ describe.skipIf(!dockerAvailable)("GitHub work-unit feed projection", () => { title: "revoked private issue title sentinel", }); }); + test("keeps unselected authors out of issue rows and pagination without deleting evidence", async () => { + await admin`insert into github_issues ( + account, author_user_id, created_at, node_id, number, repository_id, title_snapshot, url_snapshot + ) values ('alice', '12345678', '2099-01-01T12:00:00Z', 'ISSUE_foreign_author', 999, '102', 'foreign author sentinel', 'https://github.com/example/repo/issues/999')`; + const page = await readPublicGitHubActivityPage(null); + expect(JSON.stringify(page)).not.toContain("foreign author sentinel"); + expect(JSON.stringify(page)).not.toContain("2099-01-01"); + const rows = + await admin`select node_id from github_issues where node_id = 'ISSUE_foreign_author'`; + expect(rows).toHaveLength(1); + }); }); diff --git a/tests/github-activity-processor.test.ts b/tests/github-activity-processor.test.ts index 64ff39e..58085ec 100644 --- a/tests/github-activity-processor.test.ts +++ b/tests/github-activity-processor.test.ts @@ -15,8 +15,8 @@ import { GitHubRequestDeadlineError } from "../src/lib/github-api.ts"; import { mockFetch, env } from "./helpers.ts"; const originalFetch = globalThis.fetch; -const originalF0rr0Token = env.GITHUB_F0RR0_TOKEN; -const originalYuppiesTechDevToken = env.GITHUB_YUPPIESTECHDEV_TOKEN; +const originalTokens = env.GITHUB_TOKENS; +const originalGhToken = env.GH_TOKEN; const originalDefaultToken = env.GITHUB_TOKEN; const pushCommitValue = (sha: string, login: string, id: number) => ({ @@ -42,18 +42,15 @@ const restoreEnvironmentValue = ( }; beforeEach(() => { - delete env.GITHUB_F0RR0_TOKEN; - delete env.GITHUB_YUPPIESTECHDEV_TOKEN; + delete env.GITHUB_TOKENS; + delete env.GH_TOKEN; delete env.GITHUB_TOKEN; }); afterEach(() => { globalThis.fetch = originalFetch; - restoreEnvironmentValue("GITHUB_F0RR0_TOKEN", originalF0rr0Token); - restoreEnvironmentValue( - "GITHUB_YUPPIESTECHDEV_TOKEN", - originalYuppiesTechDevToken - ); + restoreEnvironmentValue("GITHUB_TOKENS", originalTokens); + restoreEnvironmentValue("GH_TOKEN", originalGhToken); restoreEnvironmentValue("GITHUB_TOKEN", originalDefaultToken); }); @@ -70,7 +67,7 @@ describe("GitHub activity commit acquisition", () => { filenames[2] = "A.ts"; const requestedPages: number[] = []; - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -127,7 +124,7 @@ describe("GitHub activity commit acquisition", () => { const sha = "d".repeat(40); const ancestryResponses = [undefined, [{ sha: "not-a-sha" }], []]; let commitReads = 0; - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -175,7 +172,7 @@ describe("GitHub activity commit acquisition", () => { const trackedSha = "3".repeat(40); const foreignSha = "4".repeat(40); const afterSha = foreignSha; - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -216,7 +213,7 @@ describe("GitHub activity commit acquisition", () => { const surplusSha = "3".repeat(40); const afterSha = "4".repeat(40); const paths: string[] = []; - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -266,7 +263,7 @@ describe("GitHub activity commit acquisition", () => { const otherSha = "3".repeat(40); const afterSha = "4".repeat(40); const paths: string[] = []; - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -313,7 +310,7 @@ describe("GitHub activity commit acquisition", () => { const firstSha = "2".repeat(40); const surplusSha = "3".repeat(40); const afterSha = "4".repeat(40); - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async () => Response.json({ ahead_by: 3, @@ -347,7 +344,7 @@ describe("GitHub activity commit acquisition", () => { const beforeSha = "1".repeat(40); const firstSha = "2".repeat(40); const afterSha = "3".repeat(40); - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async () => Response.json({ ahead_by: 2, @@ -380,7 +377,7 @@ describe("GitHub activity commit acquisition", () => { const beforeSha = "1".repeat(40); const foreignSha = "2".repeat(40); const afterSha = "3".repeat(40); - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async () => Response.json({ ahead_by: 2, @@ -426,7 +423,7 @@ describe("GitHub activity commit acquisition", () => { test("rejects a tracked commit without a provider timestamp", async () => { const beforeSha = "1".repeat(40); const afterSha = "2".repeat(40); - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async () => Response.json({ ahead_by: 1, @@ -461,7 +458,7 @@ describe("GitHub activity commit acquisition", () => { test("accepts a ref rewind with no newly reachable commits", async () => { const beforeSha = "1".repeat(40); const afterSha = "2".repeat(40); - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async () => Response.json({ ahead_by: 0, commits: [], total_commits: 0 }) ); @@ -487,7 +484,7 @@ describe("GitHub activity commit acquisition", () => { const olderSha = "2".repeat(40); const afterSha = "3".repeat(40); const paths: string[] = []; - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input, init) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -555,7 +552,7 @@ describe("GitHub activity commit acquisition", () => { const firstSha = "3".repeat(40); const afterSha = "4".repeat(40); const paths: string[] = []; - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -599,7 +596,7 @@ describe("GitHub activity commit acquisition", () => { const afterSha = knownShas.at(-1); assert.ok(afterSha !== undefined); const paths: string[] = []; - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -636,8 +633,10 @@ describe("GitHub activity commit acquisition", () => { const afterSha = knownShas.at(-1); assert.ok(afterSha !== undefined); let calls = 0; - env.GITHUB_F0RR0_TOKEN = "first-token"; - env.GITHUB_YUPPIESTECHDEV_TOKEN = "second-token"; + env.GITHUB_TOKENS = JSON.stringify({ + f0rr0: "first-token", + yuppiestechdev: "second-token", + }); globalThis.fetch = mockFetch(async () => { calls += 1; if (calls === 1) { @@ -677,7 +676,7 @@ describe("GitHub activity commit acquisition", () => { const afterSha = knownShas.at(-1); assert.ok(afterSha !== undefined); let calls = 0; - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async () => { calls += 1; return new Response(null, { status: 409 }); @@ -708,7 +707,7 @@ describe("GitHub activity commit acquisition", () => { ); const afterSha = knownShas.at(-1); assert.ok(afterSha !== undefined); - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async () => Response.json( { @@ -748,7 +747,7 @@ describe("GitHub activity commit acquisition", () => { test("bounds a new branch by the observed count without slicing history", async () => { const olderSha = "1".repeat(40); const afterSha = "2".repeat(40); - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input, init) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -811,7 +810,7 @@ describe("GitHub activity commit acquisition", () => { const middleSha = "2".repeat(40); const afterSha = "3".repeat(40); const cursors: (string | null)[] = []; - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (_input, init) => { const request = JSON.parse(await new Response(init?.body).text()); cursors.push(request.variables.cursor); @@ -892,7 +891,7 @@ describe("GitHub activity commit acquisition", () => { test("backfills a closed date window without requiring the current ref head", async () => { const rangedSha = "4".repeat(40); const afterSha = "5".repeat(40); - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (_input, init) => { const request = JSON.parse(await new Response(init?.body).text()); expect(request.variables.since).toBe("2026-07-01T00:00:00.000Z"); @@ -939,7 +938,7 @@ describe("GitHub activity commit acquisition", () => { test("accepts a ref whose reachable history predates the boundary", async () => { const afterSha = "4".repeat(40); - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (_input, init) => { const request = JSON.parse(await new Response(init?.body).text()); expect(request.variables.since).toBe("2026-08-18T00:00:00.000Z"); @@ -1124,7 +1123,7 @@ describe("GitHub pull request merge commit resolution", () => { ); }); - test("classifies a rejected GraphQL request as non-retryable", async () => { + test("defers GraphQL access loss without declaring source evidence invalid", async () => { globalThis.fetch = mockFetch(async () => Response.json({ data: null, @@ -1135,9 +1134,9 @@ describe("GitHub pull request merge commit resolution", () => { expect( resolveGitHubPullRequestMergeCommits(["PR_hidden"], "test-token") ).rejects.toMatchObject({ - code: "source_invalid", - kind: "request_rejected", - retryable: false, + code: "source_incomplete", + kind: "access_denied", + retryable: true, }); }); }); @@ -1190,7 +1189,7 @@ describe("GitHub pull request acquisition", () => { }; test("rejects an invalid associated-PR item instead of completing empty", async () => { - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async () => Response.json([{ ...pullRequest, node_id: null }]) ); @@ -1211,7 +1210,7 @@ describe("GitHub pull request acquisition", () => { expect(Object.hasOwn(rest2026MergedPullRequest, "merge_commit_sha")).toBe( false ); - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -1244,7 +1243,7 @@ describe("GitHub pull request acquisition", () => { }); test("resolves an authoritative merge SHA for REST 2026 snapshots", async () => { - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); const requestedPaths: string[] = []; globalThis.fetch = mockFetch(async (input) => { const url = @@ -1281,7 +1280,7 @@ describe("GitHub pull request acquisition", () => { }); test("preserves an authoritative null merge SHA for a rebase merge", async () => { - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -1401,7 +1400,7 @@ describe("GitHub pull request acquisition", () => { sha: headSha, }, }; - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -1428,8 +1427,10 @@ describe("GitHub pull request acquisition", () => { test("unions associated PR visibility across both tracked identities", async () => { const commitSha = "8".repeat(40); - env.GITHUB_F0RR0_TOKEN = "f0-token"; - env.GITHUB_YUPPIESTECHDEV_TOKEN = "yuppies-token"; + env.GITHUB_TOKENS = JSON.stringify({ + f0rr0: "f0-token", + yuppiestechdev: "yuppies-token", + }); let calls = 0; globalThis.fetch = mockFetch(async () => { calls += 1; @@ -1453,7 +1454,7 @@ describe("GitHub pull request acquisition", () => { test("discovers associated tracked pull requests and reconciles membership", async () => { const commitSha = "a".repeat(40); const requestedPaths: string[] = []; - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); globalThis.fetch = mockFetch(async (input) => { const url = input instanceof Request ? new URL(input.url) : new URL(input); @@ -1554,7 +1555,7 @@ describe("GitHub pull request acquisition", () => { }); test("uses a complete paginated comparison beyond the PR commit cap", async () => { - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); const comparePages: number[] = []; globalThis.fetch = mockFetch(async (input) => { const url = @@ -1695,7 +1696,7 @@ describe("GitHub pull request acquisition", () => { describe("GitHub activity provider deadlines", () => { test("propagates one absolute deadline through every worker acquisition path", async () => { - env.GITHUB_F0RR0_TOKEN = "test-token"; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); let calls = 0; globalThis.fetch = mockFetch(async () => { calls += 1; diff --git a/tests/github-activity-worker.test.ts b/tests/github-activity-worker.test.ts index 6b864fd..232c059 100644 --- a/tests/github-activity-worker.test.ts +++ b/tests/github-activity-worker.test.ts @@ -21,8 +21,20 @@ import { import { GitHubResponseError } from "../src/lib/github-api.ts"; describe("GitHub activity terminal gaps", () => { + test("missing credentials and lost repository access never become terminal gaps", () => { + const errors = [ + ...[401, 403, 404].map( + (status) => new GitHubResponseError(status, { retryable: false }) + ), + new ActivityProcessingError("source_auth_missing", "No credentials"), + new GitHubGraphQlResponseError("access_denied", { retryable: true }), + ]; + for (const error of errors) { + expect(githubActivityFailureIsTerminal(error, 100)).toBe(false); + } + }); test("given a deterministic REST gap, it becomes unavailable on the third worker claim", () => { - for (const status of [403, 404, 410, 422]) { + for (const status of [410, 422]) { const error = new GitHubResponseError(status, { retryable: false }); expect( githubActivityFailureIsTerminal( @@ -71,7 +83,6 @@ describe("GitHub activity terminal gaps", () => { ).toBe(true); for (const code of [ "membership_incomplete", - "source_auth_missing", "source_incomplete", "source_invalid", "source_unavailable", diff --git a/tests/github-backfill.test.ts b/tests/github-backfill.test.ts index fd47bf3..7e4a1b6 100644 --- a/tests/github-backfill.test.ts +++ b/tests/github-backfill.test.ts @@ -205,7 +205,7 @@ describe("GitHub factual history backfill", () => { expect(() => { requireBackfillEnvironment(request, { DATABASE_URL: "postgresql://activity.example/database", - GITHUB_F0RR0_TOKEN: "token", + GITHUB_TOKENS: JSON.stringify({ f0rr0: "token" }), }); }).not.toThrow(); expect(() => { @@ -360,7 +360,7 @@ describe("GitHub factual history backfill", () => { const discovered = await runGitHubBackfillDiscovery( { deadlineAt: now.getTime() + 30 * 60_000, - environment: { GITHUB_F0RR0_TOKEN: "token" }, + environment: { GITHUB_TOKENS: JSON.stringify({ f0rr0: "token" }) }, request, }, { @@ -413,7 +413,7 @@ describe("GitHub factual history backfill", () => { const discovered = await runGitHubBackfillDiscovery( { deadlineAt: now.getTime() + 30 * 60_000, - environment: { GITHUB_F0RR0_TOKEN: "token" }, + environment: { GITHUB_TOKENS: JSON.stringify({ f0rr0: "token" }) }, request, }, { @@ -444,7 +444,7 @@ describe("GitHub factual history backfill", () => { const discovered = await runGitHubBackfillDiscovery( { deadlineAt: now.getTime() + 30 * 60_000, - environment: { GITHUB_F0RR0_TOKEN: "token" }, + environment: { GITHUB_TOKENS: JSON.stringify({ f0rr0: "token" }) }, request, }, { @@ -476,7 +476,7 @@ describe("GitHub factual history backfill", () => { const discovered = await runGitHubBackfillDiscovery( { deadlineAt: now.getTime() + 30 * 60_000, - environment: { GITHUB_F0RR0_TOKEN: "token" }, + environment: { GITHUB_TOKENS: JSON.stringify({ f0rr0: "token" }) }, request, }, { @@ -705,3 +705,18 @@ describe("GitHub factual history backfill", () => { expect(workerPasses).toBe(1); }); }); + +test("workflow secrets avoid GitHub's reserved prefix except its built-in token", async () => { + const directory = new URL("../.github/workflows/", import.meta.url); + let checked = 0; + for await (const file of new Bun.Glob("*.yml").scan(directory.pathname)) { + const workflow = await Bun.file(new URL(file, directory)).text(); + for (const [, name] of workflow.matchAll(/\bsecrets\.([A-Za-z_]\w*)/g)) { + expect( + name === "GITHUB_TOKEN" || !name.toUpperCase().startsWith("GITHUB_") + ).toBe(true); + checked += 1; + } + } + expect(checked).toBeGreaterThan(0); +}); diff --git a/tests/github-commits.test.ts b/tests/github-commits.test.ts index 40f459c..b0fe75f 100644 --- a/tests/github-commits.test.ts +++ b/tests/github-commits.test.ts @@ -987,7 +987,7 @@ describe("token identity", () => { expect(new Headers(init?.headers).get("authorization")).toBe( "Bearer token" ); - return Response.json({ id: 8_574_219, login: "renamed-account" }); + return Response.json({ id: 8_574_219, login: "f0rr0" }); }); expect( @@ -1001,7 +1001,7 @@ describe("token identity", () => { ); expect(assertGitHubTokenIdentity("f0rr0", "token")).rejects.toThrow( - "GITHUB_F0RR0_TOKEN is not authenticated as f0rr0" + "The GitHub token is not authenticated as f0rr0" ); }); }); diff --git a/tests/github-diff.test.ts b/tests/github-diff.test.ts index 1fcc0e2..7684e6c 100644 --- a/tests/github-diff.test.ts +++ b/tests/github-diff.test.ts @@ -105,8 +105,8 @@ test("a real binary change remains explicit while the text diff stays eligible", test("PR acquisition falls back to a SHA-pinned diff and rejects a moving PR", async () => { const originalFetch = globalThis.fetch; - const originalToken = env.GITHUB_F0RR0_TOKEN; - env.GITHUB_F0RR0_TOKEN = "test-token"; + const originalToken = env.GITHUB_TOKENS; + env.GITHUB_TOKENS = JSON.stringify({ f0rr0: "test-token" }); let head = "b".repeat(40); const base = "a".repeat(40); const paths: string[] = []; @@ -145,9 +145,9 @@ test("PR acquisition falls back to a SHA-pinned diff and rejects a moving PR", a } finally { globalThis.fetch = originalFetch; if (originalToken === undefined) { - delete env.GITHUB_F0RR0_TOKEN; + delete env.GITHUB_TOKENS; } else { - env.GITHUB_F0RR0_TOKEN = originalToken; + env.GITHUB_TOKENS = originalToken; } } }); diff --git a/tests/github-pull-request-store.test.ts b/tests/github-pull-request-store.test.ts index 3c7dc86..00cef22 100644 --- a/tests/github-pull-request-store.test.ts +++ b/tests/github-pull-request-store.test.ts @@ -7,6 +7,9 @@ import { test, } from "bun:test"; import assert from "node:assert/strict"; +import { copyFile, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; import { setTimeout as delay } from "node:timers/promises"; import { and, asc, eq } from "drizzle-orm"; @@ -14,6 +17,7 @@ import { drizzle } from "drizzle-orm/postgres-js"; import { migrate } from "drizzle-orm/postgres-js/migrator"; import postgres from "postgres"; +import migrationJournal from "../drizzle/meta/_journal.json"; import type * as DatabaseClient from "../src/db/client.ts"; import type * as DatabaseSchema from "../src/db/schema.ts"; import type * as GithubActivityWorkerStore from "../src/lib/github-activity-worker-store.ts"; @@ -171,7 +175,44 @@ describe.skipIf(!dockerAvailable)("GitHub pull request persistence", () => { onnotice: (notice) => void notice, prepare: false, }); - await migrate(drizzle({ client: admin }), { migrationsFolder }); + // Exercise an upgrade with an existing checkpoint, not only an empty install. + const previousMigrations = await mkdtemp( + path.join(tmpdir(), "github-account-upgrade-") + ); + const previousEntries = migrationJournal.entries.filter( + ({ idx }) => idx < 22 + ); + try { + await mkdir(path.join(previousMigrations, "meta")); + await writeFile( + path.join(previousMigrations, "meta/_journal.json"), + JSON.stringify({ ...migrationJournal, entries: previousEntries }) + ); + await Promise.all( + previousEntries.map(async ({ tag }) => { + await copyFile( + path.join(migrationsFolder, `${tag}.sql`), + path.join(previousMigrations, `${tag}.sql`) + ); + }) + ); + await migrate(drizzle({ client: admin }), { + migrationsFolder: previousMigrations, + }); + await admin`insert into github_account_checkpoints (account, latest_event_id, paused) values ('f0rr0', '42', true)`; + await admin`insert into github_repository_inventory_heads (account_user_id, account_login) values ('8574219', 'f0rr0')`; + await migrate(drizzle({ client: admin }), { migrationsFolder }); + const [upgraded] = + await admin`select latest_event_id, paused from github_account_checkpoints where account = 'f0rr0'`; + expect(upgraded).toMatchObject({ + latest_event_id: "42", + paused: true, + }); + await admin`delete from github_repository_inventory_heads where account_user_id = '8574219'`; + await admin`delete from github_account_checkpoints where account = 'f0rr0'`; + } finally { + await rm(previousMigrations, { recursive: true, force: true }); + } env.DATABASE_URL = databaseUrl; schema = await import("../src/db/schema.ts"); const client = await import("../src/db/client.ts"); @@ -211,6 +252,24 @@ describe.skipIf(!dockerAvailable)("GitHub pull request persistence", () => { } }); + test("accepts a configured account name without adding identity registration columns", async () => { + await database + .insert(schema.githubAccountCheckpoints) + .values({ account: "alice" }); + await database.insert(schema.githubWebhookDeliveries).values({ + deliveryId: "00000000-0000-4000-8000-000000999999", + event: "ping", + accepted: false, + account: "alice", + }); + const [checkpoint] = await database + .select() + .from(schema.githubAccountCheckpoints) + .where(eq(schema.githubAccountCheckpoints.account, "alice")); + expect(checkpoint?.account).toBe("alice"); + expect(checkpoint).not.toHaveProperty("userId"); + }); + test("promotes an equal-time terminal signal without trusting its merge SHA", async () => { const initial = await persistGitHubWebhookPullRequest( "00000000-0000-4000-8000-000000000001", @@ -715,5 +774,35 @@ describe.skipIf(!dockerAvailable)("GitHub pull request persistence", () => { ); expect(discovery.repository).toBe("f0rr0/renamed-worker-path"); await releaseGitHubPullRequestDiscovery(discovery); + + // A smaller visible result cannot erase a previously verified association. + const { completeGitHubPullRequestDiscovery } = + await import("../src/lib/github-activity-worker-store"); + await database.insert(schema.githubCommitPullRequestAssociations).values({ + commitRepositoryId: workerRepositoryId, + commitSha, + pullRequestNodeId: pullRequest().nodeId, + }); + const [reclaimed] = await claimGitHubCommitsForPullRequestDiscovery( + 1, + ["f0rr0"], + new Date("2026-09-02T10:03:00.000Z") + ); + expect(await completeGitHubPullRequestDiscovery(reclaimed, [])).toBe(true); + const associations = await database + .select() + .from(schema.githubCommitPullRequestAssociations) + .where( + and( + eq( + schema.githubCommitPullRequestAssociations.commitRepositoryId, + workerRepositoryId + ), + eq(schema.githubCommitPullRequestAssociations.commitSha, commitSha) + ) + ); + expect(associations.map((row) => row.pullRequestNodeId)).toEqual([ + pullRequest().nodeId, + ]); }); }); diff --git a/tests/github-work-unit-store-postgres.test.ts b/tests/github-work-unit-store-postgres.test.ts index 513bbe3..d9027c6 100644 --- a/tests/github-work-unit-store-postgres.test.ts +++ b/tests/github-work-unit-store-postgres.test.ts @@ -555,6 +555,7 @@ describe.skipIf(!dockerAvailable)("GitHub work-unit projection store", () => { }); test("atomically swaps public facts, revisions, and summary eligibility", async () => { + const [before] = await database.select().from(schema.githubPublicFeedHead); const first = await refreshGitHubWorkUnitProjection(observedAt); expect(first).toMatchObject({ changed: true, @@ -578,9 +579,9 @@ describe.skipIf(!dockerAvailable)("GitHub work-unit projection store", () => { expect(unit.summaryInputDigest).toMatch(/^[a-f0-9]{64}$/u); expect(unit.summaryEvaluatedDigest).toMatch(/^[a-f0-9]{64}$/u); expect(head).toMatchObject({ - feedRevision: 1, - headContentRevision: 1, - orderingRevision: 1, + feedRevision: before.feedRevision + 1, + headContentRevision: before.headContentRevision + 1, + orderingRevision: before.orderingRevision + 1, }); expect(attempts).toHaveLength(1); expect(attempts[0]).toMatchObject({ @@ -603,9 +604,9 @@ describe.skipIf(!dockerAvailable)("GitHub work-unit projection store", () => { summaryAttemptsQueued: 0, }); expect(unchangedHead).toMatchObject({ - feedRevision: 1, - headContentRevision: 1, - orderingRevision: 1, + feedRevision: before.feedRevision + 1, + headContentRevision: before.headContentRevision + 1, + orderingRevision: before.orderingRevision + 1, }); }); @@ -1727,4 +1728,96 @@ describe.skipIf(!dockerAvailable)("GitHub work-unit projection store", () => { }); expect(await ensureGitHubWorkUnitProjectionRequest()).toBeNull(); }); + test("credential changes preserve the persisted projection and checkpoints without discovery", async () => { + const originalEnvironment = { + GITHUB_TOKENS: env.GITHUB_TOKENS, + GITHUB_TOKEN: env.GITHUB_TOKEN, + GH_TOKEN: env.GH_TOKEN, + }; + const originalFetch = globalThis.fetch; + try { + delete env.GITHUB_TOKEN; + delete env.GH_TOKEN; + globalThis.fetch = Object.assign( + () => { + throw new Error("Unexpected GitHub request during projection"); + }, + { preconnect: originalFetch.preconnect } + ); + await refreshGitHubWorkUnitProjection(new Date()); + const snapshot = async () => ({ + units: await database + .select({ + identity: schema.githubWorkUnits.identityKey, + membership: schema.githubWorkUnits.membershipDigest, + facts: schema.githubWorkUnits.factsDigest, + }) + .from(schema.githubWorkUnits) + .orderBy(schema.githubWorkUnits.identityKey), + checkpoints: await database + .select() + .from(schema.githubAccountCheckpoints) + .orderBy(schema.githubAccountCheckpoints.account), + commits: await database + .select({ + sha: schema.githubCommits.sha, + author: schema.githubCommits.authorUserId, + }) + .from(schema.githubCommits) + .orderBy(schema.githubCommits.repositoryId, schema.githubCommits.sha), + }); + const before = await snapshot(); + expect(before.units.length).toBeGreaterThan(0); + for (const credentials of [ + { f0rr0: "first", yuppiestechdev: "second" }, + { yuppiestechdev: "second", f0rr0: "rotated" }, + { yuppiestechdev: "second" }, + {}, + ]) { + env.GITHUB_TOKENS = JSON.stringify(credentials); + await refreshGitHubWorkUnitProjection(new Date()); + expect(await snapshot()).toEqual(before); + } + const { syncGitHubAccounts, reconcileGitHubRefs } = + await import("../src/lib/github-commits"); + expect(await syncGitHubAccounts()).toMatchObject({ + accounts: 0, + failedAccounts: [], + }); + expect( + await reconcileGitHubRefs({ + kind: "head", + repositoryLimit: 1, + deadlineAt: Date.now() + 1000, + }) + ).toMatchObject({ accounts: 0, failedAccounts: [] }); + expect(await runGitHubActivityWorker()).toMatchObject({ + commits: { claimed: 0 }, + observations: { claimed: 0 }, + }); + expect(await snapshot()).toEqual(before); + } finally { + Object.assign(env, originalEnvironment); + globalThis.fetch = originalFetch; + } + }); + test("an author-policy rebuild invalidates feed caches and cursors even without changed work units", async () => { + await database + .update(schema.githubPublicFeedHead) + .set({ + summaryPolicyDigest: "0".repeat(64), + projectionRequestToken: null, + }) + .where(eq(schema.githubPublicFeedHead.id, true)); + const [before] = await database.select().from(schema.githubPublicFeedHead); + const token = await ensureGitHubWorkUnitProjectionRequest(); + assert.ok(token !== null); + expect(await completeGitHubWorkUnitProjectionRequest(token)).toBe(true); + const [after] = await database.select().from(schema.githubPublicFeedHead); + expect(after.feedRevision).toBe(before.feedRevision + 1); + expect(after.headContentRevision).toBe(before.headContentRevision + 1); + expect(after.orderingRevision).toBe(before.orderingRevision + 1); + expect(await completeGitHubWorkUnitProjectionRequest(token)).toBe(false); + expect(await ensureGitHubWorkUnitProjectionRequest()).toBeNull(); + }); }); diff --git a/tests/production-database-migration.test.ts b/tests/production-database-migration.test.ts index 697dfc5..eca4d69 100644 --- a/tests/production-database-migration.test.ts +++ b/tests/production-database-migration.test.ts @@ -3,6 +3,7 @@ import { describe, expect, test } from "bun:test"; import { shouldConfigureSupabaseCronForProduction, supabaseCronSiteUrlFrom, + supabaseCronJobsFrom, supabaseCronUrlsFrom, } from "../scripts/configure-supabase-cron.ts"; import { @@ -45,19 +46,19 @@ describe("production Supabase cron URLs", () => { test("uses Vercel's production hostname without a managed site URL", () => { expect( supabaseCronSiteUrlFrom({ - VERCEL_PROJECT_PRODUCTION_URL: "f0rr0.dev", + VERCEL_PROJECT_PRODUCTION_URL: "project.example", }) - ).toBe("https://f0rr0.dev"); - expect(supabaseCronSiteUrlFrom({})).toBe("https://f0rr0.dev"); + ).toBe("https://project.example"); + expect(() => supabaseCronSiteUrlFrom({})).toThrow(); }); test("builds every bounded production endpoint from the site URL", () => { - expect(supabaseCronUrlsFrom("https://f0rr0.dev")).toEqual({ - codexStats: "https://f0rr0.dev/api/cron/codex-stats", - events: "https://f0rr0.dev/api/cron/github-sync", - headRefs: "https://f0rr0.dev/api/cron/github-refs?repositories=8", - summary: "https://f0rr0.dev/api/cron/github-summary", - worker: "https://f0rr0.dev/api/cron/github-worker", + expect(supabaseCronUrlsFrom("https://project.example")).toEqual({ + codexStats: "https://project.example/api/cron/codex-stats", + events: "https://project.example/api/cron/github-sync", + headRefs: "https://project.example/api/cron/github-refs?repositories=8", + summary: "https://project.example/api/cron/github-summary", + worker: "https://project.example/api/cron/github-worker", }); expect(() => supabaseCronUrlsFrom("http://localhost:3000")).toThrow( "HTTPS" @@ -95,3 +96,37 @@ describe("production migration database URL", () => { ).toThrow(ProductionMigrationConfigurationError); }); }); + +const enabledNames = ( + configuration: Parameters[0], + codex = false +) => + supabaseCronJobsFrom(configuration, codex) + .filter((job) => job.enabled) + .map((job) => job.name); + +test("schedules only configured services while retaining disabled jobs for cleanup", () => { + const environment = { VERCEL_PROJECT_PRODUCTION_URL: "example.vercel.app" }; + expect(enabledNames(environment)).toEqual([ + "github-activity-worker-every-five-minutes", + ]); + expect(enabledNames(environment, true)).toEqual([ + "github-activity-worker-every-five-minutes", + "codex-stats-every-fifteen-minutes", + ]); + expect( + enabledNames({ ...environment, OPENAI_API_KEY: "test-key" }) + ).toHaveLength(2); + const github = { + ...environment, + GITHUB_TOKENS: JSON.stringify({ f0rr0: "test-token" }), + }; + expect(enabledNames(github)).toHaveLength(3); + expect( + enabledNames({ ...github, OPENAI_API_KEY: "test-key" }, true) + ).toHaveLength(5); + expect(supabaseCronJobsFrom(environment, false)).toHaveLength(5); + for (const job of supabaseCronJobsFrom(github, true)) { + expect(new URL(job.url).origin).toBe("https://example.vercel.app"); + } +}); diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 0000000..55f8ccd --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,4 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +// Next installs this global in its server runtime; Bun uses the same native implementation. +Object.assign(globalThis, { AsyncLocalStorage }); diff --git a/tests/site-seo.test.ts b/tests/site-seo.test.ts index b132e68..7e35caf 100644 --- a/tests/site-seo.test.ts +++ b/tests/site-seo.test.ts @@ -22,8 +22,10 @@ test("deployment aliases never replace public identity, and only previews receiv ); expect(result.exitCode).toBe(0); const output = JSON.parse(result.stdout.toString()); - expect(output.origin).toBe("https://f0rr0.dev"); - expect(output.article).toBe("https://f0rr0.dev/writing/example"); + expect(output.origin).toBe("https://project-alias.vercel.app"); + expect(output.article).toBe( + "https://project-alias.vercel.app/writing/example" + ); expect(output.redirects).toEqual([ { source: "/blog/:path*", @@ -52,3 +54,67 @@ test("deployment aliases never replace public identity, and only previews receiv ); } }); + +test("an alternate profile drives site identity, structured exports, education and PDF URLs", () => { + const result = Bun.spawnSync( + [ + process.execPath, + "--eval", + ` + const { githubAccounts } = await import("./src/content/site.ts"); + githubAccounts.splice(0, githubAccounts.length, {login: "alice", id: "12345678"}); + const { resumeData, socialProfiles } = await import("./src/content/resume.ts"); + Object.assign(resumeData.person, { + name: "Alice Example", role: "Engineer", email: "alice@example.com", + image: "/alice.png", alternateNames: ["alice"], + address: {city: "London", region: "England", countryCode: "GB"} + }); + const linkedin = socialProfiles.find(profile => profile.network === "LinkedIn"); + Object.assign(linkedin, {username: "alice", url: "https://linkedin.com/in/alice"}); + resumeData.summary = "Builds useful software."; + resumeData.experience.splice(0); + resumeData.education.splice(0, resumeData.education.length, { + company: "Example University", url: "https://university.example", tagline: "Education", + roles: [{title: "Computer Science", dates: "2020 - 2024", location: "London"}] + }); + resumeData.pdf.outputPath = "public/resume/alice.pdf"; + const { siteConfig, resumePdfUrl } = await import("./src/lib/site.ts"); + const { buildJsonResume, buildLlmsTxt } = await import("./src/lib/resume.ts"); + const { buildProfilePageJsonLd } = await import("./src/lib/structured-data.ts"); + console.log(JSON.stringify({siteConfig, resumePdfUrl, resume: buildJsonResume(), profile: buildProfilePageJsonLd(), guide: buildLlmsTxt()})); + `, + ], + { env: { ...process.env, VERCEL_PROJECT_PRODUCTION_URL: "alice.example" } } + ); + expect(result.exitCode).toBe(0); + const output = JSON.parse(result.stdout.toString()); + expect(output.siteConfig).toMatchObject({ + name: "Alice Example", + url: "https://alice.example", + author: { handle: "alice", image: "/alice.png" }, + }); + expect(output.resume.basics).toMatchObject({ + name: "Alice Example", + email: "alice@example.com", + location: { city: "London", countryCode: "GB" }, + }); + expect(output.resume.basics.profiles).toContainEqual({ + network: "GitHub", + username: "alice", + url: "https://github.com/alice", + }); + expect(output.profile.mainEntity).toMatchObject({ + name: "Alice Example", + alternateName: ["alice"], + alumniOf: [ + { + "@type": "EducationalOrganization", + name: "Example University", + sameAs: "https://university.example", + }, + ], + }); + expect(output.resume.education[0].institution).toBe("Example University"); + expect(output.resumePdfUrl).toBe("/resume/alice.pdf"); + expect(output.guide).toContain("https://alice.example/resume/alice.pdf"); +}); diff --git a/tests/site-url.test.ts b/tests/site-url.test.ts new file mode 100644 index 0000000..6a076be --- /dev/null +++ b/tests/site-url.test.ts @@ -0,0 +1,73 @@ +import { expect, test } from "bun:test"; + +import { productionSiteOrigin, siteOriginFrom } from "../src/lib/site-url"; + +test("derives local and production URLs without an author-specific fallback", () => { + expect(siteOriginFrom({})).toBe("http://localhost:3000"); + expect( + siteOriginFrom({ + PORT: "", + NEXT_PUBLIC_PORT: "", + VERCEL_PROJECT_PRODUCTION_URL: "", + }) + ).toBe("http://localhost:3000"); + expect(siteOriginFrom({ PORT: "4200" })).toBe("http://localhost:4200"); + expect( + siteOriginFrom({ + VERCEL: "1", + VERCEL_PROJECT_PRODUCTION_URL: "example.vercel.app", + }) + ).toBe("https://example.vercel.app"); + for (const hostname of [ + undefined, + "", + "localhost", + "https://example.com", + "example.com/path", + "user:password@example.com", + "example.com?target=other", + ]) { + expect(() => productionSiteOrigin(hostname)).toThrow(); + } + expect(() => siteOriginFrom({ VERCEL: "1" })).toThrow(); +}); + +test("browser deployment configuration keeps previews and local development out of analytics", () => { + for (const deployment of ["production", "preview", "development"]) { + const result = Bun.spawnSync( + [ + process.execPath, + "--eval", + ` + globalThis.window = { location: { origin: "http://localhost:4200" } }; + const { env } = await import("./src/env.ts"); + const { CANONICAL_SITE_URL } = await import("./src/lib/site-url.ts"); + const blocked = ["DATABASE_URL", "GITHUB_TOKENS", "VERCEL_PROJECT_PRODUCTION_URL"].every(key => { + try { env[key]; return false; } catch { return true; } + }); + console.log(JSON.stringify({ origin: CANONICAL_SITE_URL, blocked, production: env.NEXT_PUBLIC_VERCEL_ENV === "production", region: env.NEXT_PUBLIC_POSTHOG_REGION })); + `, + ], + { + env: + deployment === "development" + ? {} + : { + NEXT_PUBLIC_VERCEL_ENV: deployment, + NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL: "example.com", + NODE_ENV: "production", + }, + } + ); + expect(result.exitCode).toBe(0); + expect(JSON.parse(result.stdout.toString())).toEqual({ + origin: + deployment === "development" + ? "http://localhost:4200" + : "https://example.com", + production: deployment === "production", + region: "us", + blocked: true, + }); + } +});