{"id":50697265,"url":"https://github.com/linuxfoundation/github-action-gate","last_synced_at":"2026-06-09T07:30:38.405Z","repository":{"id":344024215,"uuid":"1180116135","full_name":"linuxfoundation/github-action-gate","owner":"linuxfoundation","description":"GitHub App that gates GitHub Actions workflows and jobs with ownership attestations — vouch for who owns a workflow before it merges or runs","archived":false,"fork":false,"pushed_at":"2026-03-18T14:30:31.000Z","size":362,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-19T04:48:20.546Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/linuxfoundation.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-03-12T18:04:35.000Z","updated_at":"2026-03-18T14:30:36.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/linuxfoundation/github-action-gate","commit_stats":null,"previous_names":["jordanconway/github-action-gate","linuxfoundation/github-action-gate"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/linuxfoundation/github-action-gate","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/linuxfoundation%2Fgithub-action-gate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/linuxfoundation%2Fgithub-action-gate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/linuxfoundation%2Fgithub-action-gate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/linuxfoundation%2Fgithub-action-gate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/linuxfoundation","download_url":"https://codeload.github.com/linuxfoundation/github-action-gate/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/linuxfoundation%2Fgithub-action-gate/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34096950,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-09T02:00:06.510Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-09T07:30:37.443Z","updated_at":"2026-06-09T07:30:38.400Z","avatar_url":"https://github.com/linuxfoundation.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003c!--\nSPDX-FileCopyrightText: 2026 The Linux Foundation\n\nSPDX-License-Identifier: Apache-2.0\n--\u003e\n\n# Action Gate\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/assets/logo.png\" alt=\"Action Gate Logo\" width=\"160\" /\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cstrong\u003eAction Gate\u003c/strong\u003e is a GitHub App that gates GitHub Actions workflows and jobs with ownership attestations. Before a workflow-modifying pull request can merge — or a workflow can run — the relevant files and jobs must be vouched for by an authorised person or organisation.\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/badge/license-Apache%202.0-blue.svg\" alt=\"License\"/\u003e\u003c/a\u003e\n  \u003ca href=\"package.json\"\u003e\u003cimg src=\"https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg\" alt=\"Node.js\"/\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n---\n\n## Why?\n\nGitHub Actions workflows execute arbitrary code with repository secrets. In large organisations it is easy for a workflow to be added or modified without anyone formally acknowledging ownership or reviewing the supply-chain risk. Action Gate adds a lightweight attestation layer:\n\n- **PR gate** — when a pull request modifies a `.github/workflows/*.yml` file, a check run is posted. The check reports which workflows/jobs have attestations and which do not.\n- **Runtime gate** — when a workflow is triggered, the same check is applied against the head commit. In `block` mode this causes the check run to fail, which can be enforced as a required status check.\n\n---\n\n## Concepts\n\n| Concept | Description |\n| --- | --- |\n| **Attestation** | A record that a specific user or org vouches for a workflow file (or individual job within it) |\n| **Tier** | `user` — self-reported; `organization` — GitHub org membership is verified server-side |\n| **Gate mode** | `audit` (default) — warn only; `block` — fail the check run |\n| **Expiry** | Attestations expire after a configurable number of days (default 180, max 730) |\n\n---\n\n## Stack\n\n- **Runtime**: [Cloudflare Workers](https://workers.cloudflare.com/) with `nodejs_compat`\n- **Framework**: [Express 5](https://expressjs.com/) + [Probot 14](https://probot.github.io/) (context objects built manually for Workers)\n- **Database**: [Prisma 7](https://www.prisma.io/) with `@prisma/adapter-d1` / SQLite (local dev) / [Cloudflare D1](https://developers.cloudflare.com/d1/) (production)\n- **Dashboard**: Vanilla HTML/CSS/JS on [Cloudflare Pages](https://pages.cloudflare.com/)\n- **CI/CD**: Single GitHub Actions workflow — Lint → Test → Deploy (gated on deployable changes)\n\n---\n\n## Quick start\n\n### 1. Prerequisites\n\n- Node.js ≥ 20\n- A GitHub App ([create one](https://github.com/settings/apps/new))\n- (Production) A [Cloudflare account](https://dash.cloudflare.com/sign-up) with Workers and D1 enabled\n\n### 2. Install dependencies\n\n```bash\nnpm install\nnpx prisma generate\nbash scripts/patch-prisma-for-workers.sh\n```\n\nThe patch script rewrites Prisma's generated client for Cloudflare Workers\ncompatibility (static WASM import instead of runtime compilation).\n\n### 3. Configure\n\n```bash\ncp .env.example .env\n# Edit .env with your GitHub App credentials and OAuth client\n```\n\nRequired env vars:\n\n| Variable | Description |\n| --- | --- |\n| `APP_ID` | GitHub App ID |\n| `PRIVATE_KEY` | GitHub App private key (PEM, newlines as `\\n`) |\n| `WEBHOOK_SECRET` | Webhook secret set in GitHub App settings |\n| `DATABASE_URL` | SQLite path for local dev — `file:./prisma/dev.db` |\n| `GITHUB_CLIENT_ID` | OAuth App client ID (for dashboard login) |\n| `GITHUB_CLIENT_SECRET` | OAuth App client secret |\n| `API_BASE_URL` | Public URL of this server (e.g. `https://action-gate.example.com`) |\n| `DASHBOARD_URL` | Public URL of the dashboard |\n| `CORS_ORIGINS` | Comma-separated allowed origins (falls back to `DASHBOARD_URL`; rejects all if neither is set) |\n\n### 4. Run locally\n\n```bash\n# Create the local SQLite database and apply the schema\nnpx prisma migrate dev\n\n# Tunnel webhooks with smee (https://smee.io)\nnpx smee-client --url https://smee.io/\u003cyour-channel\u003e --target http://localhost:3000/api/github/hooks \u0026\n\nnpm run dev\n```\n\nThe dashboard is served at `http://localhost:3000/dashboard` in development mode.\n\n---\n\n## Deploying to Cloudflare Workers\n\nAction Gate runs on Cloudflare Workers with Node.js compatibility and uses Cloudflare D1 as its production database.\n\n### 1. Create the D1 database\n\n```bash\nnpm run d1:create\n# Copy the database_id from the output into wrangler.toml\n```\n\n### 2. Apply migrations to D1\n\n```bash\n# Local D1 environment\nnpm run d1:migrate:local\n\n# Remote (production) D1\nnpm run d1:migrate:remote\n```\n\n### 3. Set secrets\n\n```bash\nwrangler secret put APP_ID\nwrangler secret put WEBHOOK_SECRET\nwrangler secret put GITHUB_CLIENT_ID\nwrangler secret put GITHUB_CLIENT_SECRET\nwrangler secret put API_BASE_URL\nwrangler secret put DASHBOARD_URL\nwrangler secret put CORS_ORIGINS\n```\n\n**Private key** — pipe the PEM file directly to avoid shell quoting issues:\n\n```bash\ncat /path/to/your-app.private-key.pem | wrangler secret put PRIVATE_KEY\n```\n\n\u003e **Important:** Do not wrap the value in quotes. `wrangler secret put` reads\n\u003e from stdin, and any surrounding `\"` characters become part of the stored\n\u003e value, which corrupts the PEM format.\n\u003e\n\u003e The Worker automatically converts PKCS#1 keys (`BEGIN RSA PRIVATE KEY`) to\n\u003e PKCS#8 (`BEGIN PRIVATE KEY`) at runtime, since Cloudflare Workers' Web Crypto\n\u003e API only supports PKCS#8. No manual key conversion is needed.\n\n### 4. Build and deploy\n\n```bash\nnpx prisma generate\nbash scripts/patch-prisma-for-workers.sh\nnpm run build\nnpx wrangler deploy                                          # API worker\nnpx wrangler pages deploy docs --project-name \u003cproject-name\u003e # dashboard\n```\n\n\u003e **Note:** Wrangler reads from `dist-worker/`, not `src/`. Always run\n\u003e `npm run build` after source changes before manual deploys.\n\n---\n\n## CI/CD\n\nA single GitHub Actions workflow (`.github/workflows/ci.yml`) handles everything:\n\n```text\nLint     ──┐\nTest     ──┼── Deploy (main + deployable changes only)\nChanges  ──┘\n```\n\n- **Lint**: ESLint, actionlint, ShellCheck\n- **Test**: TypeScript type-check, Jest (71 tests)\n- **Changes**: Detects if any deployable paths were modified (`src/`, `docs/`, `prisma/`, `scripts/`, `package*`, `tsconfig*`, `wrangler.toml`)\n- **Deploy**: Runs only on `main` when both lint and test pass *and* deployable files changed. Manual `workflow_dispatch` always deploys.\n\nThe deploy step stamps the git SHA into the dashboard footer before the Pages deploy.\n\n---\n\n## GitHub App setup\n\nIn your [GitHub App settings](https://github.com/settings/apps):\n\n**Permissions (Repository)**\n\n- `Actions` — Read \u0026 Write\n- `Checks` — Read \u0026 Write\n- `Contents` — Read-only\n- `Pull requests` — Read-only\n\n**Events to subscribe**\n\n- `Pull request`\n- `Workflow job`\n- `Workflow run`\n\n**Authorization callback URL** (for OAuth dashboard login)\n\n```text\nhttps://your-server.example.com/auth/github/callback\n```\n\n---\n\n## REST API\n\nAll authenticated endpoints require a `Authorization: Bearer \u003cgithub_token\u003e` header.\n\n### Attestation endpoints\n\n| Method | Path | Auth | Description |\n| --- | --- | --- | --- |\n| `GET` | `/api/v1/attestations` | — | List attestations (filterable by owner, repo, workflow, job, voucher, org) |\n| `GET` | `/api/v1/attestations/:id` | — | Get a single attestation |\n| `POST` | `/api/v1/attestations` | ✓ | Create an attestation |\n| `POST` | `/api/v1/attestations/batch` | ✓ | Create up to 50 attestations in one request |\n| `DELETE` | `/api/v1/attestations/:id` | ✓ | Revoke an attestation (owner or repo admin) |\n\n### Repository endpoints\n\n| Method | Path | Auth | Description |\n| --- | --- | --- | --- |\n| `GET` | `/api/v1/repositories` | — | List known repositories |\n| `GET` | `/api/v1/repositories/:owner/:repo` | — | Get one repository |\n| `PUT` | `/api/v1/repositories/:owner/:repo/config` | ✓ admin | Update gate mode / expiry |\n\n### Dashboard endpoints\n\n| Method | Path | Auth | Description |\n| --- | --- | --- | --- |\n| `GET` | `/api/v1/summary` | — | Dashboard summary stats |\n| `GET` | `/api/v1/runs/recent` | — | Recent workflow runs (filterable by owner, repo) |\n| `GET` | `/api/v1/health` | — | Health check |\n\n### OAuth\n\n| Method | Path | Description |\n| --- | --- | --- |\n| `GET` | `/auth/github` | Redirect to GitHub OAuth |\n| `GET` | `/auth/github/callback` | OAuth callback — exchanges code for token |\n\n### Example: create a user-tier attestation\n\n```bash\ncurl -X POST https://your-server.example.com/api/v1/attestations \\\n  -H \"Authorization: Bearer \u003cyour-github-token\u003e\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"repository\":     \"owner/repo\",\n    \"workflow_path\":  \".github/workflows/ci.yml\",\n    \"tier\":           \"user\",\n    \"org_affiliation\": \"Acme Corp\",\n    \"notes\":          \"Owned by the platform team\",\n    \"expiry_days\":    180\n  }'\n```\n\n### Example: enable blocking mode for a repo\n\n```bash\ncurl -X PUT https://your-server.example.com/api/v1/repositories/owner/repo/config \\\n  -H \"Authorization: Bearer \u003cyour-github-token\u003e\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{ \"mode\": \"block\" }'\n```\n\n---\n\n## Dashboard\n\nThe `docs/` directory is a self-contained static site deployed to Cloudflare Pages.\nSet `window.ACTION_GATE_API_URL` in `docs/config.js` to your API base URL.\n\nUsers can log in with their GitHub account via the **Login with GitHub** button\nto create attestations directly from the UI, including batch vouching from\nthe recent workflow runs table.\n\n- **Repositories** (`repositories.html`) — a paginated list of all\n  repositories tracked by Action Gate, showing gate mode, active\n  attestation counts, and expiry settings. Linked from the dashboard\n  stat card.\n- **Revoke** — each active attestation in the table has a Revoke button\n  (visible when logged in). The server enforces that only the original\n  voucher or a repository admin can revoke.\n- **My Attestations** (`my-attestations.html`) — a dedicated page showing\n  all attestations created by the logged-in user, with status filters\n  (all / active / expiring soon), summary stats, and revoke support.\n\n---\n\n## Development\n\n```bash\nnpm run build          # compile TypeScript + copy WASM to dist-worker/\nnpm run dev            # tsc + start probot (local dev)\nnpm run type-check     # type-check without emitting\nnpm run lint           # run ESLint\nnpm test               # run Jest tests\nnpm run prisma:studio  # open Prisma Studio (local SQLite)\nnpm run d1:create      # create Cloudflare D1 database\nnpm run d1:migrate:local   # apply migrations to local D1 environment\nnpm run d1:migrate:remote  # apply migrations to production D1\n```\n\n### Pre-commit hooks\n\nThis project uses [pre-commit](https://pre-commit.com/) to run checks\nautomatically on every commit:\n\n```bash\npip install pre-commit   # if not already installed\npre-commit install       # one-time setup\n```\n\nHooks include: trailing-whitespace, end-of-file-fixer, YAML/JSON validation,\nESLint, markdownlint, ShellCheck, REUSE/SPDX compliance, and actionlint. To run all hooks manually:\n\n```bash\npre-commit run --all-files\n```\n\nYou should also run the full CI check before pushing:\n\n```bash\nnpm run type-check \u0026\u0026 npm run lint \u0026\u0026 npm test\n```\n\n---\n\n## License\n\n[Apache 2.0](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flinuxfoundation%2Fgithub-action-gate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flinuxfoundation%2Fgithub-action-gate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flinuxfoundation%2Fgithub-action-gate/lists"}