From 543032b28d8bf8aa9a2a61beab18cff2cd460b83 Mon Sep 17 00:00:00 2001 From: Nevyana Angelova Date: Fri, 30 Oct 2026 18:19:46 +0700 Subject: [PATCH 1/2] MM-71467 Add Playwright E2E tests: contract-checked fake GitHub on every PR, real GitHub nightly --- .github/workflows/e2e.yml | 204 + build/custom.mk | 27 + e2e/integration/.env.example | 22 + e2e/integration/.gitignore | 6 + e2e/integration/README.md | 59 + e2e/integration/docker-compose.yml | 62 + .../fake-github/contract/operations.json | 62 + .../fake-github/contract/specs/rest.json | 19416 ++++++++++++++++ .../fake-github/contract/specs/schema.graphql | 153 + .../fake-github/contract/update_specs.mjs | 193 + .../fake-github/contract/validator.ts | 220 + e2e/integration/fake-github/server.ts | 821 + e2e/integration/fake-github/state.ts | 467 + e2e/integration/fake-github/webhooks.ts | 128 + e2e/integration/global_setup.ts | 20 + e2e/integration/package-lock.json | 1954 ++ e2e/integration/package.json | 30 + e2e/integration/playwright.config.ts | 49 + e2e/integration/playwright.real.config.ts | 42 + e2e/integration/real-github/api.ts | 57 + e2e/integration/real-github/env.ts | 46 + e2e/integration/real-github/fixture.ts | 45 + e2e/integration/real-github/fixtures.ts | 99 + e2e/integration/real-github/global_setup.ts | 28 + e2e/integration/real-github/journey.spec.ts | 153 + .../real-github/login/github_login.ts | 55 + e2e/integration/real-github/login/totp.ts | 37 + e2e/integration/real-github/rate_limit.ts | 42 + e2e/integration/scripts/visual.sh | 18 + e2e/integration/support/admin.ts | 12 + e2e/integration/support/constants.ts | 43 + e2e/integration/support/fake_github.ts | 48 + e2e/integration/support/fixture.ts | 78 + e2e/integration/support/kv.ts | 105 + e2e/integration/support/layout.ts | 47 + e2e/integration/support/plugin.ts | 94 + e2e/integration/support/seed.ts | 25 + e2e/integration/support/ui.ts | 92 + e2e/integration/support/visual.css | 4 + .../visual.spec.ts/attach-issue-modal.png | Bin 0 -> 20631 bytes .../visual.spec.ts/create-issue-modal.png | Bin 0 -> 34061 bytes .../visual.spec.ts/profile-popover.png | Bin 0 -> 1437 bytes .../visual.spec.ts/reply-thread.png | Bin 0 -> 24959 bytes .../visual.spec.ts/rhs-assignments.png | Bin 0 -> 16203 bytes .../visual.spec.ts/rhs-open-prs.png | Bin 0 -> 25416 bytes .../visual.spec.ts/rhs-review-requests.png | Bin 0 -> 19211 bytes .../visual.spec.ts/rhs-unreads.png | Bin 0 -> 18813 bytes .../visual.spec.ts/sidebar-buttons.png | Bin 0 -> 1795 bytes .../__screenshots__/visual.spec.ts/todo.png | Bin 0 -> 31432 bytes .../visual.spec.ts/tooltip-issue.png | Bin 0 -> 7756 bytes .../visual.spec.ts/tooltip-pull.png | Bin 0 -> 18203 bytes .../visual.spec.ts/webhook-issue-comment.png | Bin 0 -> 12771 bytes .../visual.spec.ts/webhook-merged-pull.png | Bin 0 -> 11640 bytes .../visual.spec.ts/webhook-new-issue.png | Bin 0 -> 11338 bytes .../visual.spec.ts/webhook-new-pull.png | Bin 0 -> 24609 bytes .../visual.spec.ts/webhook-push.png | Bin 0 -> 7971 bytes e2e/integration/tests/connect.spec.ts | 140 + e2e/integration/tests/contract.spec.ts | 206 + e2e/integration/tests/issues.spec.ts | 108 + e2e/integration/tests/sidebar.spec.ts | 78 + e2e/integration/tests/smoke.spec.ts | 16 + e2e/integration/tests/todo.spec.ts | 35 + e2e/integration/tests/ui_layout.spec.ts | 105 + e2e/integration/tests/visual.spec.ts | 191 + e2e/integration/tests/webhooks.spec.ts | 146 + e2e/integration/tsconfig.json | 16 + .../mock_oauth_server/mock_oauth_server.ts | 29 + server/plugin/graphql/client.go | 39 +- server/plugin/plugin.go | 22 +- server/plugin/plugin_test.go | 15 + .../github_repo_selector.jsx | 6 +- 71 files changed, 26191 insertions(+), 24 deletions(-) create mode 100644 .github/workflows/e2e.yml create mode 100644 e2e/integration/.env.example create mode 100644 e2e/integration/.gitignore create mode 100644 e2e/integration/README.md create mode 100644 e2e/integration/docker-compose.yml create mode 100644 e2e/integration/fake-github/contract/operations.json create mode 100644 e2e/integration/fake-github/contract/specs/rest.json create mode 100644 e2e/integration/fake-github/contract/specs/schema.graphql create mode 100644 e2e/integration/fake-github/contract/update_specs.mjs create mode 100644 e2e/integration/fake-github/contract/validator.ts create mode 100644 e2e/integration/fake-github/server.ts create mode 100644 e2e/integration/fake-github/state.ts create mode 100644 e2e/integration/fake-github/webhooks.ts create mode 100644 e2e/integration/global_setup.ts create mode 100644 e2e/integration/package-lock.json create mode 100644 e2e/integration/package.json create mode 100644 e2e/integration/playwright.config.ts create mode 100644 e2e/integration/playwright.real.config.ts create mode 100644 e2e/integration/real-github/api.ts create mode 100644 e2e/integration/real-github/env.ts create mode 100644 e2e/integration/real-github/fixture.ts create mode 100644 e2e/integration/real-github/fixtures.ts create mode 100644 e2e/integration/real-github/global_setup.ts create mode 100644 e2e/integration/real-github/journey.spec.ts create mode 100644 e2e/integration/real-github/login/github_login.ts create mode 100644 e2e/integration/real-github/login/totp.ts create mode 100644 e2e/integration/real-github/rate_limit.ts create mode 100755 e2e/integration/scripts/visual.sh create mode 100644 e2e/integration/support/admin.ts create mode 100644 e2e/integration/support/constants.ts create mode 100644 e2e/integration/support/fake_github.ts create mode 100644 e2e/integration/support/fixture.ts create mode 100644 e2e/integration/support/kv.ts create mode 100644 e2e/integration/support/layout.ts create mode 100644 e2e/integration/support/plugin.ts create mode 100644 e2e/integration/support/seed.ts create mode 100644 e2e/integration/support/ui.ts create mode 100644 e2e/integration/support/visual.css create mode 100644 e2e/integration/tests/__screenshots__/visual.spec.ts/attach-issue-modal.png create mode 100644 e2e/integration/tests/__screenshots__/visual.spec.ts/create-issue-modal.png create mode 100644 e2e/integration/tests/__screenshots__/visual.spec.ts/profile-popover.png create mode 100644 e2e/integration/tests/__screenshots__/visual.spec.ts/reply-thread.png create mode 100644 e2e/integration/tests/__screenshots__/visual.spec.ts/rhs-assignments.png create mode 100644 e2e/integration/tests/__screenshots__/visual.spec.ts/rhs-open-prs.png create mode 100644 e2e/integration/tests/__screenshots__/visual.spec.ts/rhs-review-requests.png create mode 100644 e2e/integration/tests/__screenshots__/visual.spec.ts/rhs-unreads.png create mode 100644 e2e/integration/tests/__screenshots__/visual.spec.ts/sidebar-buttons.png create mode 100644 e2e/integration/tests/__screenshots__/visual.spec.ts/todo.png create mode 100644 e2e/integration/tests/__screenshots__/visual.spec.ts/tooltip-issue.png create mode 100644 e2e/integration/tests/__screenshots__/visual.spec.ts/tooltip-pull.png create mode 100644 e2e/integration/tests/__screenshots__/visual.spec.ts/webhook-issue-comment.png create mode 100644 e2e/integration/tests/__screenshots__/visual.spec.ts/webhook-merged-pull.png create mode 100644 e2e/integration/tests/__screenshots__/visual.spec.ts/webhook-new-issue.png create mode 100644 e2e/integration/tests/__screenshots__/visual.spec.ts/webhook-new-pull.png create mode 100644 e2e/integration/tests/__screenshots__/visual.spec.ts/webhook-push.png create mode 100644 e2e/integration/tests/connect.spec.ts create mode 100644 e2e/integration/tests/contract.spec.ts create mode 100644 e2e/integration/tests/issues.spec.ts create mode 100644 e2e/integration/tests/sidebar.spec.ts create mode 100644 e2e/integration/tests/smoke.spec.ts create mode 100644 e2e/integration/tests/todo.spec.ts create mode 100644 e2e/integration/tests/ui_layout.spec.ts create mode 100644 e2e/integration/tests/visual.spec.ts create mode 100644 e2e/integration/tests/webhooks.spec.ts create mode 100644 e2e/integration/tsconfig.json diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 000000000..f09994798 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,204 @@ +name: e2e +on: + schedule: + - cron: "0 3 * * *" + push: + branches: + - master + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + playwright: + name: playwright (${{ matrix.name }}) + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - name: min-server + image: mattermost/mattermost-enterprise-edition:release-12.0 + - name: master + image: mattermostdevelopment/mattermost-enterprise-edition:master + env: + CI: true + MM_IMAGE: ${{ matrix.image }} + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: | + webapp/package-lock.json + e2e/integration/package-lock.json + + - name: Build E2E plugin bundle + run: make e2e-dist + env: + # Builds only for the runner's OS/arch. + MM_SERVICESETTINGS_ENABLEDEVELOPER: true + + - name: Install e2e dependencies + working-directory: e2e/integration + run: | + npm ci + npx playwright install --with-deps chromium + + - name: Type-check e2e + working-directory: e2e/integration + run: npm run tsc + + - name: Start Mattermost + working-directory: e2e/integration + run: npm run env:up + + - name: Run Playwright + working-directory: e2e/integration + run: npm run test:ci + + - name: Collect server logs + if: failure() + working-directory: e2e/integration + run: mkdir -p playwright-report && docker compose logs mattermost > playwright-report/mattermost.log 2>&1 || true + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report-${{ matrix.name }} + path: e2e/integration/playwright-report + retention-days: 14 + + # Non-blocking: a screenshot diff needs a person to judge whether the change is intended. + visual: + name: playwright visual + runs-on: ubuntu-latest + timeout-minutes: 20 + continue-on-error: true + env: + CI: true + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: | + webapp/package-lock.json + e2e/integration/package-lock.json + + - name: Build E2E plugin bundle + run: make e2e-dist + env: + MM_SERVICESETTINGS_ENABLEDEVELOPER: true + + - name: Install e2e dependencies + working-directory: e2e/integration + run: npm ci + + - name: Compare screenshots + working-directory: e2e/integration + run: npm run test:visual + + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: playwright-visual-diffs + path: | + e2e/integration/playwright-report + e2e/integration/test-results + retention-days: 14 + + contract-drift: + name: fake-github contract drift + if: github.event_name == 'schedule' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + + - name: Compare committed specs with GitHub's published REST description and GraphQL schema + working-directory: e2e/integration + run: npm run contract:check + + real-github: + name: real github.com journey + if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && vars.REAL_GITHUB_E2E_ENABLED == 'true' + runs-on: ubuntu-latest + timeout-minutes: 30 + # The fixtures and hook on the test repository are shared between runs. + concurrency: + group: real-github-e2e + cancel-in-progress: false + env: + CI: true + E2E_GITHUB_OAUTH_CLIENT_ID: ${{ secrets.E2E_GITHUB_OAUTH_CLIENT_ID }} + E2E_GITHUB_OAUTH_CLIENT_SECRET: ${{ secrets.E2E_GITHUB_OAUTH_CLIENT_SECRET }} + E2E_GITHUB_REPO: ${{ secrets.E2E_GITHUB_REPO }} + E2E_GITHUB_MACHINE_TOKEN: ${{ secrets.E2E_GITHUB_MACHINE_TOKEN }} + E2E_GITHUB_LOGIN_USERNAME: ${{ secrets.E2E_GITHUB_LOGIN_USERNAME }} + E2E_GITHUB_LOGIN_PASSWORD: ${{ secrets.E2E_GITHUB_LOGIN_PASSWORD }} + E2E_GITHUB_LOGIN_TOTP_SECRET: ${{ secrets.E2E_GITHUB_LOGIN_TOTP_SECRET }} + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: | + webapp/package-lock.json + e2e/integration/package-lock.json + + - name: Build plugin bundle + run: make dist + env: + MM_SERVICESETTINGS_ENABLEDEVELOPER: true + + - name: Install e2e dependencies + working-directory: e2e/integration + run: | + npm ci + npx playwright install --with-deps chromium + + - name: Start Mattermost + working-directory: e2e/integration + run: npm run env:up + + # No report upload: it would carry real tokens and page content. + - name: Run journey + working-directory: e2e/integration + run: npm run test:real diff --git a/build/custom.mk b/build/custom.mk index c1cb882cd..a5407a8ab 100644 --- a/build/custom.mk +++ b/build/custom.mk @@ -6,3 +6,30 @@ endif .PHONY: deploy-e2e deploy-e2e: dist E2E_TESTING=true PLUGIN_E2E_MOCK_OAUTH_SERVER_URL=http://localhost:8080 ./build/bin/pluginctl deploy $(PLUGIN_ID) dist/$(BUNDLE_NAME) + +E2E_DIR = e2e/integration +E2E_FAKE_GITHUB_URL ?= http://fake-github:8093 + +.PHONY: e2e-dist +e2e-dist: ## Builds dist/ with GitHub OAuth, REST and GraphQL pointed at the E2E fake. Never ship this bundle. + $(MAKE) dist PLUGIN_E2E_MOCK_OAUTH_SERVER_URL=$(E2E_FAKE_GITHUB_URL) + +$(E2E_DIR)/node_modules: $(E2E_DIR)/package-lock.json + cd $(E2E_DIR) && $(NPM) ci && npx playwright install chromium + touch $@ + +.PHONY: e2e-env-up +e2e-env-up: ## Starts the Mattermost stack used by the E2E suite + cd $(E2E_DIR) && docker compose up -d --wait + +.PHONY: e2e-env-down +e2e-env-down: ## Stops the E2E Mattermost stack and drops its data + cd $(E2E_DIR) && docker compose --profile visual down -v + +.PHONY: e2e +e2e: e2e-dist $(E2E_DIR)/node_modules ## Runs the fake-GitHub E2E suite against the stack from e2e-env-up + cd $(E2E_DIR) && npx playwright test + +.PHONY: e2e-real +e2e-real: dist $(E2E_DIR)/node_modules ## Runs the real GitHub journey (needs e2e/integration/.env) + cd $(E2E_DIR) && npm run test:real diff --git a/e2e/integration/.env.example b/e2e/integration/.env.example new file mode 100644 index 000000000..04510236e --- /dev/null +++ b/e2e/integration/.env.example @@ -0,0 +1,22 @@ +# Copy to .env. docker compose and the tests both read it. +# Values for the real lane live in 1Password (search "GitHub plugin E2E"); never paste them into chats, issues or PRs. + +# Stack (optional). Defaults avoid the ports other plugin E2E stacks use. +# MM_PORT=8069 +# PW_BROWSER_PORT=3336 +# MM_IMAGE=mattermostdevelopment/mattermost-enterprise-edition:master + +# Real lane only (npm run test:real). The PR lane needs none of these. +# A classic OAuth app whose callback URL is http://127.0.0.1:${MM_PORT}/plugins/github/oauth/complete +E2E_GITHUB_OAUTH_CLIENT_ID= +E2E_GITHUB_OAUTH_CLIENT_SECRET= +# owner/name of a throwaway repository in a test organization. The machine account needs admin on it. +E2E_GITHUB_REPO= +# OAuth token (or classic personal access token) of the machine account. +# Scopes: repo, notifications, read:org, admin:org_hook, admin:repo_hook +E2E_GITHUB_MACHINE_TOKEN= + +# Optional: the real sign-in test skips without these. The account needs 2FA with an authenticator app. +E2E_GITHUB_LOGIN_USERNAME= +E2E_GITHUB_LOGIN_PASSWORD= +E2E_GITHUB_LOGIN_TOTP_SECRET= diff --git a/e2e/integration/.gitignore b/e2e/integration/.gitignore new file mode 100644 index 000000000..28f0a2d6f --- /dev/null +++ b/e2e/integration/.gitignore @@ -0,0 +1,6 @@ +node_modules +test-results +playwright-report +storage_state +.env +.auth diff --git a/e2e/integration/README.md b/e2e/integration/README.md new file mode 100644 index 000000000..4f340569e --- /dev/null +++ b/e2e/integration/README.md @@ -0,0 +1,59 @@ +# GitHub plugin Playwright tests + +Two lanes. The fake lane runs on every PR, including forks, with no secrets. The real lane talks to github.com and runs nightly only when enabled. + +The older suite in `e2e/playwright` (and `playwright.yml`) is unchanged and still runs against its own mock. + +```sh +make e2e-env-up # Postgres + Mattermost on :8069 +make e2e # builds the E2E bundle (make e2e-dist), installs the plugin, runs the suite +make e2e-env-down + +make e2e-real # normal bundle, against github.com. Needs e2e/integration/.env +``` + +`make e2e-dist` bakes `http://fake-github:8093` into `testOAuthServerURL`, which now moves the OAuth, REST, GraphQL and upload base URLs together, so the github.com code path is what gets tested. The plugin logs a warning on activation. Never ship that bundle. + +Overrides go in the shell or in `e2e/integration/.env` (copy `.env.example`; docker compose reads it too). Real values live in 1Password or GitHub Actions secrets. + +| Variable | Default | Purpose | +|---|---|---| +| `MM_IMAGE` | `mattermostdevelopment/mattermost-enterprise-edition:master` | Server image | +| `MM_PORT` | `8069` | Host port; must match between `e2e-env-up` and `e2e` | +| `PW_BROWSER_PORT` | `3336` | Host port of the dockerized visual browser | +| `PW_HEADLESS` | `true` | `false` to watch the browser | + +## How GitHub is faked + +- `fake-github/server.ts` runs inside the Playwright process on port 8093. Mattermost reaches it as `fake-github` through `extra_hosts`; Chromium through `--host-resolver-rules`. +- It is a classic OAuth app: tokens never expire and have no refresh token. Token errors come back as HTTP 200 with an `error` field, as form or JSON depending on `Accept`. Scopes from earlier grants are merged, and consent is skipped when an earlier grant already covers the request. +- Permissions follow GitHub: private repos need `repo`; repo hooks need repo admin plus `repo` or a `*:repo_hook` scope, otherwise 404; org hooks need org admin plus `admin:org_hook`; notifications need `notifications` or `repo`; issue labels, assignees and milestone are silently dropped without push access; SAML-enforced orgs answer 403 with `X-GitHub-SSO`. +- Creating an issue fires `issues.opened` to matching hooks and creating a hook sends `ping`, signed with `X-Hub-Signature` (HMAC-SHA1) and `X-Hub-Signature-256`. +- Tests drive it through `support/fake_github.ts` (`reset`, `createRepo`, `createItem`, `webhook`, `fault`, `sso`, `revoke`, `requests`, ...). +- Only `connect.spec.ts` drives the browser OAuth flow. Other specs connect with `seedUserInfo()` (`support/kv.ts`), which writes `_githubtoken` with the plugin's AES-CFB cipher. `installPlugin()` also seeds `mm34646_token_reset_done`, or the plugin resets every stored token a minute after it starts. +- Don't use `pw.initSetup()`: it resets server config and disables the plugin. + +## Keeping the fake honest + +Every request and response is checked, and each test fails at teardown if anything broke the contract. + +- **REST:** validated against [github/rest-api-description](https://github.com/github/rest-api-description) (`api.github.com.json`), trimmed to the operations in `fake-github/contract/operations.json`. A route that isn't listed fails the run. +- **GraphQL:** queries are validated and executed against GitHub's [public schema](https://docs.github.com/en/graphql/overview/public-schema), trimmed to the types the plugin uses. graphql-js rejects `... on Issue { state } ... on PullRequest { state }` as a field conflict while GitHub answers it, so that one message is ignored. The real lane checks GitHub still accepts it. +- **Webhooks:** validated against the `x-webhooks` schemas in the same REST description. These replaced [octokit/webhooks](https://github.com/octokit/webhooks), which is deprecated and no longer updated. +- Nightly, `npm run contract:check` fails if GitHub changed the committed specs. `npm run contract:update` refreshes them. + +## Real GitHub + +`npm run test:real` (or `make e2e-real`) needs the variables in `.env.example`. Global setup: + +- Creates long-lived fixtures once and reuses them: an `e2e-fixture` label, an open issue assigned to the machine account, and an open pull request from the `e2e-fixture` branch. It then polls search until both show up, because the plugin finds them through search and the index lags. +- Points a repository hook at a public placeholder URL with a fresh secret. GitHub refuses hooks to private addresses, so tests fetch the recorded delivery from the hook deliveries API and post it to the plugin. +- Seeds the machine account's token into KV, so most tests skip the browser login. + +The sign-in test logs in with password and TOTP, then revokes its grant so the next run starts clean. GitHub skips its emailed device check for accounts with 2FA. + +Writes go through `real-github/rate_limit.ts`, which stays under GitHub's content limits (80 a minute, 500 an hour) across runs on one machine. Retries are off so failures don't repeat writes. Traces, screenshots and video are off when `CI` is set. Don't put account, organization or repository names in this tree. + +## Screenshots + +`npm run test:visual` compares `@visual` shots in a dockerized browser against a pinned server image, in its own throwaway stack, and does not fail the PR job. Update baselines with `npm run test:visual-update-snapshots` and commit the PNGs. diff --git a/e2e/integration/docker-compose.yml b/e2e/integration/docker-compose.yml new file mode 100644 index 000000000..1bd3d7745 --- /dev/null +++ b/e2e/integration/docker-compose.yml @@ -0,0 +1,62 @@ +name: github-plugin-e2e + +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: mmuser + POSTGRES_PASSWORD: mostest + POSTGRES_DB: mattermost_test + healthcheck: + test: ["CMD-SHELL", "pg_isready -U mmuser -d mattermost_test"] + interval: 5s + timeout: 5s + retries: 10 + tmpfs: + - /var/lib/postgresql/data + + mattermost: + image: ${MM_IMAGE:-mattermostdevelopment/mattermost-enterprise-edition:master} + # Server images are published for amd64 only; Apple Silicon runs them under emulation. + platform: linux/amd64 + depends_on: + postgres: + condition: service_healthy + ports: + - "127.0.0.1:${MM_PORT:-8069}:${MM_PORT:-8069}" + - "127.0.0.1:${PW_BROWSER_PORT:-3336}:3333" + extra_hosts: + - "fake-github:host-gateway" + environment: + MM_SQLSETTINGS_DRIVERNAME: postgres + MM_SQLSETTINGS_DATASOURCE: postgres://mmuser:mostest@postgres:5432/mattermost_test?sslmode=disable&connect_timeout=10 + MM_SERVICESETTINGS_SITEURL: http://127.0.0.1:${MM_PORT:-8069} + MM_SERVICESETTINGS_LISTENADDRESS: ":${MM_PORT:-8069}" + MM_SERVICESETTINGS_ENABLELOCALMODE: "true" + MM_PLUGINSETTINGS_ENABLE: "true" + MM_PLUGINSETTINGS_ENABLEUPLOADS: "true" + MM_PLUGINSETTINGS_AUTOMATICPREPACKAGEDPLUGINS: "false" + MM_TEAMSETTINGS_ENABLEOPENSERVER: "true" + MM_ANNOUNCEMENTSETTINGS_ADMINNOTICESENABLED: "false" + MM_ANNOUNCEMENTSETTINGS_USERNOTICESENABLED: "false" + MM_LOGSETTINGS_CONSOLELEVEL: INFO + healthcheck: + test: ["CMD", "/mattermost/bin/mmctl", "--local", "system", "status"] + interval: 5s + timeout: 5s + retries: 60 + + # Shares Mattermost's network so the page origin matches the URL the tests use from the host. + visual-browser: + profiles: [visual] + image: mcr.microsoft.com/playwright:v${PW_VERSION:-1.64.1}-noble + network_mode: service:mattermost + # Chromium rasterizes text differently per CPU architecture; CI runners are amd64. + platform: linux/amd64 + init: true + command: npx -y playwright@${PW_VERSION:-1.64.1} run-server --port 3333 --host 0.0.0.0 + healthcheck: + test: ["CMD", "node", "-e", "require('net').connect(3333, '127.0.0.1').on('connect', () => process.exit(0)).on('error', () => process.exit(1))"] + interval: 2s + timeout: 2s + retries: 60 diff --git a/e2e/integration/fake-github/contract/operations.json b/e2e/integration/fake-github/contract/operations.json new file mode 100644 index 000000000..45cf4f222 --- /dev/null +++ b/e2e/integration/fake-github/contract/operations.json @@ -0,0 +1,62 @@ +{ + "rest": { + "url": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json", + "operations": [ + "GET /user", + "GET /users/{username}", + "GET /user/orgs", + "GET /user/repos", + "GET /users/{username}/repos", + "GET /orgs/{org}", + "GET /orgs/{org}/repos", + "GET /orgs/{org}/members/{username}", + "GET /orgs/{org}/public_members/{username}", + "GET /orgs/{org}/hooks", + "POST /orgs/{org}/hooks", + "GET /repos/{owner}/{repo}", + "GET /repos/{owner}/{repo}/hooks", + "POST /repos/{owner}/{repo}/hooks", + "GET /repos/{owner}/{repo}/issues/{issue_number}", + "POST /repos/{owner}/{repo}/issues", + "POST /repos/{owner}/{repo}/issues/{issue_number}/comments", + "GET /repos/{owner}/{repo}/labels", + "GET /repos/{owner}/{repo}/assignees", + "GET /repos/{owner}/{repo}/milestones", + "GET /repos/{owner}/{repo}/pulls/{pull_number}", + "GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews", + "GET /repos/{owner}/{repo}/commits/{ref}/status", + "GET /search/issues", + "GET /notifications" + ], + "webhooks": [ + "ping", + "pull-request-opened", + "pull-request-closed", + "pull-request-review-requested", + "issues-opened", + "issue-comment-created", + "push" + ] + }, + "graphql": { + "url": "https://docs.github.com/public/fpt/schema.docs.graphql", + "fields": { + "Query": ["search"], + "SearchResultItemConnection": ["issueCount", "nodes", "pageInfo"], + "PageInfo": ["endCursor", "hasNextPage"], + "PullRequest": ["additions", "author", "authorAssociation", "body", "changedFiles", "createdAt", "deletions", "labels", "milestone", "number", "repository", "reviewRequests", "state", "title", "updatedAt", "url"], + "Issue": ["author", "authorAssociation", "body", "createdAt", "labels", "milestone", "number", "repository", "state", "title", "updatedAt", "url"], + "Actor": ["login"], + "User": ["login"], + "Team": ["slug", "organization"], + "Organization": ["login"], + "Repository": ["name", "nameWithOwner", "owner", "url"], + "RepositoryOwner": ["login"], + "LabelConnection": ["nodes"], + "Label": ["color", "name"], + "Milestone": ["title"], + "ReviewRequestConnection": ["nodes"], + "ReviewRequest": ["requestedReviewer"] + } + } +} diff --git a/e2e/integration/fake-github/contract/specs/rest.json b/e2e/integration/fake-github/contract/specs/rest.json new file mode 100644 index 000000000..7c877c9e9 --- /dev/null +++ b/e2e/integration/fake-github/contract/specs/rest.json @@ -0,0 +1,19416 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "GitHub v3 REST API" + }, + "paths": { + "/user": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/private-user" + }, + { + "$ref": "#/components/schemas/public-user" + } + ], + "discriminator": { + "propertyName": "user_view_type", + "mapping": { + "public": "#/components/schemas/public-user", + "private": "#/components/schemas/private-user" + } + } + } + } + } + }, + "304": { + "$ref": "#/components/responses/not_modified" + }, + "401": { + "$ref": "#/components/responses/requires_authentication" + }, + "403": { + "$ref": "#/components/responses/forbidden" + } + } + } + }, + "/users/{username}": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/private-user" + }, + { + "$ref": "#/components/schemas/public-user" + } + ], + "discriminator": { + "propertyName": "user_view_type", + "mapping": { + "public": "#/components/schemas/public-user", + "private": "#/components/schemas/private-user" + } + } + } + } + } + }, + "404": { + "$ref": "#/components/responses/not_found" + } + } + } + }, + "/user/orgs": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/organization-simple" + } + } + } + }, + "headers": { + "Link": { + "$ref": "#/components/headers/link" + } + } + }, + "304": { + "$ref": "#/components/responses/not_modified" + }, + "401": { + "$ref": "#/components/responses/requires_authentication" + }, + "403": { + "$ref": "#/components/responses/forbidden" + } + } + } + }, + "/user/repos": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/repository" + } + } + } + }, + "headers": { + "Link": { + "$ref": "#/components/headers/link" + } + } + }, + "304": { + "$ref": "#/components/responses/not_modified" + }, + "401": { + "$ref": "#/components/responses/requires_authentication" + }, + "403": { + "$ref": "#/components/responses/forbidden" + }, + "422": { + "$ref": "#/components/responses/validation_failed" + } + } + } + }, + "/users/{username}/repos": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/minimal-repository" + } + } + } + }, + "headers": { + "Link": { + "$ref": "#/components/headers/link" + } + } + } + } + } + }, + "/orgs/{org}": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/organization-full" + } + } + } + }, + "404": { + "$ref": "#/components/responses/not_found" + } + } + } + }, + "/orgs/{org}/repos": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/minimal-repository" + } + } + } + }, + "headers": { + "Link": { + "$ref": "#/components/headers/link" + } + } + } + } + } + }, + "/orgs/{org}/members/{username}": { + "get": { + "responses": { + "204": {}, + "302": { + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "404": {} + } + } + }, + "/orgs/{org}/public_members/{username}": { + "get": { + "responses": { + "204": {}, + "404": {} + } + } + }, + "/orgs/{org}/hooks": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/org-hook" + } + } + } + }, + "headers": { + "Link": { + "$ref": "#/components/headers/link" + } + } + }, + "404": { + "$ref": "#/components/responses/not_found" + } + } + }, + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "config": { + "type": "object", + "properties": { + "url": { + "$ref": "#/components/schemas/webhook-config-url" + }, + "content_type": { + "$ref": "#/components/schemas/webhook-config-content-type" + }, + "secret": { + "$ref": "#/components/schemas/webhook-config-secret" + }, + "insecure_ssl": { + "$ref": "#/components/schemas/webhook-config-insecure-ssl" + }, + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "required": [ + "url" + ] + }, + "events": { + "type": "array", + "default": [ + "push" + ], + "items": { + "type": "string" + } + }, + "active": { + "type": "boolean", + "default": true + } + }, + "required": [ + "name", + "config" + ] + } + } + } + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/org-hook" + } + } + }, + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "404": { + "$ref": "#/components/responses/not_found" + }, + "422": { + "$ref": "#/components/responses/validation_failed" + } + } + } + }, + "/repos/{owner}/{repo}": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/full-repository" + } + } + } + }, + "301": { + "$ref": "#/components/responses/moved_permanently" + }, + "403": { + "$ref": "#/components/responses/forbidden" + }, + "404": { + "$ref": "#/components/responses/not_found" + } + } + } + }, + "/repos/{owner}/{repo}/hooks": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/hook" + } + } + } + }, + "headers": { + "Link": { + "$ref": "#/components/headers/link" + } + } + }, + "404": { + "$ref": "#/components/responses/not_found" + } + } + }, + "post": { + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "nullable": true, + "properties": { + "name": { + "type": "string" + }, + "config": { + "type": "object", + "properties": { + "url": { + "$ref": "#/components/schemas/webhook-config-url" + }, + "content_type": { + "$ref": "#/components/schemas/webhook-config-content-type" + }, + "secret": { + "$ref": "#/components/schemas/webhook-config-secret" + }, + "insecure_ssl": { + "$ref": "#/components/schemas/webhook-config-insecure-ssl" + } + } + }, + "events": { + "type": "array", + "default": [ + "push" + ], + "items": { + "type": "string" + } + }, + "active": { + "type": "boolean", + "default": true + } + }, + "additionalProperties": false + } + } + } + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/hook" + } + } + }, + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "$ref": "#/components/responses/forbidden" + }, + "404": { + "$ref": "#/components/responses/not_found" + }, + "422": { + "$ref": "#/components/responses/validation_failed" + } + } + } + }, + "/repos/{owner}/{repo}/issues/{issue_number}": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/issue" + } + } + } + }, + "301": { + "$ref": "#/components/responses/moved_permanently" + }, + "304": { + "$ref": "#/components/responses/not_modified" + }, + "404": { + "$ref": "#/components/responses/not_found" + }, + "410": { + "$ref": "#/components/responses/gone" + } + } + } + }, + "/repos/{owner}/{repo}/issues": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "body": { + "type": "string" + }, + "assignee": { + "type": "string", + "nullable": true + }, + "milestone": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ], + "nullable": true + }, + "labels": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "color": { + "type": "string", + "nullable": true + } + } + } + ] + } + }, + "assignees": { + "type": "array", + "items": { + "type": "string" + } + }, + "issue_field_values": { + "type": "array", + "items": { + "type": "object", + "properties": { + "field_id": { + "type": "integer" + }, + "value": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + "required": [ + "field_id", + "value" + ], + "additionalProperties": false + } + }, + "type": { + "type": "string", + "nullable": true + }, + "parent_issue_id": { + "type": "integer" + } + }, + "required": [ + "title" + ] + } + } + } + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/issue" + } + } + }, + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "$ref": "#/components/responses/bad_request" + }, + "403": { + "$ref": "#/components/responses/forbidden" + }, + "404": { + "$ref": "#/components/responses/not_found" + }, + "410": { + "$ref": "#/components/responses/gone" + }, + "422": { + "$ref": "#/components/responses/validation_failed" + }, + "503": { + "$ref": "#/components/responses/service_unavailable" + } + } + } + }, + "/repos/{owner}/{repo}/issues/{issue_number}/comments": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "body": { + "type": "string" + } + }, + "required": [ + "body" + ] + } + } + } + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/issue-comment" + } + } + }, + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "$ref": "#/components/responses/forbidden" + }, + "404": { + "$ref": "#/components/responses/not_found" + }, + "410": { + "$ref": "#/components/responses/gone" + }, + "422": { + "$ref": "#/components/responses/validation_failed" + } + } + } + }, + "/repos/{owner}/{repo}/labels": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/label" + } + } + } + }, + "headers": { + "Link": { + "$ref": "#/components/headers/link" + } + } + }, + "404": { + "$ref": "#/components/responses/not_found" + } + } + } + }, + "/repos/{owner}/{repo}/assignees": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/simple-user" + } + } + } + }, + "headers": { + "Link": { + "$ref": "#/components/headers/link" + } + } + }, + "404": { + "$ref": "#/components/responses/not_found" + } + } + } + }, + "/repos/{owner}/{repo}/milestones": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/milestone" + } + } + } + }, + "headers": { + "Link": { + "$ref": "#/components/headers/link" + } + } + }, + "404": { + "$ref": "#/components/responses/not_found" + } + } + } + }, + "/repos/{owner}/{repo}/pulls/{pull_number}": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/pull-request" + } + } + } + }, + "304": { + "$ref": "#/components/responses/not_modified" + }, + "404": { + "$ref": "#/components/responses/not_found" + }, + "406": { + "$ref": "#/components/responses/unacceptable" + }, + "422": { + "$ref": "#/components/responses/validation_failed" + }, + "500": { + "$ref": "#/components/responses/internal_error" + }, + "503": { + "$ref": "#/components/responses/service_unavailable" + } + } + } + }, + "/repos/{owner}/{repo}/pulls/{pull_number}/reviews": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pull-request-review" + } + } + } + }, + "headers": { + "Link": { + "$ref": "#/components/headers/link" + } + } + } + } + } + }, + "/repos/{owner}/{repo}/commits/{ref}/status": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/combined-commit-status" + } + } + } + }, + "404": { + "$ref": "#/components/responses/not_found" + } + } + } + }, + "/search/issues": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "total_count", + "incomplete_results", + "items", + "search_type" + ], + "properties": { + "total_count": { + "type": "integer" + }, + "incomplete_results": { + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/issue-search-result-item" + } + }, + "search_type": { + "type": "string", + "enum": [ + "lexical", + "semantic", + "hybrid" + ] + }, + "lexical_fallback_reason": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "no_text_terms", + "quoted_text", + "non_issue_target", + "or_boolean_not_supported", + "no_accessible_repos", + "server_error", + "only_non_semantic_fields_requested", + "service_unavailable" + ] + } + } + } + } + } + } + }, + "304": { + "$ref": "#/components/responses/not_modified" + }, + "401": { + "$ref": "#/components/responses/requires_authentication" + }, + "403": { + "$ref": "#/components/responses/forbidden" + }, + "422": { + "$ref": "#/components/responses/validation_failed" + }, + "503": { + "$ref": "#/components/responses/service_unavailable" + } + } + } + }, + "/notifications": { + "get": { + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/thread" + } + } + } + }, + "headers": { + "Link": { + "$ref": "#/components/headers/link" + } + } + }, + "304": { + "$ref": "#/components/responses/not_modified" + }, + "401": { + "$ref": "#/components/responses/requires_authentication" + }, + "403": { + "$ref": "#/components/responses/forbidden" + }, + "422": { + "$ref": "#/components/responses/validation_failed" + } + } + } + } + }, + "x-webhooks": { + "ping": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/webhook-ping" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/webhook-ping-form-encoded" + } + } + } + } + } + }, + "pull-request-opened": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/webhook-pull-request-opened" + } + } + } + } + } + }, + "pull-request-closed": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/webhook-pull-request-closed" + } + } + } + } + } + }, + "pull-request-review-requested": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/webhook-pull-request-review-requested" + } + } + } + } + } + }, + "issues-opened": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/webhook-issues-opened" + } + } + } + } + } + }, + "issue-comment-created": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/webhook-issue-comment-created" + } + } + } + } + } + }, + "push": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/webhook-push" + } + } + } + } + } + } + }, + "components": { + "headers": { + "link": { + "schema": { + "type": "string" + } + } + }, + "responses": { + "bad_request": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/basic-error" + } + }, + "application/scim+json": { + "schema": { + "$ref": "#/components/schemas/scim-error" + } + } + } + }, + "forbidden": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/basic-error" + } + } + } + }, + "gone": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/basic-error" + } + } + } + }, + "internal_error": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/basic-error" + } + } + } + }, + "moved_permanently": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/basic-error" + } + } + } + }, + "not_found": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/basic-error" + } + } + } + }, + "not_modified": {}, + "requires_authentication": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/basic-error" + } + } + } + }, + "service_unavailable": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "documentation_url": { + "type": "string" + } + } + } + } + } + }, + "unacceptable": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/basic-error" + } + } + } + }, + "validation_failed": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/validation-error" + } + } + } + } + }, + "schemas": { + "author-association": { + "type": "string", + "enum": [ + "COLLABORATOR", + "CONTRIBUTOR", + "FIRST_TIMER", + "FIRST_TIME_CONTRIBUTOR", + "MANNEQUIN", + "MEMBER", + "NONE", + "OWNER" + ] + }, + "auto-merge": { + "type": "object", + "properties": { + "enabled_by": { + "$ref": "#/components/schemas/simple-user" + }, + "merge_method": { + "type": "string", + "enum": [ + "merge", + "squash", + "rebase" + ] + }, + "commit_title": { + "type": "string" + }, + "commit_message": { + "type": "string" + } + }, + "required": [ + "enabled_by", + "merge_method", + "commit_title", + "commit_message" + ], + "nullable": true + }, + "basic-error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "documentation_url": { + "type": "string" + }, + "url": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "code-of-conduct": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "body": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri", + "nullable": true + } + }, + "required": [ + "url", + "html_url", + "key", + "name" + ] + }, + "code-of-conduct-simple": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri" + }, + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "html_url": { + "type": "string", + "nullable": true, + "format": "uri" + } + }, + "required": [ + "url", + "key", + "name", + "html_url" + ] + }, + "combined-commit-status": { + "type": "object", + "properties": { + "state": { + "type": "string" + }, + "statuses": { + "type": "array", + "items": { + "$ref": "#/components/schemas/simple-commit-status" + } + }, + "sha": { + "type": "string" + }, + "total_count": { + "type": "integer" + }, + "repository": { + "$ref": "#/components/schemas/minimal-repository" + }, + "commit_url": { + "type": "string", + "format": "uri" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "state", + "sha", + "total_count", + "statuses", + "repository", + "commit_url", + "url" + ] + }, + "enterprise": { + "type": "object", + "properties": { + "description": { + "type": "string", + "nullable": true + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "website_url": { + "type": "string", + "nullable": true, + "format": "uri" + }, + "id": { + "type": "integer" + }, + "node_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "created_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "updated_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "avatar_url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "id", + "node_id", + "name", + "slug", + "html_url", + "created_at", + "updated_at", + "avatar_url" + ] + }, + "enterprise-webhooks": { + "type": "object", + "properties": { + "description": { + "type": "string", + "nullable": true + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "website_url": { + "type": "string", + "nullable": true, + "format": "uri" + }, + "id": { + "type": "integer" + }, + "node_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "created_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "updated_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "avatar_url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "id", + "node_id", + "name", + "slug", + "html_url", + "created_at", + "updated_at", + "avatar_url" + ] + }, + "full-repository": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "node_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "owner": { + "$ref": "#/components/schemas/simple-user" + }, + "private": { + "type": "boolean" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string", + "nullable": true + }, + "fork": { + "type": "boolean" + }, + "url": { + "type": "string", + "format": "uri" + }, + "archive_url": { + "type": "string" + }, + "assignees_url": { + "type": "string" + }, + "blobs_url": { + "type": "string" + }, + "branches_url": { + "type": "string" + }, + "collaborators_url": { + "type": "string" + }, + "comments_url": { + "type": "string" + }, + "commits_url": { + "type": "string" + }, + "compare_url": { + "type": "string" + }, + "contents_url": { + "type": "string" + }, + "contributors_url": { + "type": "string", + "format": "uri" + }, + "deployments_url": { + "type": "string", + "format": "uri" + }, + "downloads_url": { + "type": "string", + "format": "uri" + }, + "events_url": { + "type": "string", + "format": "uri" + }, + "forks_url": { + "type": "string", + "format": "uri" + }, + "git_commits_url": { + "type": "string" + }, + "git_refs_url": { + "type": "string" + }, + "git_tags_url": { + "type": "string" + }, + "git_url": { + "type": "string" + }, + "issue_comment_url": { + "type": "string" + }, + "issue_events_url": { + "type": "string" + }, + "issues_url": { + "type": "string" + }, + "keys_url": { + "type": "string" + }, + "labels_url": { + "type": "string" + }, + "languages_url": { + "type": "string", + "format": "uri" + }, + "merges_url": { + "type": "string", + "format": "uri" + }, + "milestones_url": { + "type": "string" + }, + "notifications_url": { + "type": "string" + }, + "pulls_url": { + "type": "string" + }, + "releases_url": { + "type": "string" + }, + "ssh_url": { + "type": "string" + }, + "stargazers_url": { + "type": "string", + "format": "uri" + }, + "statuses_url": { + "type": "string" + }, + "subscribers_url": { + "type": "string", + "format": "uri" + }, + "subscription_url": { + "type": "string", + "format": "uri" + }, + "tags_url": { + "type": "string", + "format": "uri" + }, + "teams_url": { + "type": "string", + "format": "uri" + }, + "trees_url": { + "type": "string" + }, + "clone_url": { + "type": "string" + }, + "mirror_url": { + "type": "string", + "format": "uri", + "nullable": true + }, + "hooks_url": { + "type": "string", + "format": "uri" + }, + "svn_url": { + "type": "string", + "format": "uri" + }, + "homepage": { + "type": "string", + "format": "uri", + "nullable": true + }, + "language": { + "type": "string", + "nullable": true + }, + "forks_count": { + "type": "integer" + }, + "stargazers_count": { + "type": "integer" + }, + "watchers_count": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "default_branch": { + "type": "string" + }, + "open_issues_count": { + "type": "integer" + }, + "is_template": { + "type": "boolean" + }, + "topics": { + "type": "array", + "items": { + "type": "string" + } + }, + "has_issues": { + "type": "boolean" + }, + "has_projects": { + "type": "boolean" + }, + "has_wiki": { + "type": "boolean" + }, + "has_pages": { + "type": "boolean" + }, + "has_downloads": { + "type": "boolean" + }, + "has_discussions": { + "type": "boolean" + }, + "has_pull_requests": { + "type": "boolean" + }, + "pull_request_creation_policy": { + "type": "string", + "enum": [ + "all", + "collaborators_only" + ] + }, + "archived": { + "type": "boolean" + }, + "disabled": { + "type": "boolean" + }, + "visibility": { + "type": "string" + }, + "pushed_at": { + "type": "string", + "format": "date-time" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "permissions": { + "type": "object", + "properties": { + "admin": { + "type": "boolean" + }, + "maintain": { + "type": "boolean" + }, + "push": { + "type": "boolean" + }, + "triage": { + "type": "boolean" + }, + "pull": { + "type": "boolean" + } + }, + "required": [ + "admin", + "pull", + "push" + ] + }, + "allow_rebase_merge": { + "type": "boolean" + }, + "template_repository": { + "$ref": "#/components/schemas/nullable-repository" + }, + "temp_clone_token": { + "type": "string", + "nullable": true + }, + "allow_squash_merge": { + "type": "boolean" + }, + "allow_auto_merge": { + "type": "boolean" + }, + "delete_branch_on_merge": { + "type": "boolean" + }, + "allow_merge_commit": { + "type": "boolean" + }, + "allow_update_branch": { + "type": "boolean" + }, + "use_squash_pr_title_as_default": { + "type": "boolean" + }, + "squash_merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "COMMIT_OR_PR_TITLE" + ] + }, + "squash_merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "COMMIT_MESSAGES", + "BLANK" + ] + }, + "merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "MERGE_MESSAGE" + ] + }, + "merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "PR_TITLE", + "BLANK" + ] + }, + "allow_forking": { + "type": "boolean" + }, + "web_commit_signoff_required": { + "type": "boolean" + }, + "subscribers_count": { + "type": "integer" + }, + "network_count": { + "type": "integer" + }, + "license": { + "$ref": "#/components/schemas/nullable-license-simple" + }, + "organization": { + "$ref": "#/components/schemas/nullable-simple-user" + }, + "parent": { + "$ref": "#/components/schemas/repository" + }, + "source": { + "$ref": "#/components/schemas/repository" + }, + "forks": { + "type": "integer" + }, + "master_branch": { + "type": "string" + }, + "open_issues": { + "type": "integer" + }, + "watchers": { + "type": "integer" + }, + "anonymous_access_enabled": { + "default": true, + "type": "boolean" + }, + "code_of_conduct": { + "$ref": "#/components/schemas/code-of-conduct-simple" + }, + "security_and_analysis": { + "$ref": "#/components/schemas/security-and-analysis" + }, + "custom_properties": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "archive_url", + "assignees_url", + "blobs_url", + "branches_url", + "collaborators_url", + "comments_url", + "commits_url", + "compare_url", + "contents_url", + "contributors_url", + "deployments_url", + "description", + "downloads_url", + "events_url", + "fork", + "forks_url", + "full_name", + "git_commits_url", + "git_refs_url", + "git_tags_url", + "hooks_url", + "html_url", + "id", + "node_id", + "issue_comment_url", + "issue_events_url", + "issues_url", + "keys_url", + "labels_url", + "languages_url", + "merges_url", + "milestones_url", + "name", + "notifications_url", + "owner", + "private", + "pulls_url", + "releases_url", + "stargazers_url", + "statuses_url", + "subscribers_url", + "subscription_url", + "tags_url", + "teams_url", + "trees_url", + "url", + "clone_url", + "default_branch", + "forks", + "forks_count", + "git_url", + "has_issues", + "has_projects", + "has_wiki", + "has_pages", + "has_discussions", + "homepage", + "language", + "archived", + "disabled", + "mirror_url", + "open_issues", + "open_issues_count", + "license", + "pushed_at", + "size", + "ssh_url", + "stargazers_count", + "svn_url", + "watchers", + "watchers_count", + "created_at", + "updated_at", + "network_count", + "subscribers_count" + ] + }, + "hook": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "active": { + "type": "boolean" + }, + "events": { + "type": "array", + "items": { + "type": "string" + } + }, + "config": { + "$ref": "#/components/schemas/webhook-config" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "url": { + "type": "string", + "format": "uri" + }, + "test_url": { + "type": "string", + "format": "uri" + }, + "ping_url": { + "type": "string", + "format": "uri" + }, + "deliveries_url": { + "type": "string", + "format": "uri" + }, + "last_response": { + "$ref": "#/components/schemas/hook-response" + } + }, + "required": [ + "id", + "url", + "type", + "name", + "active", + "events", + "config", + "ping_url", + "created_at", + "updated_at", + "last_response", + "test_url" + ] + }, + "hook-response": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "nullable": true + }, + "status": { + "type": "string", + "nullable": true + }, + "message": { + "type": "string", + "nullable": true + } + }, + "required": [ + "code", + "status", + "message" + ] + }, + "issue": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "node_id": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "repository_url": { + "type": "string", + "format": "uri" + }, + "labels_url": { + "type": "string" + }, + "comments_url": { + "type": "string", + "format": "uri" + }, + "events_url": { + "type": "string", + "format": "uri" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "number": { + "type": "integer" + }, + "state": { + "type": "string" + }, + "state_reason": { + "type": "string", + "nullable": true, + "enum": [ + "completed", + "reopened", + "not_planned", + "duplicate" + ] + }, + "title": { + "type": "string" + }, + "body": { + "type": "string", + "nullable": true + }, + "user": { + "$ref": "#/components/schemas/nullable-simple-user" + }, + "labels": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "node_id": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "color": { + "type": "string", + "nullable": true + }, + "default": { + "type": "boolean" + }, + "archived_by": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/simple-user" + } + ] + } + } + } + ] + } + }, + "assignee": { + "$ref": "#/components/schemas/nullable-simple-user" + }, + "assignees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/simple-user" + } + }, + "milestone": { + "$ref": "#/components/schemas/nullable-milestone" + }, + "locked": { + "type": "boolean" + }, + "active_lock_reason": { + "type": "string", + "nullable": true + }, + "comments": { + "type": "integer" + }, + "pull_request": { + "type": "object", + "properties": { + "merged_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "diff_url": { + "type": "string", + "format": "uri", + "nullable": true + }, + "html_url": { + "type": "string", + "format": "uri", + "nullable": true + }, + "patch_url": { + "type": "string", + "format": "uri", + "nullable": true + }, + "url": { + "type": "string", + "format": "uri", + "nullable": true + } + }, + "required": [ + "diff_url", + "html_url", + "patch_url", + "url" + ] + }, + "closed_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "draft": { + "type": "boolean" + }, + "closed_by": { + "$ref": "#/components/schemas/nullable-simple-user" + }, + "body_html": { + "type": "string" + }, + "body_text": { + "type": "string" + }, + "timeline_url": { + "type": "string", + "format": "uri" + }, + "type": { + "$ref": "#/components/schemas/issue-type" + }, + "repository": { + "$ref": "#/components/schemas/repository" + }, + "performed_via_github_app": { + "$ref": "#/components/schemas/nullable-integration" + }, + "author_association": { + "$ref": "#/components/schemas/author-association" + }, + "reactions": { + "$ref": "#/components/schemas/reaction-rollup" + }, + "sub_issues_summary": { + "$ref": "#/components/schemas/sub-issues-summary" + }, + "parent_issue_url": { + "type": "string", + "format": "uri", + "nullable": true + }, + "pinned_comment": { + "$ref": "#/components/schemas/nullable-issue-comment" + }, + "issue_dependencies_summary": { + "$ref": "#/components/schemas/issue-dependencies-summary" + }, + "issue_field_values": { + "type": "array", + "items": { + "$ref": "#/components/schemas/issue-field-value" + } + } + }, + "required": [ + "assignee", + "closed_at", + "comments", + "comments_url", + "events_url", + "html_url", + "id", + "node_id", + "labels", + "labels_url", + "milestone", + "number", + "repository_url", + "state", + "locked", + "title", + "url", + "user", + "created_at", + "updated_at" + ] + }, + "issue-comment": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "node_id": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "body": { + "type": "string" + }, + "body_text": { + "type": "string" + }, + "body_html": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "user": { + "$ref": "#/components/schemas/nullable-simple-user" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "issue_url": { + "type": "string", + "format": "uri" + }, + "author_association": { + "$ref": "#/components/schemas/author-association" + }, + "performed_via_github_app": { + "$ref": "#/components/schemas/nullable-integration" + }, + "reactions": { + "$ref": "#/components/schemas/reaction-rollup" + }, + "pin": { + "$ref": "#/components/schemas/nullable-pinned-issue-comment" + }, + "minimized": { + "$ref": "#/components/schemas/nullable-issue-comment-minimized" + } + }, + "required": [ + "id", + "node_id", + "html_url", + "issue_url", + "user", + "url", + "created_at", + "updated_at" + ] + }, + "issue-dependencies-summary": { + "type": "object", + "properties": { + "blocked_by": { + "type": "integer" + }, + "blocking": { + "type": "integer" + }, + "total_blocked_by": { + "type": "integer" + }, + "total_blocking": { + "type": "integer" + } + }, + "required": [ + "blocked_by", + "blocking", + "total_blocked_by", + "total_blocking" + ] + }, + "issue-field-value": { + "type": "object", + "properties": { + "issue_field_id": { + "type": "integer", + "format": "int64" + }, + "issue_field_name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "data_type": { + "type": "string", + "enum": [ + "text", + "single_select", + "multi_select", + "number", + "date" + ] + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "integer" + } + ], + "nullable": true + }, + "single_select_option": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "color": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "color" + ], + "nullable": true + }, + "multi_select_options": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "color": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "color" + ] + }, + "nullable": true + } + }, + "required": [ + "issue_field_id", + "node_id", + "data_type", + "value" + ] + }, + "issue-search-result-item": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri" + }, + "repository_url": { + "type": "string", + "format": "uri" + }, + "labels_url": { + "type": "string" + }, + "comments_url": { + "type": "string", + "format": "uri" + }, + "events_url": { + "type": "string", + "format": "uri" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "node_id": { + "type": "string" + }, + "number": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "locked": { + "type": "boolean" + }, + "active_lock_reason": { + "type": "string", + "nullable": true + }, + "assignees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/simple-user" + }, + "nullable": true + }, + "user": { + "$ref": "#/components/schemas/nullable-simple-user" + }, + "labels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "node_id": { + "type": "string" + }, + "url": { + "type": "string" + }, + "name": { + "type": "string" + }, + "color": { + "type": "string" + }, + "default": { + "type": "boolean" + }, + "description": { + "type": "string", + "nullable": true + }, + "archived_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "archived_by": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/simple-user" + } + ] + } + } + } + }, + "sub_issues_summary": { + "$ref": "#/components/schemas/sub-issues-summary" + }, + "issue_dependencies_summary": { + "$ref": "#/components/schemas/issue-dependencies-summary" + }, + "issue_field_values": { + "type": "array", + "items": { + "$ref": "#/components/schemas/issue-field-value" + } + }, + "state": { + "type": "string" + }, + "state_reason": { + "type": "string", + "nullable": true + }, + "assignee": { + "$ref": "#/components/schemas/nullable-simple-user" + }, + "milestone": { + "$ref": "#/components/schemas/nullable-milestone" + }, + "comments": { + "type": "integer" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "closed_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "text_matches": { + "$ref": "#/components/schemas/search-result-text-matches" + }, + "pull_request": { + "type": "object", + "properties": { + "merged_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "diff_url": { + "type": "string", + "format": "uri", + "nullable": true + }, + "html_url": { + "type": "string", + "format": "uri", + "nullable": true + }, + "patch_url": { + "type": "string", + "format": "uri", + "nullable": true + }, + "url": { + "type": "string", + "format": "uri", + "nullable": true + } + }, + "required": [ + "diff_url", + "html_url", + "patch_url", + "url" + ] + }, + "body": { + "type": "string" + }, + "score": { + "type": "number" + }, + "author_association": { + "$ref": "#/components/schemas/author-association" + }, + "draft": { + "type": "boolean" + }, + "repository": { + "$ref": "#/components/schemas/repository" + }, + "body_html": { + "type": "string" + }, + "body_text": { + "type": "string" + }, + "timeline_url": { + "type": "string", + "format": "uri" + }, + "type": { + "$ref": "#/components/schemas/issue-type" + }, + "performed_via_github_app": { + "$ref": "#/components/schemas/nullable-integration" + }, + "pinned_comment": { + "$ref": "#/components/schemas/nullable-issue-comment" + }, + "reactions": { + "$ref": "#/components/schemas/reaction-rollup" + } + }, + "required": [ + "assignee", + "closed_at", + "comments", + "comments_url", + "events_url", + "html_url", + "id", + "node_id", + "labels", + "labels_url", + "milestone", + "number", + "repository_url", + "state", + "locked", + "title", + "url", + "user", + "author_association", + "created_at", + "updated_at", + "score" + ] + }, + "issue-type": { + "type": "object", + "nullable": true, + "properties": { + "id": { + "type": "integer" + }, + "node_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "color": { + "type": "string", + "enum": [ + "gray", + "blue", + "green", + "yellow", + "orange", + "red", + "pink", + "purple" + ], + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "is_enabled": { + "type": "boolean" + } + }, + "required": [ + "id", + "node_id", + "name", + "description" + ] + }, + "label": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "node_id": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "color": { + "type": "string" + }, + "default": { + "type": "boolean" + }, + "archived_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "archived_by": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/simple-user" + } + ] + } + }, + "required": [ + "id", + "node_id", + "url", + "name", + "description", + "color", + "default", + "archived_at", + "archived_by" + ] + }, + "link": { + "type": "object", + "properties": { + "href": { + "type": "string" + } + }, + "required": [ + "href" + ] + }, + "milestone": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "labels_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "node_id": { + "type": "string" + }, + "number": { + "type": "integer" + }, + "state": { + "type": "string", + "enum": [ + "open", + "closed" + ], + "default": "open" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "creator": { + "$ref": "#/components/schemas/nullable-simple-user" + }, + "open_issues": { + "type": "integer" + }, + "closed_issues": { + "type": "integer" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "closed_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "due_on": { + "type": "string", + "format": "date-time", + "nullable": true + } + }, + "required": [ + "closed_issues", + "creator", + "description", + "due_on", + "closed_at", + "id", + "node_id", + "labels_url", + "html_url", + "number", + "open_issues", + "state", + "title", + "url", + "created_at", + "updated_at" + ] + }, + "minimal-repository": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "node_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "owner": { + "$ref": "#/components/schemas/simple-user" + }, + "private": { + "type": "boolean" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string", + "nullable": true + }, + "fork": { + "type": "boolean" + }, + "url": { + "type": "string", + "format": "uri" + }, + "archive_url": { + "type": "string" + }, + "assignees_url": { + "type": "string" + }, + "blobs_url": { + "type": "string" + }, + "branches_url": { + "type": "string" + }, + "collaborators_url": { + "type": "string" + }, + "comments_url": { + "type": "string" + }, + "commits_url": { + "type": "string" + }, + "compare_url": { + "type": "string" + }, + "contents_url": { + "type": "string" + }, + "contributors_url": { + "type": "string", + "format": "uri" + }, + "deployments_url": { + "type": "string", + "format": "uri" + }, + "downloads_url": { + "type": "string", + "format": "uri" + }, + "events_url": { + "type": "string", + "format": "uri" + }, + "forks_url": { + "type": "string", + "format": "uri" + }, + "git_commits_url": { + "type": "string" + }, + "git_refs_url": { + "type": "string" + }, + "git_tags_url": { + "type": "string" + }, + "git_url": { + "type": "string" + }, + "issue_comment_url": { + "type": "string" + }, + "issue_events_url": { + "type": "string" + }, + "issues_url": { + "type": "string" + }, + "keys_url": { + "type": "string" + }, + "labels_url": { + "type": "string" + }, + "languages_url": { + "type": "string", + "format": "uri" + }, + "merges_url": { + "type": "string", + "format": "uri" + }, + "milestones_url": { + "type": "string" + }, + "notifications_url": { + "type": "string" + }, + "pulls_url": { + "type": "string" + }, + "releases_url": { + "type": "string" + }, + "ssh_url": { + "type": "string" + }, + "stargazers_url": { + "type": "string", + "format": "uri" + }, + "statuses_url": { + "type": "string" + }, + "subscribers_url": { + "type": "string", + "format": "uri" + }, + "subscription_url": { + "type": "string", + "format": "uri" + }, + "tags_url": { + "type": "string", + "format": "uri" + }, + "teams_url": { + "type": "string", + "format": "uri" + }, + "trees_url": { + "type": "string" + }, + "clone_url": { + "type": "string" + }, + "mirror_url": { + "type": "string", + "nullable": true + }, + "hooks_url": { + "type": "string", + "format": "uri" + }, + "svn_url": { + "type": "string" + }, + "homepage": { + "type": "string", + "nullable": true + }, + "language": { + "type": "string", + "nullable": true + }, + "forks_count": { + "type": "integer" + }, + "stargazers_count": { + "type": "integer" + }, + "watchers_count": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "default_branch": { + "type": "string" + }, + "open_issues_count": { + "type": "integer" + }, + "is_template": { + "type": "boolean" + }, + "topics": { + "type": "array", + "items": { + "type": "string" + } + }, + "has_issues": { + "type": "boolean" + }, + "has_projects": { + "type": "boolean" + }, + "has_wiki": { + "type": "boolean" + }, + "has_pages": { + "type": "boolean" + }, + "has_downloads": { + "type": "boolean" + }, + "has_discussions": { + "type": "boolean" + }, + "has_pull_requests": { + "type": "boolean" + }, + "pull_request_creation_policy": { + "type": "string", + "enum": [ + "all", + "collaborators_only" + ] + }, + "archived": { + "type": "boolean" + }, + "disabled": { + "type": "boolean" + }, + "visibility": { + "type": "string" + }, + "pushed_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "updated_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "permissions": { + "type": "object", + "properties": { + "admin": { + "type": "boolean" + }, + "maintain": { + "type": "boolean" + }, + "push": { + "type": "boolean" + }, + "triage": { + "type": "boolean" + }, + "pull": { + "type": "boolean" + } + } + }, + "role_name": { + "type": "string" + }, + "temp_clone_token": { + "type": "string" + }, + "delete_branch_on_merge": { + "type": "boolean" + }, + "subscribers_count": { + "type": "integer" + }, + "network_count": { + "type": "integer" + }, + "code_of_conduct": { + "$ref": "#/components/schemas/code-of-conduct" + }, + "license": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "spdx_id": { + "type": "string" + }, + "url": { + "type": "string", + "nullable": true + }, + "node_id": { + "type": "string" + } + }, + "nullable": true + }, + "forks": { + "type": "integer" + }, + "open_issues": { + "type": "integer" + }, + "watchers": { + "type": "integer" + }, + "allow_forking": { + "type": "boolean" + }, + "web_commit_signoff_required": { + "type": "boolean" + }, + "security_and_analysis": { + "$ref": "#/components/schemas/security-and-analysis" + }, + "custom_properties": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "archive_url", + "assignees_url", + "blobs_url", + "branches_url", + "collaborators_url", + "comments_url", + "commits_url", + "compare_url", + "contents_url", + "contributors_url", + "deployments_url", + "description", + "downloads_url", + "events_url", + "fork", + "forks_url", + "full_name", + "git_commits_url", + "git_refs_url", + "git_tags_url", + "hooks_url", + "html_url", + "id", + "node_id", + "issue_comment_url", + "issue_events_url", + "issues_url", + "keys_url", + "labels_url", + "languages_url", + "merges_url", + "milestones_url", + "name", + "notifications_url", + "owner", + "private", + "pulls_url", + "releases_url", + "stargazers_url", + "statuses_url", + "subscribers_url", + "subscription_url", + "tags_url", + "teams_url", + "trees_url", + "url" + ] + }, + "nullable-integration": { + "type": "object", + "nullable": true, + "properties": { + "id": { + "type": "integer" + }, + "slug": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "client_id": { + "type": "string" + }, + "owner": { + "oneOf": [ + { + "$ref": "#/components/schemas/simple-user" + }, + { + "$ref": "#/components/schemas/enterprise" + } + ] + }, + "name": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "external_url": { + "type": "string", + "format": "uri" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "permissions": { + "type": "object", + "properties": { + "issues": { + "type": "string" + }, + "checks": { + "type": "string" + }, + "metadata": { + "type": "string" + }, + "contents": { + "type": "string" + }, + "deployments": { + "type": "string" + } + }, + "additionalProperties": { + "type": "string" + } + }, + "events": { + "type": "array", + "items": { + "type": "string" + } + }, + "installations_count": { + "type": "integer" + } + }, + "required": [ + "id", + "node_id", + "owner", + "name", + "description", + "external_url", + "html_url", + "created_at", + "updated_at", + "permissions", + "events" + ] + }, + "nullable-issue-comment": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "node_id": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "body": { + "type": "string" + }, + "body_text": { + "type": "string" + }, + "body_html": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "user": { + "$ref": "#/components/schemas/nullable-simple-user" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "issue_url": { + "type": "string", + "format": "uri" + }, + "author_association": { + "$ref": "#/components/schemas/author-association" + }, + "performed_via_github_app": { + "$ref": "#/components/schemas/nullable-integration" + }, + "reactions": { + "$ref": "#/components/schemas/reaction-rollup" + }, + "pin": { + "$ref": "#/components/schemas/nullable-pinned-issue-comment" + }, + "minimized": { + "$ref": "#/components/schemas/nullable-issue-comment-minimized" + } + }, + "required": [ + "id", + "node_id", + "html_url", + "issue_url", + "user", + "url", + "created_at", + "updated_at" + ], + "nullable": true + }, + "nullable-issue-comment-minimized": { + "type": "object", + "properties": { + "reason": { + "type": "string", + "nullable": true + } + }, + "required": [ + "reason" + ], + "nullable": true + }, + "nullable-license-simple": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "url": { + "type": "string", + "nullable": true, + "format": "uri" + }, + "spdx_id": { + "type": "string", + "nullable": true + }, + "node_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "key", + "name", + "url", + "spdx_id", + "node_id" + ], + "nullable": true + }, + "nullable-milestone": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "labels_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "node_id": { + "type": "string" + }, + "number": { + "type": "integer" + }, + "state": { + "type": "string", + "enum": [ + "open", + "closed" + ], + "default": "open" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "creator": { + "$ref": "#/components/schemas/nullable-simple-user" + }, + "open_issues": { + "type": "integer" + }, + "closed_issues": { + "type": "integer" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "closed_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "due_on": { + "type": "string", + "format": "date-time", + "nullable": true + } + }, + "required": [ + "closed_issues", + "creator", + "description", + "due_on", + "closed_at", + "id", + "node_id", + "labels_url", + "html_url", + "number", + "open_issues", + "state", + "title", + "url", + "created_at", + "updated_at" + ], + "nullable": true + }, + "nullable-pinned-issue-comment": { + "type": "object", + "properties": { + "pinned_at": { + "type": "string", + "format": "date-time" + }, + "pinned_by": { + "$ref": "#/components/schemas/nullable-simple-user" + } + }, + "required": [ + "pinned_at", + "pinned_by" + ], + "nullable": true + }, + "nullable-repository": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "node_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "license": { + "$ref": "#/components/schemas/nullable-license-simple" + }, + "forks": { + "type": "integer" + }, + "permissions": { + "type": "object", + "properties": { + "admin": { + "type": "boolean" + }, + "pull": { + "type": "boolean" + }, + "triage": { + "type": "boolean" + }, + "push": { + "type": "boolean" + }, + "maintain": { + "type": "boolean" + } + }, + "required": [ + "admin", + "pull", + "push" + ] + }, + "owner": { + "$ref": "#/components/schemas/simple-user" + }, + "private": { + "default": false, + "type": "boolean" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string", + "nullable": true + }, + "fork": { + "type": "boolean" + }, + "url": { + "type": "string", + "format": "uri" + }, + "archive_url": { + "type": "string" + }, + "assignees_url": { + "type": "string" + }, + "blobs_url": { + "type": "string" + }, + "branches_url": { + "type": "string" + }, + "collaborators_url": { + "type": "string" + }, + "comments_url": { + "type": "string" + }, + "commits_url": { + "type": "string" + }, + "compare_url": { + "type": "string" + }, + "contents_url": { + "type": "string" + }, + "contributors_url": { + "type": "string", + "format": "uri" + }, + "deployments_url": { + "type": "string", + "format": "uri" + }, + "downloads_url": { + "type": "string", + "format": "uri" + }, + "events_url": { + "type": "string", + "format": "uri" + }, + "forks_url": { + "type": "string", + "format": "uri" + }, + "git_commits_url": { + "type": "string" + }, + "git_refs_url": { + "type": "string" + }, + "git_tags_url": { + "type": "string" + }, + "git_url": { + "type": "string" + }, + "issue_comment_url": { + "type": "string" + }, + "issue_events_url": { + "type": "string" + }, + "issues_url": { + "type": "string" + }, + "keys_url": { + "type": "string" + }, + "labels_url": { + "type": "string" + }, + "languages_url": { + "type": "string", + "format": "uri" + }, + "merges_url": { + "type": "string", + "format": "uri" + }, + "milestones_url": { + "type": "string" + }, + "notifications_url": { + "type": "string" + }, + "pulls_url": { + "type": "string" + }, + "releases_url": { + "type": "string" + }, + "ssh_url": { + "type": "string" + }, + "stargazers_url": { + "type": "string", + "format": "uri" + }, + "statuses_url": { + "type": "string" + }, + "subscribers_url": { + "type": "string", + "format": "uri" + }, + "subscription_url": { + "type": "string", + "format": "uri" + }, + "tags_url": { + "type": "string", + "format": "uri" + }, + "teams_url": { + "type": "string", + "format": "uri" + }, + "trees_url": { + "type": "string" + }, + "clone_url": { + "type": "string" + }, + "mirror_url": { + "type": "string", + "format": "uri", + "nullable": true + }, + "hooks_url": { + "type": "string", + "format": "uri" + }, + "svn_url": { + "type": "string", + "format": "uri" + }, + "homepage": { + "type": "string", + "format": "uri", + "nullable": true + }, + "language": { + "type": "string", + "nullable": true + }, + "forks_count": { + "type": "integer" + }, + "stargazers_count": { + "type": "integer" + }, + "watchers_count": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "default_branch": { + "type": "string" + }, + "open_issues_count": { + "type": "integer" + }, + "is_template": { + "default": false, + "type": "boolean" + }, + "topics": { + "type": "array", + "items": { + "type": "string" + } + }, + "has_issues": { + "default": true, + "type": "boolean" + }, + "has_projects": { + "default": true, + "type": "boolean" + }, + "has_wiki": { + "default": true, + "type": "boolean" + }, + "has_pages": { + "type": "boolean" + }, + "has_downloads": { + "default": true, + "type": "boolean", + "deprecated": true + }, + "has_discussions": { + "default": false, + "type": "boolean" + }, + "has_pull_requests": { + "default": true, + "type": "boolean" + }, + "pull_request_creation_policy": { + "type": "string", + "enum": [ + "all", + "collaborators_only" + ] + }, + "archived": { + "default": false, + "type": "boolean" + }, + "disabled": { + "type": "boolean" + }, + "visibility": { + "default": "public", + "type": "string" + }, + "pushed_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "updated_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "allow_rebase_merge": { + "default": true, + "type": "boolean" + }, + "temp_clone_token": { + "type": "string" + }, + "allow_squash_merge": { + "default": true, + "type": "boolean" + }, + "allow_auto_merge": { + "default": false, + "type": "boolean" + }, + "delete_branch_on_merge": { + "default": false, + "type": "boolean" + }, + "allow_update_branch": { + "default": false, + "type": "boolean" + }, + "use_squash_pr_title_as_default": { + "type": "boolean", + "default": false, + "deprecated": true + }, + "squash_merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "COMMIT_OR_PR_TITLE" + ] + }, + "squash_merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "COMMIT_MESSAGES", + "BLANK" + ] + }, + "merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "MERGE_MESSAGE" + ] + }, + "merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "PR_TITLE", + "BLANK" + ] + }, + "allow_merge_commit": { + "default": true, + "type": "boolean" + }, + "allow_forking": { + "type": "boolean" + }, + "web_commit_signoff_required": { + "default": false, + "type": "boolean" + }, + "open_issues": { + "type": "integer" + }, + "watchers": { + "type": "integer" + }, + "master_branch": { + "type": "string" + }, + "starred_at": { + "type": "string" + }, + "anonymous_access_enabled": { + "type": "boolean" + }, + "code_search_index_status": { + "type": "object", + "properties": { + "lexical_search_ok": { + "type": "boolean" + }, + "lexical_commit_sha": { + "type": "string" + } + } + } + }, + "required": [ + "archive_url", + "assignees_url", + "blobs_url", + "branches_url", + "collaborators_url", + "comments_url", + "commits_url", + "compare_url", + "contents_url", + "contributors_url", + "deployments_url", + "description", + "downloads_url", + "events_url", + "fork", + "forks_url", + "full_name", + "git_commits_url", + "git_refs_url", + "git_tags_url", + "hooks_url", + "html_url", + "id", + "node_id", + "issue_comment_url", + "issue_events_url", + "issues_url", + "keys_url", + "labels_url", + "languages_url", + "merges_url", + "milestones_url", + "name", + "notifications_url", + "owner", + "private", + "pulls_url", + "releases_url", + "stargazers_url", + "statuses_url", + "subscribers_url", + "subscription_url", + "tags_url", + "teams_url", + "trees_url", + "url", + "clone_url", + "default_branch", + "forks", + "forks_count", + "git_url", + "has_downloads", + "has_issues", + "has_projects", + "has_wiki", + "has_pages", + "homepage", + "language", + "archived", + "disabled", + "mirror_url", + "open_issues", + "open_issues_count", + "license", + "pushed_at", + "size", + "ssh_url", + "stargazers_count", + "svn_url", + "watchers", + "watchers_count", + "created_at", + "updated_at" + ], + "nullable": true + }, + "nullable-simple-user": { + "type": "object", + "properties": { + "name": { + "nullable": true, + "type": "string" + }, + "email": { + "nullable": true, + "type": "string" + }, + "login": { + "type": "string" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "node_id": { + "type": "string" + }, + "avatar_url": { + "type": "string", + "format": "uri" + }, + "gravatar_id": { + "type": "string", + "nullable": true + }, + "url": { + "type": "string", + "format": "uri" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string" + }, + "gists_url": { + "type": "string" + }, + "starred_url": { + "type": "string" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "events_url": { + "type": "string" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string" + }, + "site_admin": { + "type": "boolean" + }, + "starred_at": { + "type": "string" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "avatar_url", + "events_url", + "followers_url", + "following_url", + "gists_url", + "gravatar_id", + "html_url", + "id", + "node_id", + "login", + "organizations_url", + "received_events_url", + "repos_url", + "site_admin", + "starred_url", + "subscriptions_url", + "type", + "url" + ], + "nullable": true + }, + "org-hook": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "url": { + "type": "string", + "format": "uri" + }, + "ping_url": { + "type": "string", + "format": "uri" + }, + "deliveries_url": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + }, + "events": { + "type": "array", + "items": { + "type": "string" + } + }, + "active": { + "type": "boolean" + }, + "config": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "insecure_ssl": { + "type": "string" + }, + "content_type": { + "type": "string" + }, + "secret": { + "type": "string" + } + } + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "type": { + "type": "string" + } + }, + "required": [ + "id", + "url", + "type", + "name", + "active", + "events", + "config", + "ping_url", + "created_at", + "updated_at" + ] + }, + "organization-full": { + "type": "object", + "properties": { + "login": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "node_id": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "events_url": { + "type": "string", + "format": "uri" + }, + "hooks_url": { + "type": "string" + }, + "issues_url": { + "type": "string" + }, + "members_url": { + "type": "string" + }, + "public_members_url": { + "type": "string" + }, + "avatar_url": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string" + }, + "company": { + "type": "string", + "nullable": true + }, + "blog": { + "type": "string", + "format": "uri", + "nullable": true + }, + "location": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string", + "format": "email", + "nullable": true + }, + "twitter_username": { + "type": "string", + "nullable": true + }, + "is_verified": { + "type": "boolean" + }, + "has_organization_projects": { + "type": "boolean" + }, + "has_repository_projects": { + "type": "boolean" + }, + "public_repos": { + "type": "integer" + }, + "public_gists": { + "type": "integer" + }, + "followers": { + "type": "integer" + }, + "following": { + "type": "integer" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string" + }, + "total_private_repos": { + "type": "integer" + }, + "owned_private_repos": { + "type": "integer" + }, + "private_gists": { + "type": "integer", + "nullable": true + }, + "disk_usage": { + "type": "integer", + "nullable": true + }, + "collaborators": { + "type": "integer", + "nullable": true + }, + "billing_email": { + "type": "string", + "format": "email", + "nullable": true + }, + "plan": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "space": { + "type": "integer" + }, + "private_repos": { + "type": "integer" + }, + "filled_seats": { + "type": "integer" + }, + "seats": { + "type": "integer" + } + }, + "required": [ + "name", + "space", + "private_repos" + ] + }, + "default_repository_permission": { + "type": "string", + "nullable": true + }, + "default_repository_branch": { + "type": "string", + "nullable": true + }, + "members_can_create_repositories": { + "type": "boolean", + "nullable": true + }, + "two_factor_requirement_enabled": { + "type": "boolean", + "nullable": true + }, + "members_allowed_repository_creation_type": { + "type": "string" + }, + "members_can_create_public_repositories": { + "type": "boolean" + }, + "members_can_create_private_repositories": { + "type": "boolean" + }, + "members_can_create_internal_repositories": { + "type": "boolean" + }, + "members_can_create_pages": { + "type": "boolean" + }, + "members_can_create_public_pages": { + "type": "boolean" + }, + "members_can_create_private_pages": { + "type": "boolean" + }, + "members_can_delete_repositories": { + "type": "boolean" + }, + "members_can_change_repo_visibility": { + "type": "boolean" + }, + "members_can_invite_outside_collaborators": { + "type": "boolean" + }, + "members_can_delete_issues": { + "type": "boolean" + }, + "display_commenter_full_name_setting_enabled": { + "type": "boolean" + }, + "readers_can_create_discussions": { + "type": "boolean" + }, + "members_can_create_teams": { + "type": "boolean" + }, + "members_can_view_dependency_insights": { + "type": "boolean" + }, + "members_can_fork_private_repositories": { + "type": "boolean", + "nullable": true + }, + "web_commit_signoff_required": { + "type": "boolean" + }, + "advanced_security_enabled_for_new_repositories": { + "type": "boolean", + "deprecated": true + }, + "dependabot_alerts_enabled_for_new_repositories": { + "type": "boolean", + "deprecated": true + }, + "dependabot_security_updates_enabled_for_new_repositories": { + "type": "boolean", + "deprecated": true + }, + "dependency_graph_enabled_for_new_repositories": { + "type": "boolean", + "deprecated": true + }, + "secret_scanning_enabled_for_new_repositories": { + "type": "boolean", + "deprecated": true + }, + "secret_scanning_push_protection_enabled_for_new_repositories": { + "type": "boolean", + "deprecated": true + }, + "secret_scanning_push_protection_custom_link_enabled": { + "type": "boolean" + }, + "secret_scanning_push_protection_custom_link": { + "type": "string", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "archived_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "deploy_keys_enabled_for_repositories": { + "type": "boolean" + } + }, + "required": [ + "login", + "url", + "id", + "node_id", + "repos_url", + "events_url", + "hooks_url", + "issues_url", + "members_url", + "public_members_url", + "avatar_url", + "description", + "html_url", + "has_organization_projects", + "has_repository_projects", + "public_repos", + "public_gists", + "followers", + "following", + "type", + "created_at", + "updated_at", + "archived_at" + ] + }, + "organization-simple": { + "type": "object", + "properties": { + "login": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "node_id": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "events_url": { + "type": "string", + "format": "uri" + }, + "hooks_url": { + "type": "string" + }, + "issues_url": { + "type": "string" + }, + "members_url": { + "type": "string" + }, + "public_members_url": { + "type": "string" + }, + "avatar_url": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + } + }, + "required": [ + "login", + "url", + "id", + "node_id", + "repos_url", + "events_url", + "hooks_url", + "issues_url", + "members_url", + "public_members_url", + "avatar_url", + "description" + ] + }, + "organization-simple-webhooks": { + "type": "object", + "properties": { + "login": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "node_id": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "events_url": { + "type": "string", + "format": "uri" + }, + "hooks_url": { + "type": "string" + }, + "issues_url": { + "type": "string" + }, + "members_url": { + "type": "string" + }, + "public_members_url": { + "type": "string" + }, + "avatar_url": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + } + }, + "required": [ + "login", + "url", + "id", + "node_id", + "repos_url", + "events_url", + "hooks_url", + "issues_url", + "members_url", + "public_members_url", + "avatar_url", + "description" + ] + }, + "private-user": { + "type": "object", + "properties": { + "login": { + "type": "string" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "user_view_type": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "avatar_url": { + "type": "string", + "format": "uri" + }, + "gravatar_id": { + "type": "string", + "nullable": true + }, + "url": { + "type": "string", + "format": "uri" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string" + }, + "gists_url": { + "type": "string" + }, + "starred_url": { + "type": "string" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "events_url": { + "type": "string" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string" + }, + "site_admin": { + "type": "boolean" + }, + "name": { + "type": "string", + "nullable": true + }, + "company": { + "type": "string", + "nullable": true + }, + "blog": { + "type": "string", + "nullable": true + }, + "location": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string", + "format": "email", + "nullable": true + }, + "notification_email": { + "type": "string", + "format": "email", + "nullable": true + }, + "hireable": { + "type": "boolean", + "nullable": true + }, + "bio": { + "type": "string", + "nullable": true + }, + "twitter_username": { + "type": "string", + "nullable": true + }, + "public_repos": { + "type": "integer" + }, + "public_gists": { + "type": "integer" + }, + "followers": { + "type": "integer" + }, + "following": { + "type": "integer" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "private_gists": { + "type": "integer" + }, + "total_private_repos": { + "type": "integer" + }, + "owned_private_repos": { + "type": "integer" + }, + "disk_usage": { + "type": "integer" + }, + "collaborators": { + "type": "integer" + }, + "two_factor_authentication": { + "type": "boolean" + }, + "plan": { + "type": "object", + "properties": { + "collaborators": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "space": { + "type": "integer" + }, + "private_repos": { + "type": "integer" + } + }, + "required": [ + "collaborators", + "name", + "space", + "private_repos" + ] + }, + "business_plus": { + "type": "boolean" + }, + "ldap_dn": { + "type": "string" + } + }, + "required": [ + "avatar_url", + "events_url", + "followers_url", + "following_url", + "gists_url", + "gravatar_id", + "html_url", + "id", + "node_id", + "login", + "organizations_url", + "received_events_url", + "repos_url", + "site_admin", + "starred_url", + "subscriptions_url", + "type", + "url", + "bio", + "blog", + "company", + "email", + "followers", + "following", + "hireable", + "location", + "name", + "public_gists", + "public_repos", + "created_at", + "updated_at", + "collaborators", + "disk_usage", + "owned_private_repos", + "private_gists", + "total_private_repos", + "two_factor_authentication" + ] + }, + "public-user": { + "type": "object", + "properties": { + "login": { + "type": "string" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "user_view_type": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "avatar_url": { + "type": "string", + "format": "uri" + }, + "gravatar_id": { + "type": "string", + "nullable": true + }, + "url": { + "type": "string", + "format": "uri" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string" + }, + "gists_url": { + "type": "string" + }, + "starred_url": { + "type": "string" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "events_url": { + "type": "string" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string" + }, + "site_admin": { + "type": "boolean" + }, + "name": { + "type": "string", + "nullable": true + }, + "company": { + "type": "string", + "nullable": true + }, + "blog": { + "type": "string", + "nullable": true + }, + "location": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string", + "format": "email", + "nullable": true + }, + "notification_email": { + "type": "string", + "format": "email", + "nullable": true + }, + "hireable": { + "type": "boolean", + "nullable": true + }, + "bio": { + "type": "string", + "nullable": true + }, + "twitter_username": { + "type": "string", + "nullable": true + }, + "public_repos": { + "type": "integer" + }, + "public_gists": { + "type": "integer" + }, + "followers": { + "type": "integer" + }, + "following": { + "type": "integer" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "plan": { + "type": "object", + "properties": { + "collaborators": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "space": { + "type": "integer" + }, + "private_repos": { + "type": "integer" + } + }, + "required": [ + "collaborators", + "name", + "space", + "private_repos" + ] + }, + "private_gists": { + "type": "integer" + }, + "total_private_repos": { + "type": "integer" + }, + "owned_private_repos": { + "type": "integer" + }, + "disk_usage": { + "type": "integer" + }, + "collaborators": { + "type": "integer" + } + }, + "required": [ + "avatar_url", + "events_url", + "followers_url", + "following_url", + "gists_url", + "gravatar_id", + "html_url", + "id", + "node_id", + "login", + "organizations_url", + "received_events_url", + "repos_url", + "site_admin", + "starred_url", + "subscriptions_url", + "type", + "url", + "bio", + "blog", + "company", + "email", + "followers", + "following", + "hireable", + "location", + "name", + "public_gists", + "public_repos", + "created_at", + "updated_at" + ], + "additionalProperties": false + }, + "pull-request": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "node_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "diff_url": { + "type": "string", + "format": "uri" + }, + "patch_url": { + "type": "string", + "format": "uri" + }, + "issue_url": { + "type": "string", + "format": "uri" + }, + "commits_url": { + "type": "string", + "format": "uri" + }, + "review_comments_url": { + "type": "string", + "format": "uri" + }, + "review_comment_url": { + "type": "string" + }, + "comments_url": { + "type": "string", + "format": "uri" + }, + "statuses_url": { + "type": "string", + "format": "uri" + }, + "number": { + "type": "integer" + }, + "state": { + "enum": [ + "open", + "closed" + ], + "type": "string" + }, + "locked": { + "type": "boolean" + }, + "title": { + "type": "string" + }, + "user": { + "$ref": "#/components/schemas/simple-user" + }, + "body": { + "type": "string", + "nullable": true + }, + "labels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "node_id": { + "type": "string" + }, + "url": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "color": { + "type": "string" + }, + "default": { + "type": "boolean" + }, + "archived_by": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/simple-user" + } + ] + } + }, + "required": [ + "id", + "node_id", + "url", + "name", + "description", + "color", + "default", + "archived_at", + "archived_by" + ] + } + }, + "milestone": { + "$ref": "#/components/schemas/nullable-milestone" + }, + "active_lock_reason": { + "type": "string", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "closed_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "merged_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "merge_commit_sha": { + "type": "string", + "nullable": true + }, + "assignee": { + "$ref": "#/components/schemas/nullable-simple-user" + }, + "assignees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/simple-user" + } + }, + "requested_reviewers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/simple-user" + } + }, + "requested_teams": { + "type": "array", + "items": { + "$ref": "#/components/schemas/team-simple" + } + }, + "head": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "ref": { + "type": "string" + }, + "repo": { + "$ref": "#/components/schemas/repository" + }, + "sha": { + "type": "string" + }, + "user": { + "$ref": "#/components/schemas/simple-user" + } + }, + "required": [ + "label", + "ref", + "repo", + "sha", + "user" + ] + }, + "base": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "ref": { + "type": "string" + }, + "repo": { + "$ref": "#/components/schemas/repository" + }, + "sha": { + "type": "string" + }, + "user": { + "$ref": "#/components/schemas/simple-user" + } + }, + "required": [ + "label", + "ref", + "repo", + "sha", + "user" + ] + }, + "_links": { + "type": "object", + "properties": { + "comments": { + "$ref": "#/components/schemas/link" + }, + "commits": { + "$ref": "#/components/schemas/link" + }, + "statuses": { + "$ref": "#/components/schemas/link" + }, + "html": { + "$ref": "#/components/schemas/link" + }, + "issue": { + "$ref": "#/components/schemas/link" + }, + "review_comments": { + "$ref": "#/components/schemas/link" + }, + "review_comment": { + "$ref": "#/components/schemas/link" + }, + "self": { + "$ref": "#/components/schemas/link" + } + }, + "required": [ + "comments", + "commits", + "statuses", + "html", + "issue", + "review_comments", + "review_comment", + "self" + ] + }, + "author_association": { + "$ref": "#/components/schemas/author-association" + }, + "auto_merge": { + "$ref": "#/components/schemas/auto-merge" + }, + "stack": { + "$ref": "#/components/schemas/pull-request-stack" + }, + "draft": { + "type": "boolean" + }, + "merged": { + "type": "boolean" + }, + "mergeable": { + "type": "boolean", + "nullable": true + }, + "rebaseable": { + "type": "boolean", + "nullable": true + }, + "mergeable_state": { + "type": "string" + }, + "merged_by": { + "$ref": "#/components/schemas/nullable-simple-user" + }, + "comments": { + "type": "integer" + }, + "review_comments": { + "type": "integer" + }, + "maintainer_can_modify": { + "type": "boolean" + }, + "commits": { + "type": "integer" + }, + "additions": { + "type": "integer" + }, + "deletions": { + "type": "integer" + }, + "changed_files": { + "type": "integer" + } + }, + "required": [ + "_links", + "assignee", + "labels", + "base", + "body", + "closed_at", + "comments_url", + "commits_url", + "created_at", + "diff_url", + "head", + "html_url", + "id", + "node_id", + "issue_url", + "merge_commit_sha", + "merged_at", + "milestone", + "number", + "patch_url", + "review_comment_url", + "review_comments_url", + "statuses_url", + "state", + "locked", + "title", + "updated_at", + "url", + "user", + "author_association", + "auto_merge", + "additions", + "changed_files", + "comments", + "commits", + "deletions", + "mergeable", + "mergeable_state", + "merged", + "maintainer_can_modify", + "merged_by", + "review_comments" + ] + }, + "pull-request-review": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "node_id": { + "type": "string" + }, + "user": { + "$ref": "#/components/schemas/nullable-simple-user" + }, + "body": { + "type": "string" + }, + "state": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "pull_request_url": { + "type": "string", + "format": "uri" + }, + "_links": { + "type": "object", + "properties": { + "html": { + "type": "object", + "properties": { + "href": { + "type": "string" + } + }, + "required": [ + "href" + ] + }, + "pull_request": { + "type": "object", + "properties": { + "href": { + "type": "string" + } + }, + "required": [ + "href" + ] + } + }, + "required": [ + "html", + "pull_request" + ] + }, + "submitted_at": { + "type": "string", + "format": "date-time" + }, + "commit_id": { + "type": "string", + "nullable": true + }, + "body_html": { + "type": "string" + }, + "body_text": { + "type": "string" + }, + "author_association": { + "$ref": "#/components/schemas/author-association" + } + }, + "required": [ + "id", + "node_id", + "user", + "body", + "state", + "commit_id", + "html_url", + "pull_request_url", + "_links", + "author_association" + ] + }, + "pull-request-stack": { + "type": "object", + "properties": { + "base": { + "type": "object", + "properties": { + "ref": { + "type": "string" + }, + "sha": { + "type": "string" + } + }, + "required": [ + "ref", + "sha" + ] + }, + "size": { + "type": "integer" + }, + "position": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "number": { + "type": "integer" + } + }, + "required": [ + "base" + ], + "nullable": true + }, + "pull-request-webhook": { + "allOf": [ + { + "$ref": "#/components/schemas/pull-request" + }, + { + "type": "object", + "properties": { + "allow_auto_merge": { + "type": "boolean", + "default": false + }, + "allow_update_branch": { + "type": "boolean" + }, + "delete_branch_on_merge": { + "type": "boolean", + "default": false + }, + "merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "PR_TITLE", + "BLANK" + ] + }, + "merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "MERGE_MESSAGE" + ] + }, + "squash_merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "COMMIT_MESSAGES", + "BLANK" + ] + }, + "squash_merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "COMMIT_OR_PR_TITLE" + ] + }, + "use_squash_pr_title_as_default": { + "type": "boolean", + "default": false + } + } + } + ] + }, + "reaction-rollup": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri" + }, + "total_count": { + "type": "integer" + }, + "+1": { + "type": "integer" + }, + "-1": { + "type": "integer" + }, + "laugh": { + "type": "integer" + }, + "confused": { + "type": "integer" + }, + "heart": { + "type": "integer" + }, + "hooray": { + "type": "integer" + }, + "eyes": { + "type": "integer" + }, + "rocket": { + "type": "integer" + } + }, + "required": [ + "url", + "total_count", + "+1", + "-1", + "laugh", + "confused", + "heart", + "hooray", + "eyes", + "rocket" + ] + }, + "repository": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "node_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "license": { + "$ref": "#/components/schemas/nullable-license-simple" + }, + "forks": { + "type": "integer" + }, + "permissions": { + "type": "object", + "properties": { + "admin": { + "type": "boolean" + }, + "pull": { + "type": "boolean" + }, + "triage": { + "type": "boolean" + }, + "push": { + "type": "boolean" + }, + "maintain": { + "type": "boolean" + } + }, + "required": [ + "admin", + "pull", + "push" + ] + }, + "owner": { + "$ref": "#/components/schemas/simple-user" + }, + "private": { + "default": false, + "type": "boolean" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string", + "nullable": true + }, + "fork": { + "type": "boolean" + }, + "url": { + "type": "string", + "format": "uri" + }, + "archive_url": { + "type": "string" + }, + "assignees_url": { + "type": "string" + }, + "blobs_url": { + "type": "string" + }, + "branches_url": { + "type": "string" + }, + "collaborators_url": { + "type": "string" + }, + "comments_url": { + "type": "string" + }, + "commits_url": { + "type": "string" + }, + "compare_url": { + "type": "string" + }, + "contents_url": { + "type": "string" + }, + "contributors_url": { + "type": "string", + "format": "uri" + }, + "deployments_url": { + "type": "string", + "format": "uri" + }, + "downloads_url": { + "type": "string", + "format": "uri" + }, + "events_url": { + "type": "string", + "format": "uri" + }, + "forks_url": { + "type": "string", + "format": "uri" + }, + "git_commits_url": { + "type": "string" + }, + "git_refs_url": { + "type": "string" + }, + "git_tags_url": { + "type": "string" + }, + "git_url": { + "type": "string" + }, + "issue_comment_url": { + "type": "string" + }, + "issue_events_url": { + "type": "string" + }, + "issues_url": { + "type": "string" + }, + "keys_url": { + "type": "string" + }, + "labels_url": { + "type": "string" + }, + "languages_url": { + "type": "string", + "format": "uri" + }, + "merges_url": { + "type": "string", + "format": "uri" + }, + "milestones_url": { + "type": "string" + }, + "notifications_url": { + "type": "string" + }, + "pulls_url": { + "type": "string" + }, + "releases_url": { + "type": "string" + }, + "ssh_url": { + "type": "string" + }, + "stargazers_url": { + "type": "string", + "format": "uri" + }, + "statuses_url": { + "type": "string" + }, + "subscribers_url": { + "type": "string", + "format": "uri" + }, + "subscription_url": { + "type": "string", + "format": "uri" + }, + "tags_url": { + "type": "string", + "format": "uri" + }, + "teams_url": { + "type": "string", + "format": "uri" + }, + "trees_url": { + "type": "string" + }, + "clone_url": { + "type": "string" + }, + "mirror_url": { + "type": "string", + "format": "uri", + "nullable": true + }, + "hooks_url": { + "type": "string", + "format": "uri" + }, + "svn_url": { + "type": "string", + "format": "uri" + }, + "homepage": { + "type": "string", + "format": "uri", + "nullable": true + }, + "language": { + "type": "string", + "nullable": true + }, + "forks_count": { + "type": "integer" + }, + "stargazers_count": { + "type": "integer" + }, + "watchers_count": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "default_branch": { + "type": "string" + }, + "open_issues_count": { + "type": "integer" + }, + "is_template": { + "default": false, + "type": "boolean" + }, + "topics": { + "type": "array", + "items": { + "type": "string" + } + }, + "has_issues": { + "default": true, + "type": "boolean" + }, + "has_projects": { + "default": true, + "type": "boolean" + }, + "has_wiki": { + "default": true, + "type": "boolean" + }, + "has_pages": { + "type": "boolean" + }, + "has_downloads": { + "default": true, + "type": "boolean", + "deprecated": true + }, + "has_discussions": { + "default": false, + "type": "boolean" + }, + "has_pull_requests": { + "default": true, + "type": "boolean" + }, + "pull_request_creation_policy": { + "type": "string", + "enum": [ + "all", + "collaborators_only" + ] + }, + "archived": { + "default": false, + "type": "boolean" + }, + "disabled": { + "type": "boolean" + }, + "visibility": { + "default": "public", + "type": "string" + }, + "pushed_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "updated_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "allow_rebase_merge": { + "default": true, + "type": "boolean" + }, + "temp_clone_token": { + "type": "string" + }, + "allow_squash_merge": { + "default": true, + "type": "boolean" + }, + "allow_auto_merge": { + "default": false, + "type": "boolean" + }, + "delete_branch_on_merge": { + "default": false, + "type": "boolean" + }, + "allow_update_branch": { + "default": false, + "type": "boolean" + }, + "use_squash_pr_title_as_default": { + "type": "boolean", + "default": false, + "deprecated": true + }, + "squash_merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "COMMIT_OR_PR_TITLE" + ] + }, + "squash_merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "COMMIT_MESSAGES", + "BLANK" + ] + }, + "merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "MERGE_MESSAGE" + ] + }, + "merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "PR_TITLE", + "BLANK" + ] + }, + "allow_merge_commit": { + "default": true, + "type": "boolean" + }, + "allow_forking": { + "type": "boolean" + }, + "web_commit_signoff_required": { + "default": false, + "type": "boolean" + }, + "open_issues": { + "type": "integer" + }, + "watchers": { + "type": "integer" + }, + "master_branch": { + "type": "string" + }, + "starred_at": { + "type": "string" + }, + "anonymous_access_enabled": { + "type": "boolean" + }, + "code_search_index_status": { + "type": "object", + "properties": { + "lexical_search_ok": { + "type": "boolean" + }, + "lexical_commit_sha": { + "type": "string" + } + } + } + }, + "required": [ + "archive_url", + "assignees_url", + "blobs_url", + "branches_url", + "collaborators_url", + "comments_url", + "commits_url", + "compare_url", + "contents_url", + "contributors_url", + "deployments_url", + "description", + "downloads_url", + "events_url", + "fork", + "forks_url", + "full_name", + "git_commits_url", + "git_refs_url", + "git_tags_url", + "hooks_url", + "html_url", + "id", + "node_id", + "issue_comment_url", + "issue_events_url", + "issues_url", + "keys_url", + "labels_url", + "languages_url", + "merges_url", + "milestones_url", + "name", + "notifications_url", + "owner", + "private", + "pulls_url", + "releases_url", + "stargazers_url", + "statuses_url", + "subscribers_url", + "subscription_url", + "tags_url", + "teams_url", + "trees_url", + "url", + "clone_url", + "default_branch", + "forks", + "forks_count", + "git_url", + "has_downloads", + "has_issues", + "has_projects", + "has_wiki", + "has_pages", + "homepage", + "language", + "archived", + "disabled", + "mirror_url", + "open_issues", + "open_issues_count", + "license", + "pushed_at", + "size", + "ssh_url", + "stargazers_count", + "svn_url", + "watchers", + "watchers_count", + "created_at", + "updated_at" + ] + }, + "repository-webhooks": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "node_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "license": { + "$ref": "#/components/schemas/nullable-license-simple" + }, + "organization": { + "$ref": "#/components/schemas/nullable-simple-user" + }, + "forks": { + "type": "integer" + }, + "permissions": { + "type": "object", + "properties": { + "admin": { + "type": "boolean" + }, + "pull": { + "type": "boolean" + }, + "triage": { + "type": "boolean" + }, + "push": { + "type": "boolean" + }, + "maintain": { + "type": "boolean" + } + }, + "required": [ + "admin", + "pull", + "push" + ] + }, + "owner": { + "$ref": "#/components/schemas/simple-user" + }, + "private": { + "default": false, + "type": "boolean" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string", + "nullable": true + }, + "fork": { + "type": "boolean" + }, + "url": { + "type": "string", + "format": "uri" + }, + "archive_url": { + "type": "string" + }, + "assignees_url": { + "type": "string" + }, + "blobs_url": { + "type": "string" + }, + "branches_url": { + "type": "string" + }, + "collaborators_url": { + "type": "string" + }, + "comments_url": { + "type": "string" + }, + "commits_url": { + "type": "string" + }, + "compare_url": { + "type": "string" + }, + "contents_url": { + "type": "string" + }, + "contributors_url": { + "type": "string", + "format": "uri" + }, + "deployments_url": { + "type": "string", + "format": "uri" + }, + "downloads_url": { + "type": "string", + "format": "uri" + }, + "events_url": { + "type": "string", + "format": "uri" + }, + "forks_url": { + "type": "string", + "format": "uri" + }, + "git_commits_url": { + "type": "string" + }, + "git_refs_url": { + "type": "string" + }, + "git_tags_url": { + "type": "string" + }, + "git_url": { + "type": "string" + }, + "issue_comment_url": { + "type": "string" + }, + "issue_events_url": { + "type": "string" + }, + "issues_url": { + "type": "string" + }, + "keys_url": { + "type": "string" + }, + "labels_url": { + "type": "string" + }, + "languages_url": { + "type": "string", + "format": "uri" + }, + "merges_url": { + "type": "string", + "format": "uri" + }, + "milestones_url": { + "type": "string" + }, + "notifications_url": { + "type": "string" + }, + "pulls_url": { + "type": "string" + }, + "releases_url": { + "type": "string" + }, + "ssh_url": { + "type": "string" + }, + "stargazers_url": { + "type": "string", + "format": "uri" + }, + "statuses_url": { + "type": "string" + }, + "subscribers_url": { + "type": "string", + "format": "uri" + }, + "subscription_url": { + "type": "string", + "format": "uri" + }, + "tags_url": { + "type": "string", + "format": "uri" + }, + "teams_url": { + "type": "string", + "format": "uri" + }, + "trees_url": { + "type": "string" + }, + "clone_url": { + "type": "string" + }, + "mirror_url": { + "type": "string", + "format": "uri", + "nullable": true + }, + "hooks_url": { + "type": "string", + "format": "uri" + }, + "svn_url": { + "type": "string", + "format": "uri" + }, + "homepage": { + "type": "string", + "format": "uri", + "nullable": true + }, + "language": { + "type": "string", + "nullable": true + }, + "forks_count": { + "type": "integer" + }, + "stargazers_count": { + "type": "integer" + }, + "watchers_count": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "default_branch": { + "type": "string" + }, + "open_issues_count": { + "type": "integer" + }, + "is_template": { + "default": false, + "type": "boolean" + }, + "topics": { + "type": "array", + "items": { + "type": "string" + } + }, + "custom_properties": { + "type": "object", + "additionalProperties": true + }, + "has_issues": { + "default": true, + "type": "boolean" + }, + "has_projects": { + "default": true, + "type": "boolean" + }, + "has_wiki": { + "default": true, + "type": "boolean" + }, + "has_pages": { + "type": "boolean" + }, + "has_downloads": { + "default": true, + "type": "boolean" + }, + "has_discussions": { + "default": false, + "type": "boolean" + }, + "has_pull_requests": { + "default": true, + "type": "boolean" + }, + "pull_request_creation_policy": { + "type": "string", + "enum": [ + "all", + "collaborators_only" + ] + }, + "archived": { + "default": false, + "type": "boolean" + }, + "disabled": { + "type": "boolean" + }, + "visibility": { + "default": "public", + "type": "string" + }, + "pushed_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "updated_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "allow_rebase_merge": { + "default": true, + "type": "boolean" + }, + "template_repository": { + "nullable": true, + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "node_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "owner": { + "type": "object", + "properties": { + "login": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "node_id": { + "type": "string" + }, + "avatar_url": { + "type": "string" + }, + "gravatar_id": { + "type": "string" + }, + "url": { + "type": "string" + }, + "html_url": { + "type": "string" + }, + "followers_url": { + "type": "string" + }, + "following_url": { + "type": "string" + }, + "gists_url": { + "type": "string" + }, + "starred_url": { + "type": "string" + }, + "subscriptions_url": { + "type": "string" + }, + "organizations_url": { + "type": "string" + }, + "repos_url": { + "type": "string" + }, + "events_url": { + "type": "string" + }, + "received_events_url": { + "type": "string" + }, + "type": { + "type": "string" + }, + "site_admin": { + "type": "boolean" + } + } + }, + "private": { + "type": "boolean" + }, + "html_url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "fork": { + "type": "boolean" + }, + "url": { + "type": "string" + }, + "archive_url": { + "type": "string" + }, + "assignees_url": { + "type": "string" + }, + "blobs_url": { + "type": "string" + }, + "branches_url": { + "type": "string" + }, + "collaborators_url": { + "type": "string" + }, + "comments_url": { + "type": "string" + }, + "commits_url": { + "type": "string" + }, + "compare_url": { + "type": "string" + }, + "contents_url": { + "type": "string" + }, + "contributors_url": { + "type": "string" + }, + "deployments_url": { + "type": "string" + }, + "downloads_url": { + "type": "string" + }, + "events_url": { + "type": "string" + }, + "forks_url": { + "type": "string" + }, + "git_commits_url": { + "type": "string" + }, + "git_refs_url": { + "type": "string" + }, + "git_tags_url": { + "type": "string" + }, + "git_url": { + "type": "string" + }, + "issue_comment_url": { + "type": "string" + }, + "issue_events_url": { + "type": "string" + }, + "issues_url": { + "type": "string" + }, + "keys_url": { + "type": "string" + }, + "labels_url": { + "type": "string" + }, + "languages_url": { + "type": "string" + }, + "merges_url": { + "type": "string" + }, + "milestones_url": { + "type": "string" + }, + "notifications_url": { + "type": "string" + }, + "pulls_url": { + "type": "string" + }, + "releases_url": { + "type": "string" + }, + "ssh_url": { + "type": "string" + }, + "stargazers_url": { + "type": "string" + }, + "statuses_url": { + "type": "string" + }, + "subscribers_url": { + "type": "string" + }, + "subscription_url": { + "type": "string" + }, + "tags_url": { + "type": "string" + }, + "teams_url": { + "type": "string" + }, + "trees_url": { + "type": "string" + }, + "clone_url": { + "type": "string" + }, + "mirror_url": { + "type": "string" + }, + "hooks_url": { + "type": "string" + }, + "svn_url": { + "type": "string" + }, + "homepage": { + "type": "string" + }, + "language": { + "type": "string" + }, + "forks_count": { + "type": "integer" + }, + "stargazers_count": { + "type": "integer" + }, + "watchers_count": { + "type": "integer" + }, + "size": { + "type": "integer" + }, + "default_branch": { + "type": "string" + }, + "open_issues_count": { + "type": "integer" + }, + "is_template": { + "type": "boolean" + }, + "topics": { + "type": "array", + "items": { + "type": "string" + } + }, + "has_issues": { + "type": "boolean" + }, + "has_projects": { + "type": "boolean" + }, + "has_wiki": { + "type": "boolean" + }, + "has_pages": { + "type": "boolean" + }, + "has_downloads": { + "type": "boolean" + }, + "archived": { + "type": "boolean" + }, + "disabled": { + "type": "boolean" + }, + "visibility": { + "type": "string" + }, + "pushed_at": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "permissions": { + "type": "object", + "properties": { + "admin": { + "type": "boolean" + }, + "maintain": { + "type": "boolean" + }, + "push": { + "type": "boolean" + }, + "triage": { + "type": "boolean" + }, + "pull": { + "type": "boolean" + } + } + }, + "allow_rebase_merge": { + "type": "boolean" + }, + "temp_clone_token": { + "type": "string" + }, + "allow_squash_merge": { + "type": "boolean" + }, + "allow_auto_merge": { + "type": "boolean" + }, + "delete_branch_on_merge": { + "type": "boolean" + }, + "allow_update_branch": { + "type": "boolean" + }, + "use_squash_pr_title_as_default": { + "type": "boolean" + }, + "squash_merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "COMMIT_OR_PR_TITLE" + ] + }, + "squash_merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "COMMIT_MESSAGES", + "BLANK" + ] + }, + "merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "MERGE_MESSAGE" + ] + }, + "merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "PR_TITLE", + "BLANK" + ] + }, + "allow_merge_commit": { + "type": "boolean" + }, + "subscribers_count": { + "type": "integer" + }, + "network_count": { + "type": "integer" + } + } + }, + "temp_clone_token": { + "type": "string" + }, + "allow_squash_merge": { + "default": true, + "type": "boolean" + }, + "allow_auto_merge": { + "default": false, + "type": "boolean" + }, + "delete_branch_on_merge": { + "default": false, + "type": "boolean" + }, + "allow_update_branch": { + "default": false, + "type": "boolean" + }, + "use_squash_pr_title_as_default": { + "type": "boolean", + "default": false, + "deprecated": true + }, + "squash_merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "COMMIT_OR_PR_TITLE" + ] + }, + "squash_merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "COMMIT_MESSAGES", + "BLANK" + ] + }, + "merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "MERGE_MESSAGE" + ] + }, + "merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "PR_TITLE", + "BLANK" + ] + }, + "allow_merge_commit": { + "default": true, + "type": "boolean" + }, + "allow_forking": { + "type": "boolean" + }, + "web_commit_signoff_required": { + "default": false, + "type": "boolean" + }, + "subscribers_count": { + "type": "integer" + }, + "network_count": { + "type": "integer" + }, + "open_issues": { + "type": "integer" + }, + "watchers": { + "type": "integer" + }, + "master_branch": { + "type": "string" + }, + "starred_at": { + "type": "string" + }, + "anonymous_access_enabled": { + "type": "boolean" + } + }, + "required": [ + "archive_url", + "assignees_url", + "blobs_url", + "branches_url", + "collaborators_url", + "comments_url", + "commits_url", + "compare_url", + "contents_url", + "contributors_url", + "deployments_url", + "description", + "downloads_url", + "events_url", + "fork", + "forks_url", + "full_name", + "git_commits_url", + "git_refs_url", + "git_tags_url", + "hooks_url", + "html_url", + "id", + "node_id", + "issue_comment_url", + "issue_events_url", + "issues_url", + "keys_url", + "labels_url", + "languages_url", + "merges_url", + "milestones_url", + "name", + "notifications_url", + "owner", + "private", + "pulls_url", + "releases_url", + "stargazers_url", + "statuses_url", + "subscribers_url", + "subscription_url", + "tags_url", + "teams_url", + "trees_url", + "url", + "clone_url", + "default_branch", + "forks", + "forks_count", + "git_url", + "has_downloads", + "has_issues", + "has_projects", + "has_wiki", + "has_pages", + "homepage", + "language", + "archived", + "disabled", + "mirror_url", + "open_issues", + "open_issues_count", + "license", + "pushed_at", + "size", + "ssh_url", + "stargazers_count", + "svn_url", + "watchers", + "watchers_count", + "created_at", + "updated_at" + ] + }, + "scim-error": { + "type": "object", + "properties": { + "message": { + "type": "string", + "nullable": true + }, + "documentation_url": { + "type": "string", + "nullable": true + }, + "detail": { + "type": "string", + "nullable": true + }, + "status": { + "type": "integer" + }, + "scimType": { + "type": "string", + "nullable": true + }, + "schemas": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "search-result-text-matches": { + "type": "array", + "items": { + "type": "object", + "properties": { + "object_url": { + "type": "string" + }, + "object_type": { + "nullable": true, + "type": "string" + }, + "property": { + "type": "string" + }, + "fragment": { + "type": "string" + }, + "matches": { + "type": "array", + "items": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "indices": { + "type": "array", + "items": { + "type": "integer" + } + } + } + } + } + } + } + }, + "security-and-analysis": { + "nullable": true, + "type": "object", + "properties": { + "advanced_security": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + } + } + }, + "code_security": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + } + } + }, + "dependabot_security_updates": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + } + } + }, + "secret_scanning": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + } + } + }, + "secret_scanning_push_protection": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + } + } + }, + "secret_scanning_non_provider_patterns": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + } + } + }, + "secret_scanning_ai_detection": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + } + } + }, + "secret_scanning_delegated_alert_dismissal": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + } + } + }, + "secret_scanning_delegated_bypass": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + } + } + }, + "secret_scanning_delegated_bypass_options": { + "type": "object", + "properties": { + "reviewers": { + "type": "array", + "items": { + "type": "object", + "required": [ + "reviewer_id", + "reviewer_type" + ], + "properties": { + "reviewer_id": { + "type": "integer" + }, + "reviewer_type": { + "type": "string", + "enum": [ + "TEAM", + "ROLE" + ] + }, + "mode": { + "type": "string", + "enum": [ + "ALWAYS", + "EXEMPT" + ], + "default": "ALWAYS" + } + } + } + } + } + } + } + }, + "simple-commit-status": { + "type": "object", + "properties": { + "description": { + "type": "string", + "nullable": true + }, + "id": { + "type": "integer" + }, + "node_id": { + "type": "string" + }, + "state": { + "type": "string" + }, + "context": { + "type": "string" + }, + "target_url": { + "type": "string", + "format": "uri", + "nullable": true + }, + "required": { + "type": "boolean", + "nullable": true + }, + "avatar_url": { + "type": "string", + "nullable": true, + "format": "uri" + }, + "url": { + "type": "string", + "format": "uri" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "node_id", + "state", + "context", + "target_url", + "avatar_url", + "url", + "created_at", + "updated_at" + ] + }, + "simple-installation": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "node_id": { + "type": "string" + } + }, + "required": [ + "id", + "node_id" + ] + }, + "simple-user": { + "type": "object", + "properties": { + "name": { + "nullable": true, + "type": "string" + }, + "email": { + "nullable": true, + "type": "string" + }, + "login": { + "type": "string" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "node_id": { + "type": "string" + }, + "avatar_url": { + "type": "string", + "format": "uri" + }, + "gravatar_id": { + "type": "string", + "nullable": true + }, + "url": { + "type": "string", + "format": "uri" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string" + }, + "gists_url": { + "type": "string" + }, + "starred_url": { + "type": "string" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "events_url": { + "type": "string" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string" + }, + "site_admin": { + "type": "boolean" + }, + "starred_at": { + "type": "string" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "avatar_url", + "events_url", + "followers_url", + "following_url", + "gists_url", + "gravatar_id", + "html_url", + "id", + "node_id", + "login", + "organizations_url", + "received_events_url", + "repos_url", + "site_admin", + "starred_url", + "subscriptions_url", + "type", + "url" + ] + }, + "sub-issues-summary": { + "type": "object", + "properties": { + "total": { + "type": "integer" + }, + "completed": { + "type": "integer" + }, + "percent_completed": { + "type": "integer" + } + }, + "required": [ + "total", + "completed", + "percent_completed" + ] + }, + "team-simple": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "node_id": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "members_url": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "permission": { + "type": "string" + }, + "privacy": { + "type": "string" + }, + "notification_setting": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "repositories_url": { + "type": "string", + "format": "uri" + }, + "slug": { + "type": "string" + }, + "ldap_dn": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "enterprise", + "organization" + ] + }, + "organization_id": { + "type": "integer" + }, + "enterprise_id": { + "type": "integer" + } + }, + "required": [ + "id", + "node_id", + "url", + "members_url", + "name", + "description", + "permission", + "html_url", + "repositories_url", + "slug", + "type" + ] + }, + "thread": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "repository": { + "$ref": "#/components/schemas/minimal-repository" + }, + "subject": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "url": { + "type": "string" + }, + "latest_comment_url": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "title", + "url", + "latest_comment_url", + "type" + ] + }, + "reason": { + "type": "string" + }, + "unread": { + "type": "boolean" + }, + "updated_at": { + "type": "string" + }, + "last_read_at": { + "type": "string", + "nullable": true + }, + "url": { + "type": "string" + }, + "subscription_url": { + "type": "string" + } + }, + "required": [ + "id", + "unread", + "reason", + "updated_at", + "last_read_at", + "subject", + "repository", + "url", + "subscription_url" + ] + }, + "validation-error": { + "type": "object", + "required": [ + "message", + "documentation_url" + ], + "properties": { + "message": { + "type": "string" + }, + "documentation_url": { + "type": "string" + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "required": [ + "code" + ], + "properties": { + "resource": { + "type": "string" + }, + "field": { + "type": "string" + }, + "message": { + "type": "string" + }, + "code": { + "type": "string" + }, + "index": { + "type": "integer" + }, + "value": { + "oneOf": [ + { + "type": "string", + "nullable": true + }, + { + "type": "integer", + "nullable": true + }, + { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } + } + ] + } + } + } + } + } + }, + "webhook-config": { + "type": "object", + "properties": { + "url": { + "$ref": "#/components/schemas/webhook-config-url" + }, + "content_type": { + "$ref": "#/components/schemas/webhook-config-content-type" + }, + "secret": { + "$ref": "#/components/schemas/webhook-config-secret" + }, + "insecure_ssl": { + "$ref": "#/components/schemas/webhook-config-insecure-ssl" + } + } + }, + "webhook-config-content-type": { + "type": "string" + }, + "webhook-config-insecure-ssl": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + }, + "webhook-config-secret": { + "type": "string" + }, + "webhook-config-url": { + "type": "string", + "format": "uri" + }, + "webhook-issue-comment-created": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "created" + ] + }, + "comment": { + "type": "object", + "properties": { + "author_association": { + "type": "string", + "enum": [ + "COLLABORATOR", + "CONTRIBUTOR", + "FIRST_TIMER", + "FIRST_TIME_CONTRIBUTOR", + "MANNEQUIN", + "MEMBER", + "NONE", + "OWNER" + ] + }, + "body": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "issue_url": { + "type": "string", + "format": "uri" + }, + "node_id": { + "type": "string" + }, + "performed_via_github_app": { + "$ref": "#/components/schemas/nullable-integration" + }, + "reactions": { + "type": "object", + "properties": { + "+1": { + "type": "integer" + }, + "-1": { + "type": "integer" + }, + "confused": { + "type": "integer" + }, + "eyes": { + "type": "integer" + }, + "heart": { + "type": "integer" + }, + "hooray": { + "type": "integer" + }, + "laugh": { + "type": "integer" + }, + "rocket": { + "type": "integer" + }, + "total_count": { + "type": "integer" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "url", + "total_count", + "+1", + "-1", + "laugh", + "confused", + "heart", + "hooray", + "eyes", + "rocket" + ] + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "url": { + "type": "string", + "format": "uri" + }, + "pin": { + "$ref": "#/components/schemas/nullable-pinned-issue-comment" + }, + "minimized": { + "$ref": "#/components/schemas/nullable-issue-comment-minimized" + }, + "user": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + } + }, + "required": [ + "url", + "html_url", + "issue_url", + "id", + "node_id", + "user", + "created_at", + "updated_at", + "author_association", + "performed_via_github_app", + "body", + "reactions" + ] + }, + "enterprise": { + "$ref": "#/components/schemas/enterprise-webhooks" + }, + "installation": { + "$ref": "#/components/schemas/simple-installation" + }, + "issue": { + "allOf": [ + { + "type": "object", + "properties": { + "active_lock_reason": { + "type": "string", + "nullable": true, + "enum": [ + "resolved", + "off-topic", + "too heated", + "spam", + null + ] + }, + "assignee": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization", + "Mannequin" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "assignees": { + "type": "array", + "items": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization", + "Mannequin" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + } + }, + "author_association": { + "type": "string", + "enum": [ + "COLLABORATOR", + "CONTRIBUTOR", + "FIRST_TIMER", + "FIRST_TIME_CONTRIBUTOR", + "MANNEQUIN", + "MEMBER", + "NONE", + "OWNER" + ] + }, + "body": { + "type": "string", + "nullable": true + }, + "closed_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "comments": { + "type": "integer" + }, + "comments_url": { + "type": "string", + "format": "uri" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "draft": { + "type": "boolean" + }, + "events_url": { + "type": "string", + "format": "uri" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "labels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "color": { + "type": "string" + }, + "default": { + "type": "boolean" + }, + "description": { + "type": "string", + "nullable": true + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "id", + "node_id", + "url", + "name", + "color", + "default", + "description" + ] + } + }, + "labels_url": { + "type": "string", + "format": "uri-template" + }, + "locked": { + "type": "boolean" + }, + "milestone": { + "type": "object", + "nullable": true, + "properties": { + "closed_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "closed_issues": { + "type": "integer" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "creator": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization", + "Mannequin" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "description": { + "type": "string", + "nullable": true + }, + "due_on": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "labels_url": { + "type": "string", + "format": "uri" + }, + "node_id": { + "type": "string" + }, + "number": { + "type": "integer" + }, + "open_issues": { + "type": "integer" + }, + "state": { + "type": "string", + "enum": [ + "open", + "closed" + ] + }, + "title": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "url", + "html_url", + "labels_url", + "id", + "node_id", + "number", + "title", + "description", + "creator", + "open_issues", + "closed_issues", + "state", + "created_at", + "updated_at", + "due_on", + "closed_at" + ] + }, + "node_id": { + "type": "string" + }, + "number": { + "type": "integer" + }, + "performed_via_github_app": { + "type": "object", + "nullable": true, + "properties": { + "created_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "description": { + "type": "string", + "nullable": true + }, + "events": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "branch_protection_rule", + "check_run", + "check_suite", + "code_scanning_alert", + "commit_comment", + "content_reference", + "create", + "delete", + "deployment", + "deployment_review", + "deployment_status", + "deploy_key", + "discussion", + "discussion_comment", + "fork", + "gollum", + "issues", + "issue_comment", + "label", + "member", + "membership", + "milestone", + "organization", + "org_block", + "page_build", + "project", + "project_card", + "project_column", + "public", + "pull_request", + "pull_request_review", + "pull_request_review_comment", + "push", + "registry_package", + "release", + "repository", + "repository_dispatch", + "secret_scanning_alert", + "star", + "status", + "team", + "team_add", + "watch", + "workflow_dispatch", + "workflow_run", + "reminder", + "pull_request_review_thread" + ] + } + }, + "external_url": { + "type": "string", + "nullable": true, + "format": "uri" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "nullable": true + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "owner": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "permissions": { + "type": "object", + "properties": { + "actions": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "administration": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "checks": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "content_references": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "contents": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "deployments": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "discussions": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "emails": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "environments": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "issues": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "keys": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "members": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "metadata": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "organization_administration": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "organization_hooks": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "organization_packages": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "organization_plan": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "organization_projects": { + "type": "string", + "enum": [ + "read", + "write", + "admin" + ] + }, + "organization_secrets": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "organization_self_hosted_runners": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "organization_user_blocking": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "packages": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "pages": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "pull_requests": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "repository_hooks": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "repository_projects": { + "type": "string", + "enum": [ + "read", + "write", + "admin" + ] + }, + "secret_scanning_alerts": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "secrets": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "security_events": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "security_scanning_alert": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "single_file": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "statuses": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "vulnerability_alerts": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "workflows": { + "type": "string", + "enum": [ + "read", + "write" + ] + } + } + }, + "slug": { + "type": "string" + }, + "updated_at": { + "type": "string", + "nullable": true, + "format": "date-time" + } + }, + "required": [ + "id", + "node_id", + "owner", + "name", + "description", + "external_url", + "html_url", + "created_at", + "updated_at" + ] + }, + "pull_request": { + "type": "object", + "properties": { + "diff_url": { + "type": "string", + "format": "uri" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "merged_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "patch_url": { + "type": "string", + "format": "uri" + }, + "url": { + "type": "string", + "format": "uri" + } + } + }, + "reactions": { + "type": "object", + "properties": { + "+1": { + "type": "integer" + }, + "-1": { + "type": "integer" + }, + "confused": { + "type": "integer" + }, + "eyes": { + "type": "integer" + }, + "heart": { + "type": "integer" + }, + "hooray": { + "type": "integer" + }, + "laugh": { + "type": "integer" + }, + "rocket": { + "type": "integer" + }, + "total_count": { + "type": "integer" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "url", + "total_count", + "+1", + "-1", + "laugh", + "confused", + "heart", + "hooray", + "eyes", + "rocket" + ] + }, + "repository_url": { + "type": "string", + "format": "uri" + }, + "sub_issues_summary": { + "$ref": "#/components/schemas/sub-issues-summary" + }, + "issue_dependencies_summary": { + "$ref": "#/components/schemas/issue-dependencies-summary" + }, + "state": { + "type": "string", + "enum": [ + "open", + "closed" + ] + }, + "state_reason": { + "type": "string", + "nullable": true + }, + "timeline_url": { + "type": "string", + "format": "uri" + }, + "title": { + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/issue-type" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "url": { + "type": "string", + "format": "uri" + }, + "user": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization", + "Mannequin" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + } + }, + "required": [ + "url", + "repository_url", + "labels_url", + "comments_url", + "events_url", + "html_url", + "id", + "node_id", + "number", + "title", + "user", + "assignees", + "milestone", + "comments", + "created_at", + "updated_at", + "closed_at", + "author_association", + "active_lock_reason", + "body", + "reactions" + ] + }, + { + "type": "object", + "properties": { + "active_lock_reason": { + "type": "string", + "nullable": true + }, + "assignee": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization", + "Mannequin" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "assignees": { + "type": "array", + "items": { + "type": "object", + "nullable": true + } + }, + "author_association": { + "type": "string" + }, + "body": { + "type": "string", + "nullable": true + }, + "closed_at": { + "type": "string", + "nullable": true + }, + "comments": { + "type": "integer" + }, + "comments_url": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "events_url": { + "type": "string" + }, + "html_url": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "labels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "color": { + "type": "string" + }, + "default": { + "type": "boolean" + }, + "description": { + "type": "string", + "nullable": true + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "id", + "node_id", + "url", + "name", + "color", + "default", + "description" + ] + } + }, + "labels_url": { + "type": "string" + }, + "locked": { + "type": "boolean" + }, + "milestone": { + "type": "object", + "nullable": true + }, + "node_id": { + "type": "string" + }, + "number": { + "type": "integer" + }, + "performed_via_github_app": { + "type": "object", + "nullable": true + }, + "reactions": { + "type": "object", + "properties": { + "+1": { + "type": "integer" + }, + "-1": { + "type": "integer" + }, + "confused": { + "type": "integer" + }, + "eyes": { + "type": "integer" + }, + "heart": { + "type": "integer" + }, + "hooray": { + "type": "integer" + }, + "laugh": { + "type": "integer" + }, + "rocket": { + "type": "integer" + }, + "total_count": { + "type": "integer" + }, + "url": { + "type": "string" + } + } + }, + "repository_url": { + "type": "string" + }, + "state": { + "type": "string", + "enum": [ + "open", + "closed" + ] + }, + "timeline_url": { + "type": "string" + }, + "title": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "url": { + "type": "string" + }, + "user": { + "type": "object", + "properties": { + "avatar_url": { + "type": "string" + }, + "events_url": { + "type": "string" + }, + "followers_url": { + "type": "string" + }, + "following_url": { + "type": "string" + }, + "gists_url": { + "type": "string" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "login": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string" + }, + "received_events_url": { + "type": "string" + }, + "repos_url": { + "type": "string" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string" + }, + "subscriptions_url": { + "type": "string" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + } + } + } + }, + "required": [ + "labels", + "state", + "locked", + "assignee" + ] + } + ] + }, + "organization": { + "$ref": "#/components/schemas/organization-simple-webhooks" + }, + "repository": { + "$ref": "#/components/schemas/repository-webhooks" + }, + "sender": { + "$ref": "#/components/schemas/simple-user" + } + }, + "required": [ + "action", + "issue", + "comment", + "repository", + "sender" + ] + }, + "webhook-issues-opened": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "opened" + ] + }, + "changes": { + "type": "object", + "properties": { + "old_issue": { + "type": "object", + "nullable": true, + "properties": { + "active_lock_reason": { + "type": "string", + "nullable": true, + "enum": [ + "resolved", + "off-topic", + "too heated", + "spam", + null + ] + }, + "assignee": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "assignees": { + "type": "array", + "items": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + } + }, + "author_association": { + "type": "string", + "enum": [ + "COLLABORATOR", + "CONTRIBUTOR", + "FIRST_TIMER", + "FIRST_TIME_CONTRIBUTOR", + "MANNEQUIN", + "MEMBER", + "NONE", + "OWNER" + ] + }, + "body": { + "type": "string", + "nullable": true + }, + "closed_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "comments": { + "type": "integer" + }, + "comments_url": { + "type": "string", + "format": "uri" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "draft": { + "type": "boolean" + }, + "events_url": { + "type": "string", + "format": "uri" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "labels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "color": { + "type": "string" + }, + "default": { + "type": "boolean" + }, + "description": { + "type": "string", + "nullable": true + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "id", + "node_id", + "url", + "name", + "color", + "default", + "description" + ] + } + }, + "labels_url": { + "type": "string", + "format": "uri-template" + }, + "locked": { + "type": "boolean" + }, + "milestone": { + "type": "object", + "nullable": true, + "properties": { + "closed_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "closed_issues": { + "type": "integer" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "creator": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "description": { + "type": "string", + "nullable": true + }, + "due_on": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "labels_url": { + "type": "string", + "format": "uri" + }, + "node_id": { + "type": "string" + }, + "number": { + "type": "integer" + }, + "open_issues": { + "type": "integer" + }, + "state": { + "type": "string", + "enum": [ + "open", + "closed" + ] + }, + "title": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "url", + "html_url", + "labels_url", + "id", + "node_id", + "number", + "title", + "description", + "creator", + "open_issues", + "closed_issues", + "state", + "created_at", + "updated_at", + "due_on", + "closed_at" + ] + }, + "node_id": { + "type": "string" + }, + "number": { + "type": "integer" + }, + "performed_via_github_app": { + "type": "object", + "nullable": true, + "properties": { + "created_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "description": { + "type": "string", + "nullable": true + }, + "events": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "branch_protection_rule", + "check_run", + "check_suite", + "code_scanning_alert", + "commit_comment", + "content_reference", + "create", + "delete", + "deployment", + "deployment_review", + "deployment_status", + "deploy_key", + "discussion", + "discussion_comment", + "fork", + "gollum", + "issues", + "issue_comment", + "label", + "member", + "membership", + "milestone", + "organization", + "org_block", + "page_build", + "project", + "project_card", + "project_column", + "public", + "pull_request", + "pull_request_review", + "pull_request_review_comment", + "push", + "registry_package", + "release", + "repository", + "repository_dispatch", + "secret_scanning_alert", + "star", + "status", + "team", + "team_add", + "watch", + "workflow_dispatch", + "workflow_run" + ] + } + }, + "external_url": { + "type": "string", + "nullable": true, + "format": "uri" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "nullable": true + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "owner": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "permissions": { + "type": "object", + "properties": { + "actions": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "administration": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "checks": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "content_references": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "contents": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "deployments": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "discussions": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "emails": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "environments": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "issues": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "keys": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "members": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "metadata": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "organization_administration": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "organization_hooks": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "organization_packages": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "organization_plan": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "organization_projects": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "organization_secrets": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "organization_self_hosted_runners": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "organization_user_blocking": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "packages": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "pages": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "pull_requests": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "repository_hooks": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "repository_projects": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "secret_scanning_alerts": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "secrets": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "security_events": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "security_scanning_alert": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "single_file": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "statuses": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "vulnerability_alerts": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "workflows": { + "type": "string", + "enum": [ + "read", + "write" + ] + } + } + }, + "slug": { + "type": "string" + }, + "updated_at": { + "type": "string", + "nullable": true, + "format": "date-time" + } + }, + "required": [ + "id", + "node_id", + "owner", + "name", + "description", + "external_url", + "html_url", + "created_at", + "updated_at" + ] + }, + "pull_request": { + "type": "object", + "properties": { + "diff_url": { + "type": "string", + "format": "uri" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "merged_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "patch_url": { + "type": "string", + "format": "uri" + }, + "url": { + "type": "string", + "format": "uri" + } + } + }, + "reactions": { + "type": "object", + "properties": { + "+1": { + "type": "integer" + }, + "-1": { + "type": "integer" + }, + "confused": { + "type": "integer" + }, + "eyes": { + "type": "integer" + }, + "heart": { + "type": "integer" + }, + "hooray": { + "type": "integer" + }, + "laugh": { + "type": "integer" + }, + "rocket": { + "type": "integer" + }, + "total_count": { + "type": "integer" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "url", + "total_count", + "+1", + "-1", + "laugh", + "confused", + "heart", + "hooray", + "eyes", + "rocket" + ] + }, + "repository_url": { + "type": "string", + "format": "uri" + }, + "sub_issues_summary": { + "$ref": "#/components/schemas/sub-issues-summary" + }, + "issue_dependencies_summary": { + "$ref": "#/components/schemas/issue-dependencies-summary" + }, + "issue_field_values": { + "type": "array", + "items": { + "$ref": "#/components/schemas/issue-field-value" + } + }, + "state": { + "type": "string", + "enum": [ + "open", + "closed" + ] + }, + "state_reason": { + "type": "string", + "nullable": true + }, + "timeline_url": { + "type": "string", + "format": "uri" + }, + "title": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "url": { + "type": "string", + "format": "uri" + }, + "pinned_comment": { + "$ref": "#/components/schemas/nullable-issue-comment" + }, + "user": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "type": { + "$ref": "#/components/schemas/issue-type" + } + }, + "required": [ + "id", + "number" + ] + }, + "old_repository": { + "type": "object", + "properties": { + "allow_auto_merge": { + "type": "boolean", + "default": false + }, + "allow_forking": { + "type": "boolean" + }, + "allow_merge_commit": { + "type": "boolean", + "default": true + }, + "allow_rebase_merge": { + "type": "boolean", + "default": true + }, + "allow_squash_merge": { + "type": "boolean", + "default": true + }, + "allow_update_branch": { + "type": "boolean" + }, + "archive_url": { + "type": "string", + "format": "uri-template" + }, + "archived": { + "type": "boolean", + "default": false + }, + "assignees_url": { + "type": "string", + "format": "uri-template" + }, + "blobs_url": { + "type": "string", + "format": "uri-template" + }, + "branches_url": { + "type": "string", + "format": "uri-template" + }, + "clone_url": { + "type": "string", + "format": "uri" + }, + "collaborators_url": { + "type": "string", + "format": "uri-template" + }, + "comments_url": { + "type": "string", + "format": "uri-template" + }, + "commits_url": { + "type": "string", + "format": "uri-template" + }, + "compare_url": { + "type": "string", + "format": "uri-template" + }, + "contents_url": { + "type": "string", + "format": "uri-template" + }, + "contributors_url": { + "type": "string", + "format": "uri" + }, + "created_at": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "custom_properties": { + "type": "object", + "additionalProperties": true + }, + "default_branch": { + "type": "string" + }, + "delete_branch_on_merge": { + "type": "boolean", + "default": false + }, + "deployments_url": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string", + "nullable": true + }, + "disabled": { + "type": "boolean" + }, + "downloads_url": { + "type": "string", + "format": "uri" + }, + "events_url": { + "type": "string", + "format": "uri" + }, + "fork": { + "type": "boolean" + }, + "forks": { + "type": "integer" + }, + "forks_count": { + "type": "integer" + }, + "forks_url": { + "type": "string", + "format": "uri" + }, + "full_name": { + "type": "string" + }, + "git_commits_url": { + "type": "string", + "format": "uri-template" + }, + "git_refs_url": { + "type": "string", + "format": "uri-template" + }, + "git_tags_url": { + "type": "string", + "format": "uri-template" + }, + "git_url": { + "type": "string", + "format": "uri" + }, + "has_discussions": { + "type": "boolean" + }, + "has_pull_requests": { + "type": "boolean", + "default": true + }, + "pull_request_creation_policy": { + "type": "string", + "enum": [ + "all", + "collaborators_only" + ] + }, + "has_downloads": { + "type": "boolean", + "default": true + }, + "has_issues": { + "type": "boolean", + "default": true + }, + "has_pages": { + "type": "boolean" + }, + "has_projects": { + "type": "boolean", + "default": true + }, + "has_wiki": { + "type": "boolean", + "default": true + }, + "homepage": { + "type": "string", + "nullable": true + }, + "hooks_url": { + "type": "string", + "format": "uri" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "is_template": { + "type": "boolean" + }, + "issue_comment_url": { + "type": "string", + "format": "uri-template" + }, + "issue_events_url": { + "type": "string", + "format": "uri-template" + }, + "issues_url": { + "type": "string", + "format": "uri-template" + }, + "keys_url": { + "type": "string", + "format": "uri-template" + }, + "labels_url": { + "type": "string", + "format": "uri-template" + }, + "language": { + "type": "string", + "nullable": true + }, + "languages_url": { + "type": "string", + "format": "uri" + }, + "license": { + "type": "object", + "nullable": true, + "properties": { + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "spdx_id": { + "type": "string" + }, + "url": { + "type": "string", + "nullable": true, + "format": "uri" + } + }, + "required": [ + "key", + "name", + "spdx_id", + "url", + "node_id" + ] + }, + "master_branch": { + "type": "string" + }, + "merges_url": { + "type": "string", + "format": "uri" + }, + "milestones_url": { + "type": "string", + "format": "uri-template" + }, + "mirror_url": { + "type": "string", + "nullable": true, + "format": "uri" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "notifications_url": { + "type": "string", + "format": "uri-template" + }, + "open_issues": { + "type": "integer" + }, + "open_issues_count": { + "type": "integer" + }, + "organization": { + "type": "string" + }, + "owner": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "permissions": { + "type": "object", + "properties": { + "admin": { + "type": "boolean" + }, + "maintain": { + "type": "boolean" + }, + "pull": { + "type": "boolean" + }, + "push": { + "type": "boolean" + }, + "triage": { + "type": "boolean" + } + }, + "required": [ + "pull", + "push", + "admin" + ] + }, + "private": { + "type": "boolean" + }, + "public": { + "type": "boolean" + }, + "pulls_url": { + "type": "string", + "format": "uri-template" + }, + "pushed_at": { + "nullable": true, + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "releases_url": { + "type": "string", + "format": "uri-template" + }, + "role_name": { + "type": "string", + "nullable": true + }, + "size": { + "type": "integer" + }, + "ssh_url": { + "type": "string" + }, + "stargazers": { + "type": "integer" + }, + "stargazers_count": { + "type": "integer" + }, + "stargazers_url": { + "type": "string", + "format": "uri" + }, + "statuses_url": { + "type": "string", + "format": "uri-template" + }, + "subscribers_url": { + "type": "string", + "format": "uri" + }, + "subscription_url": { + "type": "string", + "format": "uri" + }, + "svn_url": { + "type": "string", + "format": "uri" + }, + "tags_url": { + "type": "string", + "format": "uri" + }, + "teams_url": { + "type": "string", + "format": "uri" + }, + "topics": { + "type": "array", + "items": { + "type": "string" + } + }, + "trees_url": { + "type": "string", + "format": "uri-template" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "url": { + "type": "string", + "format": "uri" + }, + "visibility": { + "type": "string", + "enum": [ + "public", + "private", + "internal" + ] + }, + "watchers": { + "type": "integer" + }, + "watchers_count": { + "type": "integer" + }, + "web_commit_signoff_required": { + "type": "boolean" + } + }, + "required": [ + "id", + "node_id", + "name", + "full_name", + "private", + "owner", + "html_url", + "description", + "fork", + "url", + "forks_url", + "keys_url", + "collaborators_url", + "teams_url", + "hooks_url", + "issue_events_url", + "events_url", + "assignees_url", + "branches_url", + "tags_url", + "blobs_url", + "git_tags_url", + "git_refs_url", + "trees_url", + "statuses_url", + "languages_url", + "stargazers_url", + "contributors_url", + "subscribers_url", + "subscription_url", + "commits_url", + "git_commits_url", + "comments_url", + "issue_comment_url", + "contents_url", + "compare_url", + "merges_url", + "archive_url", + "downloads_url", + "issues_url", + "pulls_url", + "milestones_url", + "notifications_url", + "labels_url", + "releases_url", + "deployments_url", + "created_at", + "updated_at", + "pushed_at", + "git_url", + "ssh_url", + "clone_url", + "svn_url", + "homepage", + "size", + "stargazers_count", + "watchers_count", + "language", + "has_issues", + "has_projects", + "has_downloads", + "has_wiki", + "has_pages", + "forks_count", + "mirror_url", + "archived", + "open_issues_count", + "license", + "forks", + "open_issues", + "watchers", + "default_branch", + "topics", + "visibility" + ] + } + }, + "required": [ + "old_issue", + "old_repository" + ] + }, + "enterprise": { + "$ref": "#/components/schemas/enterprise-webhooks" + }, + "installation": { + "$ref": "#/components/schemas/simple-installation" + }, + "issue": { + "type": "object", + "properties": { + "active_lock_reason": { + "type": "string", + "nullable": true, + "enum": [ + "resolved", + "off-topic", + "too heated", + "spam", + null + ] + }, + "assignee": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "assignees": { + "type": "array", + "items": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + } + }, + "author_association": { + "type": "string", + "enum": [ + "COLLABORATOR", + "CONTRIBUTOR", + "FIRST_TIMER", + "FIRST_TIME_CONTRIBUTOR", + "MANNEQUIN", + "MEMBER", + "NONE", + "OWNER" + ] + }, + "body": { + "type": "string", + "nullable": true + }, + "closed_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "comments": { + "type": "integer" + }, + "comments_url": { + "type": "string", + "format": "uri" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "draft": { + "type": "boolean" + }, + "events_url": { + "type": "string", + "format": "uri" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "labels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "color": { + "type": "string" + }, + "default": { + "type": "boolean" + }, + "description": { + "type": "string", + "nullable": true + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "id", + "node_id", + "url", + "name", + "color", + "default", + "description" + ] + } + }, + "labels_url": { + "type": "string", + "format": "uri-template" + }, + "locked": { + "type": "boolean" + }, + "milestone": { + "type": "object", + "nullable": true, + "properties": { + "closed_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "closed_issues": { + "type": "integer" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "creator": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization", + "Mannequin" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "description": { + "type": "string", + "nullable": true + }, + "due_on": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "labels_url": { + "type": "string", + "format": "uri" + }, + "node_id": { + "type": "string" + }, + "number": { + "type": "integer" + }, + "open_issues": { + "type": "integer" + }, + "state": { + "type": "string", + "enum": [ + "open", + "closed" + ] + }, + "title": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "url", + "html_url", + "labels_url", + "id", + "node_id", + "number", + "title", + "description", + "creator", + "open_issues", + "closed_issues", + "state", + "created_at", + "updated_at", + "due_on", + "closed_at" + ] + }, + "node_id": { + "type": "string" + }, + "number": { + "type": "integer" + }, + "performed_via_github_app": { + "type": "object", + "nullable": true, + "properties": { + "created_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "description": { + "type": "string", + "nullable": true + }, + "events": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "branch_protection_rule", + "check_run", + "check_suite", + "code_scanning_alert", + "commit_comment", + "content_reference", + "create", + "delete", + "deployment", + "deployment_review", + "deployment_status", + "deploy_key", + "discussion", + "discussion_comment", + "fork", + "gollum", + "issues", + "issue_comment", + "label", + "member", + "membership", + "milestone", + "organization", + "org_block", + "page_build", + "project", + "project_card", + "project_column", + "public", + "pull_request", + "pull_request_review", + "pull_request_review_comment", + "push", + "registry_package", + "release", + "repository", + "repository_dispatch", + "secret_scanning_alert", + "star", + "status", + "team", + "team_add", + "watch", + "workflow_dispatch", + "workflow_run", + "security_and_analysis", + "pull_request_review_thread", + "reminder" + ] + } + }, + "external_url": { + "type": "string", + "nullable": true, + "format": "uri" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "nullable": true + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "owner": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "permissions": { + "type": "object", + "properties": { + "actions": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "administration": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "checks": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "content_references": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "contents": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "deployments": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "discussions": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "emails": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "environments": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "issues": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "keys": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "members": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "metadata": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "organization_administration": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "organization_hooks": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "organization_packages": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "organization_plan": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "organization_projects": { + "type": "string", + "enum": [ + "read", + "write", + "admin" + ] + }, + "organization_secrets": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "organization_self_hosted_runners": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "organization_user_blocking": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "packages": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "pages": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "pull_requests": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "repository_hooks": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "repository_projects": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "secret_scanning_alerts": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "secrets": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "security_events": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "security_scanning_alert": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "single_file": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "statuses": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "vulnerability_alerts": { + "type": "string", + "enum": [ + "read", + "write" + ] + }, + "workflows": { + "type": "string", + "enum": [ + "read", + "write" + ] + } + } + }, + "slug": { + "type": "string" + }, + "updated_at": { + "type": "string", + "nullable": true, + "format": "date-time" + } + }, + "required": [ + "id", + "node_id", + "owner", + "name", + "description", + "external_url", + "html_url", + "created_at", + "updated_at" + ] + }, + "pull_request": { + "type": "object", + "properties": { + "diff_url": { + "type": "string", + "format": "uri" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "merged_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "patch_url": { + "type": "string", + "format": "uri" + }, + "url": { + "type": "string", + "format": "uri" + } + } + }, + "reactions": { + "type": "object", + "properties": { + "+1": { + "type": "integer" + }, + "-1": { + "type": "integer" + }, + "confused": { + "type": "integer" + }, + "eyes": { + "type": "integer" + }, + "heart": { + "type": "integer" + }, + "hooray": { + "type": "integer" + }, + "laugh": { + "type": "integer" + }, + "rocket": { + "type": "integer" + }, + "total_count": { + "type": "integer" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "url", + "total_count", + "+1", + "-1", + "laugh", + "confused", + "heart", + "hooray", + "eyes", + "rocket" + ] + }, + "repository_url": { + "type": "string", + "format": "uri" + }, + "sub_issues_summary": { + "$ref": "#/components/schemas/sub-issues-summary" + }, + "issue_dependencies_summary": { + "$ref": "#/components/schemas/issue-dependencies-summary" + }, + "issue_field_values": { + "type": "array", + "items": { + "$ref": "#/components/schemas/issue-field-value" + } + }, + "state": { + "type": "string", + "enum": [ + "open", + "closed" + ] + }, + "state_reason": { + "type": "string", + "nullable": true + }, + "timeline_url": { + "type": "string", + "format": "uri" + }, + "title": { + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/issue-type" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "url": { + "type": "string", + "format": "uri" + }, + "pinned_comment": { + "$ref": "#/components/schemas/nullable-issue-comment" + }, + "user": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + } + }, + "required": [ + "url", + "repository_url", + "labels_url", + "comments_url", + "events_url", + "html_url", + "id", + "node_id", + "number", + "title", + "user", + "assignees", + "milestone", + "comments", + "created_at", + "updated_at", + "closed_at", + "author_association", + "active_lock_reason", + "body", + "reactions" + ] + }, + "organization": { + "$ref": "#/components/schemas/organization-simple-webhooks" + }, + "repository": { + "$ref": "#/components/schemas/repository-webhooks" + }, + "sender": { + "$ref": "#/components/schemas/simple-user" + } + }, + "required": [ + "action", + "issue", + "repository", + "sender" + ] + }, + "webhook-ping": { + "type": "object", + "properties": { + "hook": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "app_id": { + "type": "integer" + }, + "config": { + "type": "object", + "properties": { + "content_type": { + "$ref": "#/components/schemas/webhook-config-content-type" + }, + "insecure_ssl": { + "$ref": "#/components/schemas/webhook-config-insecure-ssl" + }, + "secret": { + "$ref": "#/components/schemas/webhook-config-secret" + }, + "url": { + "$ref": "#/components/schemas/webhook-config-url" + } + } + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "deliveries_url": { + "type": "string", + "format": "uri" + }, + "events": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "integer" + }, + "last_response": { + "$ref": "#/components/schemas/hook-response" + }, + "name": { + "type": "string", + "enum": [ + "web" + ] + }, + "ping_url": { + "type": "string", + "format": "uri" + }, + "test_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "id", + "type", + "name", + "active", + "events", + "config", + "created_at", + "updated_at" + ] + }, + "hook_id": { + "type": "integer" + }, + "organization": { + "$ref": "#/components/schemas/organization-simple-webhooks" + }, + "repository": { + "$ref": "#/components/schemas/repository-webhooks" + }, + "sender": { + "$ref": "#/components/schemas/simple-user" + }, + "zen": { + "type": "string" + } + } + }, + "webhook-ping-form-encoded": { + "type": "object", + "properties": { + "payload": { + "type": "string" + } + }, + "required": [ + "payload" + ] + }, + "webhook-pull-request-closed": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "closed" + ] + }, + "enterprise": { + "$ref": "#/components/schemas/enterprise-webhooks" + }, + "installation": { + "$ref": "#/components/schemas/simple-installation" + }, + "number": { + "$ref": "#/components/schemas/webhooks_number" + }, + "organization": { + "$ref": "#/components/schemas/organization-simple-webhooks" + }, + "pull_request": { + "$ref": "#/components/schemas/pull-request-webhook" + }, + "repository": { + "$ref": "#/components/schemas/repository-webhooks" + }, + "sender": { + "$ref": "#/components/schemas/simple-user" + } + }, + "required": [ + "action", + "number", + "pull_request", + "repository", + "sender" + ] + }, + "webhook-pull-request-opened": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "opened" + ] + }, + "enterprise": { + "$ref": "#/components/schemas/enterprise-webhooks" + }, + "installation": { + "$ref": "#/components/schemas/simple-installation" + }, + "number": { + "$ref": "#/components/schemas/webhooks_number" + }, + "organization": { + "$ref": "#/components/schemas/organization-simple-webhooks" + }, + "pull_request": { + "$ref": "#/components/schemas/pull-request-webhook" + }, + "repository": { + "$ref": "#/components/schemas/repository-webhooks" + }, + "sender": { + "$ref": "#/components/schemas/simple-user" + } + }, + "required": [ + "action", + "number", + "pull_request", + "repository", + "sender" + ] + }, + "webhook-pull-request-review-requested": { + "oneOf": [ + { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "review_requested" + ] + }, + "enterprise": { + "$ref": "#/components/schemas/enterprise-webhooks" + }, + "installation": { + "$ref": "#/components/schemas/simple-installation" + }, + "number": { + "type": "integer" + }, + "organization": { + "$ref": "#/components/schemas/organization-simple-webhooks" + }, + "pull_request": { + "type": "object", + "properties": { + "_links": { + "type": "object", + "properties": { + "comments": { + "type": "object", + "properties": { + "href": { + "type": "string", + "format": "uri-template" + } + }, + "required": [ + "href" + ] + }, + "commits": { + "type": "object", + "properties": { + "href": { + "type": "string", + "format": "uri-template" + } + }, + "required": [ + "href" + ] + }, + "html": { + "type": "object", + "properties": { + "href": { + "type": "string", + "format": "uri-template" + } + }, + "required": [ + "href" + ] + }, + "issue": { + "type": "object", + "properties": { + "href": { + "type": "string", + "format": "uri-template" + } + }, + "required": [ + "href" + ] + }, + "review_comment": { + "type": "object", + "properties": { + "href": { + "type": "string", + "format": "uri-template" + } + }, + "required": [ + "href" + ] + }, + "review_comments": { + "type": "object", + "properties": { + "href": { + "type": "string", + "format": "uri-template" + } + }, + "required": [ + "href" + ] + }, + "self": { + "type": "object", + "properties": { + "href": { + "type": "string", + "format": "uri-template" + } + }, + "required": [ + "href" + ] + }, + "statuses": { + "type": "object", + "properties": { + "href": { + "type": "string", + "format": "uri-template" + } + }, + "required": [ + "href" + ] + } + }, + "required": [ + "self", + "html", + "issue", + "comments", + "review_comments", + "review_comment", + "commits", + "statuses" + ] + }, + "active_lock_reason": { + "type": "string", + "nullable": true, + "enum": [ + "resolved", + "off-topic", + "too heated", + "spam", + null + ] + }, + "additions": { + "type": "integer" + }, + "assignee": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization", + "Mannequin" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "assignees": { + "type": "array", + "items": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization", + "Mannequin" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + } + }, + "author_association": { + "type": "string", + "enum": [ + "COLLABORATOR", + "CONTRIBUTOR", + "FIRST_TIMER", + "FIRST_TIME_CONTRIBUTOR", + "MANNEQUIN", + "MEMBER", + "NONE", + "OWNER" + ] + }, + "auto_merge": { + "type": "object", + "nullable": true, + "properties": { + "commit_message": { + "type": "string", + "nullable": true + }, + "commit_title": { + "type": "string", + "nullable": true + }, + "enabled_by": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "merge_method": { + "type": "string", + "enum": [ + "merge", + "squash", + "rebase" + ] + } + }, + "required": [ + "enabled_by", + "merge_method", + "commit_title", + "commit_message" + ] + }, + "base": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "ref": { + "type": "string" + }, + "repo": { + "type": "object", + "properties": { + "allow_auto_merge": { + "type": "boolean", + "default": false + }, + "allow_forking": { + "type": "boolean" + }, + "allow_merge_commit": { + "type": "boolean", + "default": true + }, + "allow_rebase_merge": { + "type": "boolean", + "default": true + }, + "allow_squash_merge": { + "type": "boolean", + "default": true + }, + "allow_update_branch": { + "type": "boolean" + }, + "archive_url": { + "type": "string", + "format": "uri-template" + }, + "archived": { + "type": "boolean", + "default": false + }, + "assignees_url": { + "type": "string", + "format": "uri-template" + }, + "blobs_url": { + "type": "string", + "format": "uri-template" + }, + "branches_url": { + "type": "string", + "format": "uri-template" + }, + "clone_url": { + "type": "string", + "format": "uri" + }, + "collaborators_url": { + "type": "string", + "format": "uri-template" + }, + "comments_url": { + "type": "string", + "format": "uri-template" + }, + "commits_url": { + "type": "string", + "format": "uri-template" + }, + "compare_url": { + "type": "string", + "format": "uri-template" + }, + "contents_url": { + "type": "string", + "format": "uri-template" + }, + "contributors_url": { + "type": "string", + "format": "uri" + }, + "created_at": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "default_branch": { + "type": "string" + }, + "delete_branch_on_merge": { + "type": "boolean", + "default": false + }, + "deployments_url": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string", + "nullable": true + }, + "disabled": { + "type": "boolean" + }, + "downloads_url": { + "type": "string", + "format": "uri" + }, + "events_url": { + "type": "string", + "format": "uri" + }, + "fork": { + "type": "boolean" + }, + "forks": { + "type": "integer" + }, + "forks_count": { + "type": "integer" + }, + "forks_url": { + "type": "string", + "format": "uri" + }, + "full_name": { + "type": "string" + }, + "git_commits_url": { + "type": "string", + "format": "uri-template" + }, + "git_refs_url": { + "type": "string", + "format": "uri-template" + }, + "git_tags_url": { + "type": "string", + "format": "uri-template" + }, + "git_url": { + "type": "string", + "format": "uri" + }, + "has_downloads": { + "type": "boolean", + "default": true + }, + "has_issues": { + "type": "boolean", + "default": true + }, + "has_pages": { + "type": "boolean" + }, + "has_projects": { + "type": "boolean", + "default": true + }, + "has_wiki": { + "type": "boolean", + "default": true + }, + "has_discussions": { + "type": "boolean", + "default": false + }, + "has_pull_requests": { + "type": "boolean", + "default": true + }, + "pull_request_creation_policy": { + "type": "string", + "enum": [ + "all", + "collaborators_only" + ] + }, + "homepage": { + "type": "string", + "nullable": true + }, + "hooks_url": { + "type": "string", + "format": "uri" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "is_template": { + "type": "boolean" + }, + "issue_comment_url": { + "type": "string", + "format": "uri-template" + }, + "issue_events_url": { + "type": "string", + "format": "uri-template" + }, + "issues_url": { + "type": "string", + "format": "uri-template" + }, + "keys_url": { + "type": "string", + "format": "uri-template" + }, + "labels_url": { + "type": "string", + "format": "uri-template" + }, + "language": { + "type": "string", + "nullable": true + }, + "languages_url": { + "type": "string", + "format": "uri" + }, + "license": { + "type": "object", + "nullable": true, + "properties": { + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "spdx_id": { + "type": "string" + }, + "url": { + "type": "string", + "nullable": true, + "format": "uri" + } + }, + "required": [ + "key", + "name", + "spdx_id", + "url", + "node_id" + ] + }, + "master_branch": { + "type": "string" + }, + "merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "PR_TITLE", + "BLANK" + ] + }, + "merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "MERGE_MESSAGE" + ] + }, + "merges_url": { + "type": "string", + "format": "uri" + }, + "milestones_url": { + "type": "string", + "format": "uri-template" + }, + "mirror_url": { + "type": "string", + "nullable": true, + "format": "uri" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "notifications_url": { + "type": "string", + "format": "uri-template" + }, + "open_issues": { + "type": "integer" + }, + "open_issues_count": { + "type": "integer" + }, + "organization": { + "type": "string" + }, + "owner": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "permissions": { + "type": "object", + "properties": { + "admin": { + "type": "boolean" + }, + "maintain": { + "type": "boolean" + }, + "pull": { + "type": "boolean" + }, + "push": { + "type": "boolean" + }, + "triage": { + "type": "boolean" + } + }, + "required": [ + "pull", + "push", + "admin" + ] + }, + "private": { + "type": "boolean" + }, + "public": { + "type": "boolean" + }, + "pulls_url": { + "type": "string", + "format": "uri-template" + }, + "pushed_at": { + "nullable": true, + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "releases_url": { + "type": "string", + "format": "uri-template" + }, + "role_name": { + "type": "string", + "nullable": true + }, + "size": { + "type": "integer" + }, + "squash_merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "COMMIT_MESSAGES", + "BLANK" + ] + }, + "squash_merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "COMMIT_OR_PR_TITLE" + ] + }, + "ssh_url": { + "type": "string" + }, + "stargazers": { + "type": "integer" + }, + "stargazers_count": { + "type": "integer" + }, + "stargazers_url": { + "type": "string", + "format": "uri" + }, + "statuses_url": { + "type": "string", + "format": "uri-template" + }, + "subscribers_url": { + "type": "string", + "format": "uri" + }, + "subscription_url": { + "type": "string", + "format": "uri" + }, + "svn_url": { + "type": "string", + "format": "uri" + }, + "tags_url": { + "type": "string", + "format": "uri" + }, + "teams_url": { + "type": "string", + "format": "uri" + }, + "topics": { + "type": "array", + "items": { + "type": "string" + } + }, + "trees_url": { + "type": "string", + "format": "uri-template" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "url": { + "type": "string", + "format": "uri" + }, + "use_squash_pr_title_as_default": { + "type": "boolean", + "default": false + }, + "visibility": { + "type": "string", + "enum": [ + "public", + "private", + "internal" + ] + }, + "watchers": { + "type": "integer" + }, + "watchers_count": { + "type": "integer" + }, + "web_commit_signoff_required": { + "type": "boolean" + } + }, + "required": [ + "id", + "node_id", + "name", + "full_name", + "private", + "owner", + "html_url", + "description", + "fork", + "url", + "forks_url", + "keys_url", + "collaborators_url", + "teams_url", + "hooks_url", + "issue_events_url", + "events_url", + "assignees_url", + "branches_url", + "tags_url", + "blobs_url", + "git_tags_url", + "git_refs_url", + "trees_url", + "statuses_url", + "languages_url", + "stargazers_url", + "contributors_url", + "subscribers_url", + "subscription_url", + "commits_url", + "git_commits_url", + "comments_url", + "issue_comment_url", + "contents_url", + "compare_url", + "merges_url", + "archive_url", + "downloads_url", + "issues_url", + "pulls_url", + "milestones_url", + "notifications_url", + "labels_url", + "releases_url", + "deployments_url", + "created_at", + "updated_at", + "pushed_at", + "git_url", + "ssh_url", + "clone_url", + "svn_url", + "homepage", + "size", + "stargazers_count", + "watchers_count", + "language", + "has_issues", + "has_projects", + "has_downloads", + "has_wiki", + "has_pages", + "has_discussions", + "forks_count", + "mirror_url", + "archived", + "open_issues_count", + "license", + "forks", + "open_issues", + "watchers", + "default_branch", + "topics", + "visibility" + ] + }, + "sha": { + "type": "string" + }, + "user": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + } + }, + "required": [ + "label", + "ref", + "sha", + "user", + "repo" + ] + }, + "body": { + "type": "string", + "nullable": true + }, + "changed_files": { + "type": "integer" + }, + "closed_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "comments": { + "type": "integer" + }, + "comments_url": { + "type": "string", + "format": "uri" + }, + "commits": { + "type": "integer" + }, + "commits_url": { + "type": "string", + "format": "uri" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "deletions": { + "type": "integer" + }, + "diff_url": { + "type": "string", + "format": "uri" + }, + "draft": { + "type": "boolean" + }, + "head": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "ref": { + "type": "string" + }, + "repo": { + "type": "object", + "properties": { + "allow_auto_merge": { + "type": "boolean", + "default": false + }, + "allow_forking": { + "type": "boolean" + }, + "allow_merge_commit": { + "type": "boolean", + "default": true + }, + "allow_rebase_merge": { + "type": "boolean", + "default": true + }, + "allow_squash_merge": { + "type": "boolean", + "default": true + }, + "allow_update_branch": { + "type": "boolean" + }, + "archive_url": { + "type": "string", + "format": "uri-template" + }, + "archived": { + "type": "boolean", + "default": false + }, + "assignees_url": { + "type": "string", + "format": "uri-template" + }, + "blobs_url": { + "type": "string", + "format": "uri-template" + }, + "branches_url": { + "type": "string", + "format": "uri-template" + }, + "clone_url": { + "type": "string", + "format": "uri" + }, + "collaborators_url": { + "type": "string", + "format": "uri-template" + }, + "comments_url": { + "type": "string", + "format": "uri-template" + }, + "commits_url": { + "type": "string", + "format": "uri-template" + }, + "compare_url": { + "type": "string", + "format": "uri-template" + }, + "contents_url": { + "type": "string", + "format": "uri-template" + }, + "contributors_url": { + "type": "string", + "format": "uri" + }, + "created_at": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "default_branch": { + "type": "string" + }, + "delete_branch_on_merge": { + "type": "boolean", + "default": false + }, + "deployments_url": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string", + "nullable": true + }, + "disabled": { + "type": "boolean" + }, + "downloads_url": { + "type": "string", + "format": "uri" + }, + "events_url": { + "type": "string", + "format": "uri" + }, + "fork": { + "type": "boolean" + }, + "forks": { + "type": "integer" + }, + "forks_count": { + "type": "integer" + }, + "forks_url": { + "type": "string", + "format": "uri" + }, + "full_name": { + "type": "string" + }, + "git_commits_url": { + "type": "string", + "format": "uri-template" + }, + "git_refs_url": { + "type": "string", + "format": "uri-template" + }, + "git_tags_url": { + "type": "string", + "format": "uri-template" + }, + "git_url": { + "type": "string", + "format": "uri" + }, + "has_downloads": { + "type": "boolean", + "default": true + }, + "has_issues": { + "type": "boolean", + "default": true + }, + "has_pages": { + "type": "boolean" + }, + "has_projects": { + "type": "boolean", + "default": true + }, + "has_wiki": { + "type": "boolean", + "default": true + }, + "has_discussions": { + "type": "boolean", + "default": false + }, + "has_pull_requests": { + "type": "boolean", + "default": true + }, + "pull_request_creation_policy": { + "type": "string", + "enum": [ + "all", + "collaborators_only" + ] + }, + "homepage": { + "type": "string", + "nullable": true + }, + "hooks_url": { + "type": "string", + "format": "uri" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "is_template": { + "type": "boolean" + }, + "issue_comment_url": { + "type": "string", + "format": "uri-template" + }, + "issue_events_url": { + "type": "string", + "format": "uri-template" + }, + "issues_url": { + "type": "string", + "format": "uri-template" + }, + "keys_url": { + "type": "string", + "format": "uri-template" + }, + "labels_url": { + "type": "string", + "format": "uri-template" + }, + "language": { + "type": "string", + "nullable": true + }, + "languages_url": { + "type": "string", + "format": "uri" + }, + "license": { + "type": "object", + "nullable": true, + "properties": { + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "spdx_id": { + "type": "string" + }, + "url": { + "type": "string", + "nullable": true, + "format": "uri" + } + }, + "required": [ + "key", + "name", + "spdx_id", + "url", + "node_id" + ] + }, + "master_branch": { + "type": "string" + }, + "merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "PR_TITLE", + "BLANK" + ] + }, + "merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "MERGE_MESSAGE" + ] + }, + "merges_url": { + "type": "string", + "format": "uri" + }, + "milestones_url": { + "type": "string", + "format": "uri-template" + }, + "mirror_url": { + "type": "string", + "nullable": true, + "format": "uri" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "notifications_url": { + "type": "string", + "format": "uri-template" + }, + "open_issues": { + "type": "integer" + }, + "open_issues_count": { + "type": "integer" + }, + "organization": { + "type": "string" + }, + "owner": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "permissions": { + "type": "object", + "properties": { + "admin": { + "type": "boolean" + }, + "maintain": { + "type": "boolean" + }, + "pull": { + "type": "boolean" + }, + "push": { + "type": "boolean" + }, + "triage": { + "type": "boolean" + } + }, + "required": [ + "pull", + "push", + "admin" + ] + }, + "private": { + "type": "boolean" + }, + "public": { + "type": "boolean" + }, + "pulls_url": { + "type": "string", + "format": "uri-template" + }, + "pushed_at": { + "nullable": true, + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "releases_url": { + "type": "string", + "format": "uri-template" + }, + "role_name": { + "type": "string", + "nullable": true + }, + "size": { + "type": "integer" + }, + "squash_merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "COMMIT_MESSAGES", + "BLANK" + ] + }, + "squash_merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "COMMIT_OR_PR_TITLE" + ] + }, + "ssh_url": { + "type": "string" + }, + "stargazers": { + "type": "integer" + }, + "stargazers_count": { + "type": "integer" + }, + "stargazers_url": { + "type": "string", + "format": "uri" + }, + "statuses_url": { + "type": "string", + "format": "uri-template" + }, + "subscribers_url": { + "type": "string", + "format": "uri" + }, + "subscription_url": { + "type": "string", + "format": "uri" + }, + "svn_url": { + "type": "string", + "format": "uri" + }, + "tags_url": { + "type": "string", + "format": "uri" + }, + "teams_url": { + "type": "string", + "format": "uri" + }, + "topics": { + "type": "array", + "items": { + "type": "string" + } + }, + "trees_url": { + "type": "string", + "format": "uri-template" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "url": { + "type": "string", + "format": "uri" + }, + "use_squash_pr_title_as_default": { + "type": "boolean", + "default": false + }, + "visibility": { + "type": "string", + "enum": [ + "public", + "private", + "internal" + ] + }, + "watchers": { + "type": "integer" + }, + "watchers_count": { + "type": "integer" + }, + "web_commit_signoff_required": { + "type": "boolean" + } + }, + "required": [ + "id", + "node_id", + "name", + "full_name", + "private", + "owner", + "html_url", + "description", + "fork", + "url", + "forks_url", + "keys_url", + "collaborators_url", + "teams_url", + "hooks_url", + "issue_events_url", + "events_url", + "assignees_url", + "branches_url", + "tags_url", + "blobs_url", + "git_tags_url", + "git_refs_url", + "trees_url", + "statuses_url", + "languages_url", + "stargazers_url", + "contributors_url", + "subscribers_url", + "subscription_url", + "commits_url", + "git_commits_url", + "comments_url", + "issue_comment_url", + "contents_url", + "compare_url", + "merges_url", + "archive_url", + "downloads_url", + "issues_url", + "pulls_url", + "milestones_url", + "notifications_url", + "labels_url", + "releases_url", + "deployments_url", + "created_at", + "updated_at", + "pushed_at", + "git_url", + "ssh_url", + "clone_url", + "svn_url", + "homepage", + "size", + "stargazers_count", + "watchers_count", + "language", + "has_issues", + "has_projects", + "has_downloads", + "has_wiki", + "has_pages", + "has_discussions", + "forks_count", + "mirror_url", + "archived", + "open_issues_count", + "license", + "forks", + "open_issues", + "watchers", + "default_branch", + "topics", + "visibility" + ] + }, + "sha": { + "type": "string" + }, + "user": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + } + }, + "required": [ + "label", + "ref", + "sha", + "user", + "repo" + ] + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "issue_url": { + "type": "string", + "format": "uri" + }, + "labels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "color": { + "type": "string" + }, + "default": { + "type": "boolean" + }, + "description": { + "type": "string", + "nullable": true + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "id", + "node_id", + "url", + "name", + "color", + "default", + "description" + ] + } + }, + "locked": { + "type": "boolean" + }, + "maintainer_can_modify": { + "type": "boolean" + }, + "merge_commit_sha": { + "type": "string", + "nullable": true + }, + "mergeable": { + "type": "boolean", + "nullable": true + }, + "mergeable_state": { + "type": "string" + }, + "merged": { + "type": "boolean", + "nullable": true + }, + "merged_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "merged_by": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "milestone": { + "type": "object", + "nullable": true, + "properties": { + "closed_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "closed_issues": { + "type": "integer" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "creator": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization", + "Mannequin" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "description": { + "type": "string", + "nullable": true + }, + "due_on": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "labels_url": { + "type": "string", + "format": "uri" + }, + "node_id": { + "type": "string" + }, + "number": { + "type": "integer" + }, + "open_issues": { + "type": "integer" + }, + "state": { + "type": "string", + "enum": [ + "open", + "closed" + ] + }, + "title": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "url", + "html_url", + "labels_url", + "id", + "node_id", + "number", + "title", + "description", + "creator", + "open_issues", + "closed_issues", + "state", + "created_at", + "updated_at", + "due_on", + "closed_at" + ] + }, + "node_id": { + "type": "string" + }, + "number": { + "type": "integer" + }, + "patch_url": { + "type": "string", + "format": "uri" + }, + "rebaseable": { + "type": "boolean", + "nullable": true + }, + "requested_reviewers": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + { + "type": "object", + "properties": { + "deleted": { + "type": "boolean" + }, + "description": { + "type": "string", + "nullable": true + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "members_url": { + "type": "string", + "format": "uri-template" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "parent": { + "type": "object", + "nullable": true, + "properties": { + "description": { + "type": "string", + "nullable": true + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "members_url": { + "type": "string", + "format": "uri-template" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "permission": { + "type": "string" + }, + "privacy": { + "type": "string", + "enum": [ + "open", + "closed", + "secret" + ] + }, + "repositories_url": { + "type": "string", + "format": "uri" + }, + "slug": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "name", + "id", + "node_id", + "slug", + "description", + "privacy", + "url", + "html_url", + "members_url", + "repositories_url", + "permission" + ] + }, + "permission": { + "type": "string" + }, + "privacy": { + "type": "string", + "enum": [ + "open", + "closed", + "secret" + ] + }, + "repositories_url": { + "type": "string", + "format": "uri" + }, + "slug": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "name", + "id", + "node_id", + "slug", + "description", + "privacy", + "url", + "html_url", + "members_url", + "repositories_url", + "permission" + ] + } + ] + } + }, + "requested_teams": { + "type": "array", + "items": { + "type": "object", + "properties": { + "deleted": { + "type": "boolean" + }, + "description": { + "type": "string", + "nullable": true + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "members_url": { + "type": "string", + "format": "uri-template" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "parent": { + "type": "object", + "nullable": true, + "properties": { + "description": { + "type": "string", + "nullable": true + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "members_url": { + "type": "string", + "format": "uri-template" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "permission": { + "type": "string" + }, + "privacy": { + "type": "string", + "enum": [ + "open", + "closed", + "secret" + ] + }, + "repositories_url": { + "type": "string", + "format": "uri" + }, + "slug": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "name", + "id", + "node_id", + "slug", + "description", + "privacy", + "url", + "html_url", + "members_url", + "repositories_url", + "permission" + ] + }, + "permission": { + "type": "string" + }, + "privacy": { + "type": "string", + "enum": [ + "open", + "closed", + "secret" + ] + }, + "repositories_url": { + "type": "string", + "format": "uri" + }, + "slug": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "name", + "id" + ] + } + }, + "review_comment_url": { + "type": "string", + "format": "uri-template" + }, + "review_comments": { + "type": "integer" + }, + "review_comments_url": { + "type": "string", + "format": "uri" + }, + "stack": { + "$ref": "#/components/schemas/pull-request-stack" + }, + "state": { + "type": "string", + "enum": [ + "open", + "closed" + ] + }, + "statuses_url": { + "type": "string", + "format": "uri" + }, + "title": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "url": { + "type": "string", + "format": "uri" + }, + "user": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization", + "Mannequin" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + } + }, + "required": [ + "url", + "id", + "node_id", + "html_url", + "diff_url", + "patch_url", + "issue_url", + "number", + "state", + "locked", + "title", + "user", + "body", + "created_at", + "updated_at", + "closed_at", + "merged_at", + "merge_commit_sha", + "assignee", + "assignees", + "requested_reviewers", + "requested_teams", + "labels", + "milestone", + "commits_url", + "review_comments_url", + "review_comment_url", + "comments_url", + "statuses_url", + "head", + "base", + "_links", + "author_association", + "auto_merge", + "active_lock_reason", + "draft" + ] + }, + "repository": { + "$ref": "#/components/schemas/repository-webhooks" + }, + "requested_reviewer": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization", + "Mannequin" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "sender": { + "$ref": "#/components/schemas/simple-user" + } + }, + "required": [ + "requested_reviewer", + "action", + "number", + "pull_request", + "repository", + "sender" + ] + }, + { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "review_requested" + ] + }, + "enterprise": { + "$ref": "#/components/schemas/enterprise-webhooks" + }, + "installation": { + "$ref": "#/components/schemas/simple-installation" + }, + "number": { + "type": "integer" + }, + "organization": { + "$ref": "#/components/schemas/organization-simple-webhooks" + }, + "pull_request": { + "type": "object", + "properties": { + "_links": { + "type": "object", + "properties": { + "comments": { + "type": "object", + "properties": { + "href": { + "type": "string", + "format": "uri-template" + } + }, + "required": [ + "href" + ] + }, + "commits": { + "type": "object", + "properties": { + "href": { + "type": "string", + "format": "uri-template" + } + }, + "required": [ + "href" + ] + }, + "html": { + "type": "object", + "properties": { + "href": { + "type": "string", + "format": "uri-template" + } + }, + "required": [ + "href" + ] + }, + "issue": { + "type": "object", + "properties": { + "href": { + "type": "string", + "format": "uri-template" + } + }, + "required": [ + "href" + ] + }, + "review_comment": { + "type": "object", + "properties": { + "href": { + "type": "string", + "format": "uri-template" + } + }, + "required": [ + "href" + ] + }, + "review_comments": { + "type": "object", + "properties": { + "href": { + "type": "string", + "format": "uri-template" + } + }, + "required": [ + "href" + ] + }, + "self": { + "type": "object", + "properties": { + "href": { + "type": "string", + "format": "uri-template" + } + }, + "required": [ + "href" + ] + }, + "statuses": { + "type": "object", + "properties": { + "href": { + "type": "string", + "format": "uri-template" + } + }, + "required": [ + "href" + ] + } + }, + "required": [ + "self", + "html", + "issue", + "comments", + "review_comments", + "review_comment", + "commits", + "statuses" + ] + }, + "active_lock_reason": { + "type": "string", + "nullable": true, + "enum": [ + "resolved", + "off-topic", + "too heated", + "spam", + null + ] + }, + "additions": { + "type": "integer" + }, + "assignee": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization", + "Mannequin" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "assignees": { + "type": "array", + "items": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization", + "Mannequin" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + } + }, + "author_association": { + "type": "string", + "enum": [ + "COLLABORATOR", + "CONTRIBUTOR", + "FIRST_TIMER", + "FIRST_TIME_CONTRIBUTOR", + "MANNEQUIN", + "MEMBER", + "NONE", + "OWNER" + ] + }, + "auto_merge": { + "type": "object", + "nullable": true, + "properties": { + "commit_message": { + "type": "string", + "nullable": true + }, + "commit_title": { + "type": "string", + "nullable": true + }, + "enabled_by": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "merge_method": { + "type": "string", + "enum": [ + "merge", + "squash", + "rebase" + ] + } + }, + "required": [ + "enabled_by", + "merge_method", + "commit_title", + "commit_message" + ] + }, + "base": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "ref": { + "type": "string" + }, + "repo": { + "type": "object", + "properties": { + "allow_auto_merge": { + "type": "boolean", + "default": false + }, + "allow_forking": { + "type": "boolean" + }, + "allow_merge_commit": { + "type": "boolean", + "default": true + }, + "allow_rebase_merge": { + "type": "boolean", + "default": true + }, + "allow_squash_merge": { + "type": "boolean", + "default": true + }, + "allow_update_branch": { + "type": "boolean" + }, + "archive_url": { + "type": "string", + "format": "uri-template" + }, + "archived": { + "type": "boolean", + "default": false + }, + "assignees_url": { + "type": "string", + "format": "uri-template" + }, + "blobs_url": { + "type": "string", + "format": "uri-template" + }, + "branches_url": { + "type": "string", + "format": "uri-template" + }, + "clone_url": { + "type": "string", + "format": "uri" + }, + "collaborators_url": { + "type": "string", + "format": "uri-template" + }, + "comments_url": { + "type": "string", + "format": "uri-template" + }, + "commits_url": { + "type": "string", + "format": "uri-template" + }, + "compare_url": { + "type": "string", + "format": "uri-template" + }, + "contents_url": { + "type": "string", + "format": "uri-template" + }, + "contributors_url": { + "type": "string", + "format": "uri" + }, + "created_at": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "default_branch": { + "type": "string" + }, + "delete_branch_on_merge": { + "type": "boolean", + "default": false + }, + "deployments_url": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string", + "nullable": true + }, + "disabled": { + "type": "boolean" + }, + "downloads_url": { + "type": "string", + "format": "uri" + }, + "events_url": { + "type": "string", + "format": "uri" + }, + "fork": { + "type": "boolean" + }, + "forks": { + "type": "integer" + }, + "forks_count": { + "type": "integer" + }, + "forks_url": { + "type": "string", + "format": "uri" + }, + "full_name": { + "type": "string" + }, + "git_commits_url": { + "type": "string", + "format": "uri-template" + }, + "git_refs_url": { + "type": "string", + "format": "uri-template" + }, + "git_tags_url": { + "type": "string", + "format": "uri-template" + }, + "git_url": { + "type": "string", + "format": "uri" + }, + "has_downloads": { + "type": "boolean", + "default": true + }, + "has_issues": { + "type": "boolean", + "default": true + }, + "has_pages": { + "type": "boolean" + }, + "has_projects": { + "type": "boolean", + "default": true + }, + "has_wiki": { + "type": "boolean", + "default": true + }, + "has_discussions": { + "type": "boolean", + "default": false + }, + "has_pull_requests": { + "type": "boolean", + "default": true + }, + "pull_request_creation_policy": { + "type": "string", + "enum": [ + "all", + "collaborators_only" + ] + }, + "homepage": { + "type": "string", + "nullable": true + }, + "hooks_url": { + "type": "string", + "format": "uri" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "is_template": { + "type": "boolean" + }, + "issue_comment_url": { + "type": "string", + "format": "uri-template" + }, + "issue_events_url": { + "type": "string", + "format": "uri-template" + }, + "issues_url": { + "type": "string", + "format": "uri-template" + }, + "keys_url": { + "type": "string", + "format": "uri-template" + }, + "labels_url": { + "type": "string", + "format": "uri-template" + }, + "language": { + "type": "string", + "nullable": true + }, + "languages_url": { + "type": "string", + "format": "uri" + }, + "license": { + "type": "object", + "nullable": true, + "properties": { + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "spdx_id": { + "type": "string" + }, + "url": { + "type": "string", + "nullable": true, + "format": "uri" + } + }, + "required": [ + "key", + "name", + "spdx_id", + "url", + "node_id" + ] + }, + "master_branch": { + "type": "string" + }, + "merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "PR_TITLE", + "BLANK" + ] + }, + "merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "MERGE_MESSAGE" + ] + }, + "merges_url": { + "type": "string", + "format": "uri" + }, + "milestones_url": { + "type": "string", + "format": "uri-template" + }, + "mirror_url": { + "type": "string", + "nullable": true, + "format": "uri" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "notifications_url": { + "type": "string", + "format": "uri-template" + }, + "open_issues": { + "type": "integer" + }, + "open_issues_count": { + "type": "integer" + }, + "organization": { + "type": "string" + }, + "owner": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "permissions": { + "type": "object", + "properties": { + "admin": { + "type": "boolean" + }, + "maintain": { + "type": "boolean" + }, + "pull": { + "type": "boolean" + }, + "push": { + "type": "boolean" + }, + "triage": { + "type": "boolean" + } + }, + "required": [ + "pull", + "push", + "admin" + ] + }, + "private": { + "type": "boolean" + }, + "public": { + "type": "boolean" + }, + "pulls_url": { + "type": "string", + "format": "uri-template" + }, + "pushed_at": { + "nullable": true, + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "releases_url": { + "type": "string", + "format": "uri-template" + }, + "role_name": { + "type": "string", + "nullable": true + }, + "size": { + "type": "integer" + }, + "squash_merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "COMMIT_MESSAGES", + "BLANK" + ] + }, + "squash_merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "COMMIT_OR_PR_TITLE" + ] + }, + "ssh_url": { + "type": "string" + }, + "stargazers": { + "type": "integer" + }, + "stargazers_count": { + "type": "integer" + }, + "stargazers_url": { + "type": "string", + "format": "uri" + }, + "statuses_url": { + "type": "string", + "format": "uri-template" + }, + "subscribers_url": { + "type": "string", + "format": "uri" + }, + "subscription_url": { + "type": "string", + "format": "uri" + }, + "svn_url": { + "type": "string", + "format": "uri" + }, + "tags_url": { + "type": "string", + "format": "uri" + }, + "teams_url": { + "type": "string", + "format": "uri" + }, + "topics": { + "type": "array", + "items": { + "type": "string" + } + }, + "trees_url": { + "type": "string", + "format": "uri-template" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "url": { + "type": "string", + "format": "uri" + }, + "use_squash_pr_title_as_default": { + "type": "boolean", + "default": false + }, + "visibility": { + "type": "string", + "enum": [ + "public", + "private", + "internal" + ] + }, + "watchers": { + "type": "integer" + }, + "watchers_count": { + "type": "integer" + }, + "web_commit_signoff_required": { + "type": "boolean" + } + }, + "required": [ + "id", + "node_id", + "name", + "full_name", + "private", + "owner", + "html_url", + "description", + "fork", + "url", + "forks_url", + "keys_url", + "collaborators_url", + "teams_url", + "hooks_url", + "issue_events_url", + "events_url", + "assignees_url", + "branches_url", + "tags_url", + "blobs_url", + "git_tags_url", + "git_refs_url", + "trees_url", + "statuses_url", + "languages_url", + "stargazers_url", + "contributors_url", + "subscribers_url", + "subscription_url", + "commits_url", + "git_commits_url", + "comments_url", + "issue_comment_url", + "contents_url", + "compare_url", + "merges_url", + "archive_url", + "downloads_url", + "issues_url", + "pulls_url", + "milestones_url", + "notifications_url", + "labels_url", + "releases_url", + "deployments_url", + "created_at", + "updated_at", + "pushed_at", + "git_url", + "ssh_url", + "clone_url", + "svn_url", + "homepage", + "size", + "stargazers_count", + "watchers_count", + "language", + "has_issues", + "has_projects", + "has_downloads", + "has_wiki", + "has_pages", + "has_discussions", + "forks_count", + "mirror_url", + "archived", + "open_issues_count", + "license", + "forks", + "open_issues", + "watchers", + "default_branch", + "topics", + "visibility" + ] + }, + "sha": { + "type": "string" + }, + "user": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + } + }, + "required": [ + "label", + "ref", + "sha", + "user", + "repo" + ] + }, + "body": { + "type": "string", + "nullable": true + }, + "changed_files": { + "type": "integer" + }, + "closed_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "comments": { + "type": "integer" + }, + "comments_url": { + "type": "string", + "format": "uri" + }, + "commits": { + "type": "integer" + }, + "commits_url": { + "type": "string", + "format": "uri" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "deletions": { + "type": "integer" + }, + "diff_url": { + "type": "string", + "format": "uri" + }, + "draft": { + "type": "boolean" + }, + "head": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "ref": { + "type": "string" + }, + "repo": { + "type": "object", + "properties": { + "allow_auto_merge": { + "type": "boolean", + "default": false + }, + "allow_forking": { + "type": "boolean" + }, + "allow_merge_commit": { + "type": "boolean", + "default": true + }, + "allow_rebase_merge": { + "type": "boolean", + "default": true + }, + "allow_squash_merge": { + "type": "boolean", + "default": true + }, + "allow_update_branch": { + "type": "boolean" + }, + "archive_url": { + "type": "string", + "format": "uri-template" + }, + "archived": { + "type": "boolean", + "default": false + }, + "assignees_url": { + "type": "string", + "format": "uri-template" + }, + "blobs_url": { + "type": "string", + "format": "uri-template" + }, + "branches_url": { + "type": "string", + "format": "uri-template" + }, + "clone_url": { + "type": "string", + "format": "uri" + }, + "collaborators_url": { + "type": "string", + "format": "uri-template" + }, + "comments_url": { + "type": "string", + "format": "uri-template" + }, + "commits_url": { + "type": "string", + "format": "uri-template" + }, + "compare_url": { + "type": "string", + "format": "uri-template" + }, + "contents_url": { + "type": "string", + "format": "uri-template" + }, + "contributors_url": { + "type": "string", + "format": "uri" + }, + "created_at": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "default_branch": { + "type": "string" + }, + "delete_branch_on_merge": { + "type": "boolean", + "default": false + }, + "deployments_url": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string", + "nullable": true + }, + "disabled": { + "type": "boolean" + }, + "downloads_url": { + "type": "string", + "format": "uri" + }, + "events_url": { + "type": "string", + "format": "uri" + }, + "fork": { + "type": "boolean" + }, + "forks": { + "type": "integer" + }, + "forks_count": { + "type": "integer" + }, + "forks_url": { + "type": "string", + "format": "uri" + }, + "full_name": { + "type": "string" + }, + "git_commits_url": { + "type": "string", + "format": "uri-template" + }, + "git_refs_url": { + "type": "string", + "format": "uri-template" + }, + "git_tags_url": { + "type": "string", + "format": "uri-template" + }, + "git_url": { + "type": "string", + "format": "uri" + }, + "has_downloads": { + "type": "boolean", + "default": true + }, + "has_issues": { + "type": "boolean", + "default": true + }, + "has_pages": { + "type": "boolean" + }, + "has_projects": { + "type": "boolean", + "default": true + }, + "has_wiki": { + "type": "boolean", + "default": true + }, + "has_discussions": { + "type": "boolean", + "default": false + }, + "has_pull_requests": { + "type": "boolean", + "default": true + }, + "pull_request_creation_policy": { + "type": "string", + "enum": [ + "all", + "collaborators_only" + ] + }, + "homepage": { + "type": "string", + "nullable": true + }, + "hooks_url": { + "type": "string", + "format": "uri" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "is_template": { + "type": "boolean" + }, + "issue_comment_url": { + "type": "string", + "format": "uri-template" + }, + "issue_events_url": { + "type": "string", + "format": "uri-template" + }, + "issues_url": { + "type": "string", + "format": "uri-template" + }, + "keys_url": { + "type": "string", + "format": "uri-template" + }, + "labels_url": { + "type": "string", + "format": "uri-template" + }, + "language": { + "type": "string", + "nullable": true + }, + "languages_url": { + "type": "string", + "format": "uri" + }, + "license": { + "type": "object", + "nullable": true, + "properties": { + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "spdx_id": { + "type": "string" + }, + "url": { + "type": "string", + "nullable": true, + "format": "uri" + } + }, + "required": [ + "key", + "name", + "spdx_id", + "url", + "node_id" + ] + }, + "master_branch": { + "type": "string" + }, + "merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "PR_TITLE", + "BLANK" + ] + }, + "merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "MERGE_MESSAGE" + ] + }, + "merges_url": { + "type": "string", + "format": "uri" + }, + "milestones_url": { + "type": "string", + "format": "uri-template" + }, + "mirror_url": { + "type": "string", + "nullable": true, + "format": "uri" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "notifications_url": { + "type": "string", + "format": "uri-template" + }, + "open_issues": { + "type": "integer" + }, + "open_issues_count": { + "type": "integer" + }, + "organization": { + "type": "string" + }, + "owner": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "permissions": { + "type": "object", + "properties": { + "admin": { + "type": "boolean" + }, + "maintain": { + "type": "boolean" + }, + "pull": { + "type": "boolean" + }, + "push": { + "type": "boolean" + }, + "triage": { + "type": "boolean" + } + }, + "required": [ + "pull", + "push", + "admin" + ] + }, + "private": { + "type": "boolean" + }, + "public": { + "type": "boolean" + }, + "pulls_url": { + "type": "string", + "format": "uri-template" + }, + "pushed_at": { + "nullable": true, + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "releases_url": { + "type": "string", + "format": "uri-template" + }, + "role_name": { + "type": "string", + "nullable": true + }, + "size": { + "type": "integer" + }, + "squash_merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "COMMIT_MESSAGES", + "BLANK" + ] + }, + "squash_merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "COMMIT_OR_PR_TITLE" + ] + }, + "ssh_url": { + "type": "string" + }, + "stargazers": { + "type": "integer" + }, + "stargazers_count": { + "type": "integer" + }, + "stargazers_url": { + "type": "string", + "format": "uri" + }, + "statuses_url": { + "type": "string", + "format": "uri-template" + }, + "subscribers_url": { + "type": "string", + "format": "uri" + }, + "subscription_url": { + "type": "string", + "format": "uri" + }, + "svn_url": { + "type": "string", + "format": "uri" + }, + "tags_url": { + "type": "string", + "format": "uri" + }, + "teams_url": { + "type": "string", + "format": "uri" + }, + "topics": { + "type": "array", + "items": { + "type": "string" + } + }, + "trees_url": { + "type": "string", + "format": "uri-template" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "url": { + "type": "string", + "format": "uri" + }, + "use_squash_pr_title_as_default": { + "type": "boolean", + "default": false + }, + "visibility": { + "type": "string", + "enum": [ + "public", + "private", + "internal" + ] + }, + "watchers": { + "type": "integer" + }, + "watchers_count": { + "type": "integer" + }, + "web_commit_signoff_required": { + "type": "boolean" + } + }, + "required": [ + "id", + "node_id", + "name", + "full_name", + "private", + "owner", + "html_url", + "description", + "fork", + "url", + "forks_url", + "keys_url", + "collaborators_url", + "teams_url", + "hooks_url", + "issue_events_url", + "events_url", + "assignees_url", + "branches_url", + "tags_url", + "blobs_url", + "git_tags_url", + "git_refs_url", + "trees_url", + "statuses_url", + "languages_url", + "stargazers_url", + "contributors_url", + "subscribers_url", + "subscription_url", + "commits_url", + "git_commits_url", + "comments_url", + "issue_comment_url", + "contents_url", + "compare_url", + "merges_url", + "archive_url", + "downloads_url", + "issues_url", + "pulls_url", + "milestones_url", + "notifications_url", + "labels_url", + "releases_url", + "deployments_url", + "created_at", + "updated_at", + "pushed_at", + "git_url", + "ssh_url", + "clone_url", + "svn_url", + "homepage", + "size", + "stargazers_count", + "watchers_count", + "language", + "has_issues", + "has_projects", + "has_downloads", + "has_wiki", + "has_pages", + "has_discussions", + "forks_count", + "mirror_url", + "archived", + "open_issues_count", + "license", + "forks", + "open_issues", + "watchers", + "default_branch", + "topics", + "visibility" + ] + }, + "sha": { + "type": "string" + }, + "user": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + } + }, + "required": [ + "label", + "ref", + "sha", + "user", + "repo" + ] + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "issue_url": { + "type": "string", + "format": "uri" + }, + "labels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "color": { + "type": "string" + }, + "default": { + "type": "boolean" + }, + "description": { + "type": "string", + "nullable": true + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "id", + "node_id", + "url", + "name", + "color", + "default", + "description" + ] + } + }, + "locked": { + "type": "boolean" + }, + "maintainer_can_modify": { + "type": "boolean" + }, + "merge_commit_sha": { + "type": "string", + "nullable": true + }, + "mergeable": { + "type": "boolean", + "nullable": true + }, + "mergeable_state": { + "type": "string" + }, + "merged": { + "type": "boolean", + "nullable": true + }, + "merged_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "merged_by": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "milestone": { + "type": "object", + "nullable": true, + "properties": { + "closed_at": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "closed_issues": { + "type": "integer" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "creator": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization", + "Mannequin" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "description": { + "type": "string", + "nullable": true + }, + "due_on": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "labels_url": { + "type": "string", + "format": "uri" + }, + "node_id": { + "type": "string" + }, + "number": { + "type": "integer" + }, + "open_issues": { + "type": "integer" + }, + "state": { + "type": "string", + "enum": [ + "open", + "closed" + ] + }, + "title": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "url", + "html_url", + "labels_url", + "id", + "node_id", + "number", + "title", + "description", + "creator", + "open_issues", + "closed_issues", + "state", + "created_at", + "updated_at", + "due_on", + "closed_at" + ] + }, + "node_id": { + "type": "string" + }, + "number": { + "type": "integer" + }, + "patch_url": { + "type": "string", + "format": "uri" + }, + "rebaseable": { + "type": "boolean", + "nullable": true + }, + "requested_reviewers": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + { + "type": "object", + "properties": { + "deleted": { + "type": "boolean" + }, + "description": { + "type": "string", + "nullable": true + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "members_url": { + "type": "string", + "format": "uri-template" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "parent": { + "type": "object", + "nullable": true, + "properties": { + "description": { + "type": "string", + "nullable": true + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "members_url": { + "type": "string", + "format": "uri-template" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "permission": { + "type": "string" + }, + "privacy": { + "type": "string", + "enum": [ + "open", + "closed", + "secret" + ] + }, + "repositories_url": { + "type": "string", + "format": "uri" + }, + "slug": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "name", + "id", + "node_id", + "slug", + "description", + "privacy", + "url", + "html_url", + "members_url", + "repositories_url", + "permission" + ] + }, + "permission": { + "type": "string" + }, + "privacy": { + "type": "string", + "enum": [ + "open", + "closed", + "secret" + ] + }, + "repositories_url": { + "type": "string", + "format": "uri" + }, + "slug": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "name", + "id" + ] + } + ] + } + }, + "requested_teams": { + "type": "array", + "items": { + "type": "object", + "properties": { + "deleted": { + "type": "boolean" + }, + "description": { + "type": "string", + "nullable": true + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "members_url": { + "type": "string", + "format": "uri-template" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "parent": { + "type": "object", + "nullable": true, + "properties": { + "description": { + "type": "string", + "nullable": true + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "members_url": { + "type": "string", + "format": "uri-template" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "permission": { + "type": "string" + }, + "privacy": { + "type": "string", + "enum": [ + "open", + "closed", + "secret" + ] + }, + "repositories_url": { + "type": "string", + "format": "uri" + }, + "slug": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "name", + "id", + "node_id", + "slug", + "description", + "privacy", + "url", + "html_url", + "members_url", + "repositories_url", + "permission" + ] + }, + "permission": { + "type": "string" + }, + "privacy": { + "type": "string", + "enum": [ + "open", + "closed", + "secret" + ] + }, + "repositories_url": { + "type": "string", + "format": "uri" + }, + "slug": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "name", + "id" + ] + } + }, + "review_comment_url": { + "type": "string", + "format": "uri-template" + }, + "review_comments": { + "type": "integer" + }, + "review_comments_url": { + "type": "string", + "format": "uri" + }, + "stack": { + "$ref": "#/components/schemas/pull-request-stack" + }, + "state": { + "type": "string", + "enum": [ + "open", + "closed" + ] + }, + "statuses_url": { + "type": "string", + "format": "uri" + }, + "title": { + "type": "string" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "url": { + "type": "string", + "format": "uri" + }, + "user": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization", + "Mannequin" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + } + }, + "required": [ + "url", + "id", + "node_id", + "html_url", + "diff_url", + "patch_url", + "issue_url", + "number", + "state", + "locked", + "title", + "user", + "body", + "created_at", + "updated_at", + "closed_at", + "merged_at", + "merge_commit_sha", + "assignee", + "assignees", + "requested_reviewers", + "requested_teams", + "labels", + "milestone", + "commits_url", + "review_comments_url", + "review_comment_url", + "comments_url", + "statuses_url", + "head", + "base", + "_links", + "author_association", + "auto_merge", + "active_lock_reason", + "draft" + ] + }, + "repository": { + "$ref": "#/components/schemas/repository-webhooks" + }, + "requested_team": { + "type": "object", + "properties": { + "deleted": { + "type": "boolean" + }, + "description": { + "type": "string", + "nullable": true + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "members_url": { + "type": "string", + "format": "uri-template" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "parent": { + "type": "object", + "nullable": true, + "properties": { + "description": { + "type": "string", + "nullable": true + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "members_url": { + "type": "string", + "format": "uri-template" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "permission": { + "type": "string" + }, + "privacy": { + "type": "string", + "enum": [ + "open", + "closed", + "secret" + ] + }, + "repositories_url": { + "type": "string", + "format": "uri" + }, + "slug": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "name", + "id", + "node_id", + "slug", + "description", + "privacy", + "url", + "html_url", + "members_url", + "repositories_url", + "permission" + ] + }, + "permission": { + "type": "string" + }, + "privacy": { + "type": "string", + "enum": [ + "open", + "closed", + "secret" + ] + }, + "repositories_url": { + "type": "string", + "format": "uri" + }, + "slug": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "name", + "id" + ] + }, + "sender": { + "$ref": "#/components/schemas/simple-user" + } + }, + "required": [ + "requested_team", + "action", + "number", + "pull_request", + "repository", + "sender" + ] + } + ] + }, + "webhook-push": { + "type": "object", + "properties": { + "after": { + "type": "string" + }, + "base_ref": { + "$ref": "#/components/schemas/webhooks_nullable_string" + }, + "before": { + "type": "string" + }, + "commits": { + "type": "array", + "items": { + "type": "object", + "properties": { + "added": { + "type": "array", + "items": { + "type": "string" + } + }, + "author": { + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "date-time" + }, + "email": { + "type": "string", + "nullable": true, + "format": "email" + }, + "name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "required": [ + "email", + "name" + ] + }, + "committer": { + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "date-time" + }, + "email": { + "type": "string", + "nullable": true, + "format": "email" + }, + "name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "required": [ + "email", + "name" + ] + }, + "distinct": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "message": { + "type": "string" + }, + "modified": { + "type": "array", + "items": { + "type": "string" + } + }, + "removed": { + "type": "array", + "items": { + "type": "string" + } + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "tree_id": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "id", + "tree_id", + "distinct", + "message", + "timestamp", + "url", + "author", + "committer" + ] + } + }, + "compare": { + "type": "string" + }, + "created": { + "type": "boolean" + }, + "deleted": { + "type": "boolean" + }, + "enterprise": { + "$ref": "#/components/schemas/enterprise-webhooks" + }, + "forced": { + "type": "boolean" + }, + "head_commit": { + "type": "object", + "nullable": true, + "properties": { + "added": { + "type": "array", + "items": { + "type": "string" + } + }, + "author": { + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "date-time" + }, + "email": { + "type": "string", + "nullable": true, + "format": "email" + }, + "name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "required": [ + "email", + "name" + ] + }, + "committer": { + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "date-time" + }, + "email": { + "type": "string", + "nullable": true, + "format": "email" + }, + "name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "required": [ + "email", + "name" + ] + }, + "distinct": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "message": { + "type": "string" + }, + "modified": { + "type": "array", + "items": { + "type": "string" + } + }, + "removed": { + "type": "array", + "items": { + "type": "string" + } + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "tree_id": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "id", + "tree_id", + "distinct", + "message", + "timestamp", + "url", + "author", + "committer" + ] + }, + "installation": { + "$ref": "#/components/schemas/simple-installation" + }, + "organization": { + "$ref": "#/components/schemas/organization-simple-webhooks" + }, + "pusher": { + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "date-time" + }, + "email": { + "type": "string", + "nullable": true, + "format": "email" + }, + "name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "ref": { + "type": "string" + }, + "repository": { + "type": "object", + "properties": { + "allow_auto_merge": { + "type": "boolean", + "default": false + }, + "allow_forking": { + "type": "boolean" + }, + "allow_merge_commit": { + "type": "boolean", + "default": true + }, + "allow_rebase_merge": { + "type": "boolean", + "default": true + }, + "allow_squash_merge": { + "type": "boolean", + "default": true + }, + "allow_update_branch": { + "type": "boolean" + }, + "archive_url": { + "type": "string", + "format": "uri-template" + }, + "archived": { + "type": "boolean", + "default": false + }, + "assignees_url": { + "type": "string", + "format": "uri-template" + }, + "blobs_url": { + "type": "string", + "format": "uri-template" + }, + "branches_url": { + "type": "string", + "format": "uri-template" + }, + "clone_url": { + "type": "string", + "format": "uri" + }, + "collaborators_url": { + "type": "string", + "format": "uri-template" + }, + "comments_url": { + "type": "string", + "format": "uri-template" + }, + "commits_url": { + "type": "string", + "format": "uri-template" + }, + "compare_url": { + "type": "string", + "format": "uri-template" + }, + "contents_url": { + "type": "string", + "format": "uri-template" + }, + "contributors_url": { + "type": "string", + "format": "uri" + }, + "created_at": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "custom_properties": { + "type": "object", + "additionalProperties": true + }, + "default_branch": { + "type": "string" + }, + "delete_branch_on_merge": { + "type": "boolean", + "default": false + }, + "deployments_url": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string", + "nullable": true + }, + "disabled": { + "type": "boolean" + }, + "downloads_url": { + "type": "string", + "format": "uri" + }, + "events_url": { + "type": "string", + "format": "uri" + }, + "fork": { + "type": "boolean" + }, + "forks": { + "type": "integer" + }, + "forks_count": { + "type": "integer" + }, + "forks_url": { + "type": "string", + "format": "uri" + }, + "full_name": { + "type": "string" + }, + "git_commits_url": { + "type": "string", + "format": "uri-template" + }, + "git_refs_url": { + "type": "string", + "format": "uri-template" + }, + "git_tags_url": { + "type": "string", + "format": "uri-template" + }, + "git_url": { + "type": "string", + "format": "uri" + }, + "has_downloads": { + "type": "boolean", + "default": true + }, + "has_issues": { + "type": "boolean", + "default": true + }, + "has_pages": { + "type": "boolean" + }, + "has_projects": { + "type": "boolean", + "default": true + }, + "has_wiki": { + "type": "boolean", + "default": true + }, + "has_discussions": { + "type": "boolean", + "default": false + }, + "has_pull_requests": { + "type": "boolean", + "default": true + }, + "pull_request_creation_policy": { + "type": "string", + "enum": [ + "all", + "collaborators_only" + ] + }, + "homepage": { + "type": "string", + "nullable": true + }, + "hooks_url": { + "type": "string", + "format": "uri" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer", + "format": "int64" + }, + "is_template": { + "type": "boolean" + }, + "issue_comment_url": { + "type": "string", + "format": "uri-template" + }, + "issue_events_url": { + "type": "string", + "format": "uri-template" + }, + "issues_url": { + "type": "string", + "format": "uri-template" + }, + "keys_url": { + "type": "string", + "format": "uri-template" + }, + "labels_url": { + "type": "string", + "format": "uri-template" + }, + "language": { + "type": "string", + "nullable": true + }, + "languages_url": { + "type": "string", + "format": "uri" + }, + "license": { + "type": "object", + "nullable": true, + "properties": { + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "spdx_id": { + "type": "string" + }, + "url": { + "type": "string", + "nullable": true, + "format": "uri" + } + }, + "required": [ + "key", + "name", + "spdx_id", + "url", + "node_id" + ] + }, + "master_branch": { + "type": "string" + }, + "merges_url": { + "type": "string", + "format": "uri" + }, + "milestones_url": { + "type": "string", + "format": "uri-template" + }, + "mirror_url": { + "type": "string", + "nullable": true, + "format": "uri" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "notifications_url": { + "type": "string", + "format": "uri-template" + }, + "open_issues": { + "type": "integer" + }, + "open_issues_count": { + "type": "integer" + }, + "organization": { + "type": "string" + }, + "owner": { + "type": "object", + "nullable": true, + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "type": "string", + "nullable": true + }, + "events_url": { + "type": "string", + "format": "uri-template" + }, + "followers_url": { + "type": "string", + "format": "uri" + }, + "following_url": { + "type": "string", + "format": "uri-template" + }, + "gists_url": { + "type": "string", + "format": "uri-template" + }, + "gravatar_id": { + "type": "string" + }, + "html_url": { + "type": "string", + "format": "uri" + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_id": { + "type": "string" + }, + "organizations_url": { + "type": "string", + "format": "uri" + }, + "received_events_url": { + "type": "string", + "format": "uri" + }, + "repos_url": { + "type": "string", + "format": "uri" + }, + "site_admin": { + "type": "boolean" + }, + "starred_url": { + "type": "string", + "format": "uri-template" + }, + "subscriptions_url": { + "type": "string", + "format": "uri" + }, + "type": { + "type": "string", + "enum": [ + "Bot", + "User", + "Organization" + ] + }, + "url": { + "type": "string", + "format": "uri" + }, + "user_view_type": { + "type": "string" + } + }, + "required": [ + "login", + "id" + ] + }, + "permissions": { + "type": "object", + "properties": { + "admin": { + "type": "boolean" + }, + "maintain": { + "type": "boolean" + }, + "pull": { + "type": "boolean" + }, + "push": { + "type": "boolean" + }, + "triage": { + "type": "boolean" + } + }, + "required": [ + "pull", + "push", + "admin" + ] + }, + "private": { + "type": "boolean" + }, + "public": { + "type": "boolean" + }, + "pulls_url": { + "type": "string", + "format": "uri-template" + }, + "pushed_at": { + "nullable": true, + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "releases_url": { + "type": "string", + "format": "uri-template" + }, + "role_name": { + "type": "string", + "nullable": true + }, + "size": { + "type": "integer" + }, + "ssh_url": { + "type": "string" + }, + "stargazers": { + "type": "integer" + }, + "stargazers_count": { + "type": "integer" + }, + "stargazers_url": { + "type": "string", + "format": "uri" + }, + "statuses_url": { + "type": "string", + "format": "uri-template" + }, + "subscribers_url": { + "type": "string", + "format": "uri" + }, + "subscription_url": { + "type": "string", + "format": "uri" + }, + "svn_url": { + "type": "string", + "format": "uri" + }, + "tags_url": { + "type": "string", + "format": "uri" + }, + "teams_url": { + "type": "string", + "format": "uri" + }, + "topics": { + "type": "array", + "items": { + "type": "string" + } + }, + "trees_url": { + "type": "string", + "format": "uri-template" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "url": { + "type": "string", + "format": "uri" + }, + "visibility": { + "type": "string", + "enum": [ + "public", + "private", + "internal" + ] + }, + "watchers": { + "type": "integer" + }, + "watchers_count": { + "type": "integer" + }, + "web_commit_signoff_required": { + "type": "boolean" + } + }, + "required": [ + "id", + "node_id", + "name", + "full_name", + "private", + "owner", + "html_url", + "description", + "fork", + "url", + "forks_url", + "keys_url", + "collaborators_url", + "teams_url", + "hooks_url", + "issue_events_url", + "events_url", + "assignees_url", + "branches_url", + "tags_url", + "blobs_url", + "git_tags_url", + "git_refs_url", + "trees_url", + "statuses_url", + "languages_url", + "stargazers_url", + "contributors_url", + "subscribers_url", + "subscription_url", + "commits_url", + "git_commits_url", + "comments_url", + "issue_comment_url", + "contents_url", + "compare_url", + "merges_url", + "archive_url", + "downloads_url", + "issues_url", + "pulls_url", + "milestones_url", + "notifications_url", + "labels_url", + "releases_url", + "deployments_url", + "created_at", + "updated_at", + "pushed_at", + "git_url", + "ssh_url", + "clone_url", + "svn_url", + "homepage", + "size", + "stargazers_count", + "watchers_count", + "language", + "has_issues", + "has_projects", + "has_downloads", + "has_wiki", + "has_pages", + "has_discussions", + "forks_count", + "mirror_url", + "archived", + "open_issues_count", + "license", + "forks", + "open_issues", + "watchers", + "default_branch", + "topics", + "visibility" + ] + }, + "sender": { + "$ref": "#/components/schemas/simple-user" + } + }, + "required": [ + "ref", + "before", + "after", + "created", + "deleted", + "forced", + "base_ref", + "compare", + "commits", + "head_commit", + "repository", + "pusher" + ] + }, + "webhooks_nullable_string": { + "type": "string", + "nullable": true + }, + "webhooks_number": { + "type": "integer" + } + } + } +} diff --git a/e2e/integration/fake-github/contract/specs/schema.graphql b/e2e/integration/fake-github/contract/specs/schema.graphql new file mode 100644 index 000000000..263b2a77f --- /dev/null +++ b/e2e/integration/fake-github/contract/specs/schema.graphql @@ -0,0 +1,153 @@ +interface Actor { + login: String! +} + +enum CommentAuthorAssociation { + COLLABORATOR + CONTRIBUTOR + FIRST_TIMER + FIRST_TIME_CONTRIBUTOR + MANNEQUIN + MEMBER + NONE + OWNER +} + +scalar DateTime + +type Issue { + author: Actor + authorAssociation: CommentAuthorAssociation! + body: String! + createdAt: DateTime! + labels(after: String, before: String, first: Int, last: Int, orderBy: LabelOrder = {direction: ASC, field: CREATED_AT}): LabelConnection + milestone: Milestone + number: Int! + repository: Repository! + state: IssueState! + title: String! + updatedAt: DateTime! + url: URI! +} + +enum IssueState { + CLOSED + OPEN +} + +type Label { + color: String! + name: String! +} + +type LabelConnection { + nodes: [Label] +} + +input LabelOrder { + direction: OrderDirection! + field: LabelOrderField! +} + +enum LabelOrderField { + CREATED_AT + ISSUE_COUNT + NAME +} + +type Milestone { + title: String! +} + +enum OrderDirection { + ASC + DESC +} + +type Organization implements Actor & RepositoryOwner { + login: String! +} + +type PageInfo { + endCursor: String + hasNextPage: Boolean! +} + +type PullRequest { + additions: Int! + author: Actor + authorAssociation: CommentAuthorAssociation! + body: String! + changedFiles: Int! + createdAt: DateTime! + deletions: Int! + labels(after: String, before: String, first: Int, last: Int, orderBy: LabelOrder = {direction: ASC, field: CREATED_AT}): LabelConnection + milestone: Milestone + number: Int! + repository: Repository! + reviewRequests(after: String, before: String, first: Int, last: Int): ReviewRequestConnection + state: PullRequestState! + title: String! + updatedAt: DateTime! + url: URI! +} + +enum PullRequestState { + CLOSED + MERGED + OPEN +} + +type Query { + search(after: String, before: String, first: Int, last: Int, query: String!, type: SearchType!): SearchResultItemConnection! +} + +type Repository { + name: String! + nameWithOwner: String! + owner: RepositoryOwner! + url: URI! +} + +interface RepositoryOwner { + login: String! +} + +union RequestedReviewer = Team | User + +type ReviewRequest { + requestedReviewer: RequestedReviewer +} + +type ReviewRequestConnection { + nodes: [ReviewRequest] +} + +union SearchResultItem = Issue | Organization | PullRequest | Repository | User + +type SearchResultItemConnection { + issueCount: Int! + nodes: [SearchResultItem] + pageInfo: PageInfo! +} + +enum SearchType { + DISCUSSION + ISSUE + ISSUE_ADVANCED + ISSUE_HYBRID + ISSUE_SEMANTIC + REPOSITORY + USER +} + +type Team { + organization: Organization! + slug: String! +} + +scalar URI + +type User implements Actor & RepositoryOwner { + login: String! +} diff --git a/e2e/integration/fake-github/contract/update_specs.mjs b/e2e/integration/fake-github/contract/update_specs.mjs new file mode 100644 index 000000000..5cba3dcc3 --- /dev/null +++ b/e2e/integration/fake-github/contract/update_specs.mjs @@ -0,0 +1,193 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +// Trims GitHub's published REST description (OpenAPI, with webhook payloads) and GraphQL schema +// down to what fake-github serves. +// node update_specs.mjs rewrite specs/ from upstream +// node update_specs.mjs --check exit 1 if upstream drifted from the committed copies + +/* eslint-disable no-console */ + +import fs from 'node:fs/promises'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; + +import {Kind, assertValidSchema, buildASTSchema, lexicographicSortSchema, parse, printSchema, specifiedScalarTypes} from 'graphql'; + +const dir = path.dirname(fileURLToPath(import.meta.url)); +const specsDir = path.join(dir, 'specs'); +const check = process.argv.includes('--check'); +const manifest = JSON.parse(await fs.readFile(path.join(dir, 'operations.json'), 'utf8')); + +const outputs = { + 'rest.json': `${JSON.stringify(trimRest(await download(manifest.rest.url, 'json'), manifest.rest), null, 2)}\n`, + 'schema.graphql': trimGraphQL(await download(manifest.graphql.url, 'text'), manifest.graphql.fields), +}; + +let drifted = false; +for (const [file, next] of Object.entries(outputs)) { + const outPath = path.join(specsDir, file); + if (check) { + if (await fs.readFile(outPath, 'utf8').catch(() => '') !== next) { + drifted = true; + console.error(`${file}: GitHub changed what fake-github serves`); + } + continue; + } + await fs.mkdir(specsDir, {recursive: true}); + await fs.writeFile(outPath, next); + console.log(`${file}: updated`); +} + +if (drifted) { + console.error('Run `npm run contract:update`, review the diff, and update fake-github to match.'); + process.exit(1); +} + +async function download(url, as) { + const res = await fetch(url); + if (!res.ok) { + throw new Error(`Failed to download ${url}: ${res.status}`); + } + return as === 'json' ? res.json() : res.text(); +} + +function trimRest(spec, {operations, webhooks}) { + const paths = {}; + for (const operation of operations) { + const [method, opPath] = operation.split(' '); + const op = spec.paths[opPath]?.[method.toLowerCase()]; + if (!op) { + throw new Error(`rest: ${operation} is not in the upstream description`); + } + paths[opPath] = {...paths[opPath], [method.toLowerCase()]: stripDocs({requestBody: op.requestBody, responses: op.responses})}; + } + + const hooks = {}; + for (const name of webhooks) { + const op = spec['x-webhooks'][name]?.post; + if (!op) { + throw new Error(`rest: webhook ${name} is not in the upstream description`); + } + hooks[name] = {post: stripDocs({requestBody: op.requestBody})}; + } + + const components = {}; + const queue = collectRefs([paths, hooks]); + while (queue.length) { + const [kind, name] = queue.pop().replace('#/components/', '').split('/'); + if (components[kind]?.[name]) { + continue; + } + components[kind] = {...components[kind], [name]: stripDocs(spec.components[kind][name])}; + queue.push(...collectRefs(components[kind][name])); + } + + return { + openapi: spec.openapi, + info: {title: spec.info.title}, + paths, + 'x-webhooks': hooks, + components: Object.fromEntries(Object.keys(components).sort().map((kind) => [kind, sortKeys(components[kind])])), + }; +} + +// GitHub edits prose and examples often; dropping them keeps --check focused on shape changes. +function stripDocs(node, isPropertyMap = false) { + if (Array.isArray(node)) { + return node.map((item) => stripDocs(item)); + } + if (!node || typeof node !== 'object') { + return node; + } + const out = {}; + for (const [key, value] of Object.entries(node)) { + if (!isPropertyMap && (['description', 'example', 'examples', 'summary', 'externalDocs', 'title'].includes(key) || key.startsWith('x-'))) { + continue; + } + out[key] = stripDocs(value, !isPropertyMap && key === 'properties'); + } + return out; +} + +function collectRefs(node, found = []) { + if (Array.isArray(node)) { + node.forEach((item) => collectRefs(item, found)); + } else if (node && typeof node === 'object') { + for (const [key, value] of Object.entries(node)) { + if (key === '$ref' && typeof value === 'string') { + found.push(value); + } else { + collectRefs(value, found); + } + } + } + return found; +} + +function sortKeys(object) { + return Object.fromEntries(Object.entries(object).sort(([a], [b]) => a.localeCompare(b))); +} + +// Keeps the listed object/interface fields, every enum, scalar and input type they reference, and unions +// narrowed to listed members. Querying anything else then fails validation, like an uncovered REST route. +function trimGraphQL(sdl, fields) { + const definitions = new Map(parse(sdl).definitions.filter((d) => 'name' in d && d.name).map((d) => [d.name.value, d])); + const builtins = new Set(specifiedScalarTypes.map((t) => t.name)); + const kept = new Map(); + const queue = ['Query']; + + while (queue.length) { + const name = queue.pop(); + if (kept.has(name) || builtins.has(name)) { + continue; + } + const def = definitions.get(name); + if (!def) { + throw new Error(`graphql: ${name} is not in the upstream schema`); + } + + let trimmed = stripDirectives({...def, description: undefined}); + if (def.kind === Kind.OBJECT_TYPE_DEFINITION || def.kind === Kind.INTERFACE_TYPE_DEFINITION) { + const wanted = fields[name]; + if (!wanted) { + throw new Error(`graphql: ${name} is reachable from a listed field; add it to operations.json`); + } + const fieldDefs = def.fields.filter((f) => wanted.includes(f.name.value)); + const missing = wanted.filter((f) => !fieldDefs.some((d) => d.name.value === f)); + if (missing.length) { + throw new Error(`graphql: ${name} has no field(s) ${missing.join(', ')}`); + } + trimmed = { + ...trimmed, + interfaces: (def.interfaces ?? []).filter((i) => fields[i.name.value]), + fields: fieldDefs.map((f) => stripDirectives({...f, description: undefined, arguments: f.arguments?.map((a) => stripDirectives({...a, description: undefined}))})), + }; + for (const f of trimmed.fields) { + queue.push(namedType(f.type), ...(f.arguments ?? []).map((a) => namedType(a.type))); + } + queue.push(...trimmed.interfaces.map((i) => i.name.value)); + } else if (def.kind === Kind.UNION_TYPE_DEFINITION) { + trimmed = {...trimmed, types: def.types.filter((t) => fields[t.name.value])}; + queue.push(...trimmed.types.map((t) => t.name.value)); + } else if (def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION) { + trimmed = {...trimmed, fields: def.fields.map((f) => stripDirectives({...f, description: undefined}))}; + queue.push(...trimmed.fields.map((f) => namedType(f.type))); + } else if (def.kind === Kind.ENUM_TYPE_DEFINITION) { + trimmed = {...trimmed, values: def.values.map((v) => stripDirectives({...v, description: undefined}))}; + } + kept.set(name, trimmed); + } + + const schema = buildASTSchema({kind: Kind.DOCUMENT, definitions: [...kept.values()]}); + assertValidSchema(schema); + return `${printSchema(lexicographicSortSchema(schema))}\n`; +} + +function namedType(type) { + return type.kind === Kind.NAMED_TYPE ? type.name.value : namedType(type.type); +} + +function stripDirectives(node) { + return {...node, directives: (node.directives ?? []).filter((d) => d.name.value === 'deprecated')}; +} diff --git a/e2e/integration/fake-github/contract/validator.ts b/e2e/integration/fake-github/contract/validator.ts new file mode 100644 index 000000000..b5f0b3ca1 --- /dev/null +++ b/e2e/integration/fake-github/contract/validator.ts @@ -0,0 +1,220 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import fs from 'node:fs'; +import path from 'node:path'; + +import Ajv, {type ErrorObject, type ValidateFunction} from 'ajv'; +import addFormats from 'ajv-formats'; +import {type DocumentNode, type GraphQLSchema, buildSchema, parse, validate} from 'graphql'; + +type JsonObject = Record; + +const specsDir = path.join(__dirname, 'specs'); +const rest: JsonObject = JSON.parse(fs.readFileSync(path.join(specsDir, 'rest.json'), 'utf8')); + +export const graphQLSchema: GraphQLSchema = buildSchema(fs.readFileSync(path.join(specsDir, 'schema.graphql'), 'utf8')); + +// GitHub answers any authenticated route with these, documented or not. +const basicError = { + type: 'object', + required: ['message'], + properties: {message: {type: 'string'}, documentation_url: {type: 'string'}, status: {type: 'string'}}, +}; +const apiWideResponses: Record = {401: basicError, 403: basicError, 404: basicError}; + +const ajv = new Ajv({strict: false, allErrors: true, validateFormats: true}); +addFormats(ajv); +for (const format of ['uri-template', 'repo.nwo', 'ssh-key', 'ssh-key fingerprint']) { + ajv.addFormat(format, true); +} +ajv.addSchema({components: fromOpenAPI(rest.components)}, 'rest'); + +type Operation = {method: string; template: string; pattern: RegExp; request?: ValidateFunction; responses: Map}; + +const operations: Operation[] = Object.entries(rest.paths as JsonObject).flatMap(([template, methods]) => Object.entries(methods as JsonObject).map(([method, op]) => ({ + method: method.toUpperCase(), + template, + pattern: templateToRegExp(template), + request: jsonSchema(op.requestBody) && ajv.compile(fromOpenAPI(jsonSchema(op.requestBody))), + responses: new Map(Object.entries(op.responses as JsonObject).flatMap(([status, response]) => { + const schema = jsonSchema(resolve(response)); + return schema ? [[status, ajv.compile(fromOpenAPI(schema))]] : []; + })), +}))); + +const webhooks = new Map(Object.entries(rest['x-webhooks'] as JsonObject).map(([name, hook]) => [name, ajv.compile(fromOpenAPI(jsonSchema(hook.post.requestBody)))])); +const apiWide = new Map(Object.entries(apiWideResponses).map(([status, schema]) => [status, ajv.compile(schema)])); + +export function findOperation(method: string, pathname: string): Operation | undefined { + return operations.find((op) => op.method === method && op.pattern.test(pathname)); +} + +export function validateRequest(method: string, pathname: string, body: unknown): string[] { + const op = findOperation(method, pathname); + if (!op) { + return [`${method} ${pathname} is not in operations.json`]; + } + return op.request ? describe(op.request, body, `${method} ${op.template} request`) : []; +} + +export function validateResponse(method: string, pathname: string, status: number, body: unknown): string[] { + const op = findOperation(method, pathname); + if (!op) { + return [`${method} ${pathname} is not in operations.json`]; + } + const validator = op.responses.get(String(status)) ?? apiWide.get(String(status)); + if (!validator) { + if (op.responses.has(String(status)) || status === 204 || status === 304) { + return []; + } + return [`${method} ${op.template}: GitHub does not document a ${status} response`]; + } + return describe(validator, body, `${method} ${op.template} ${status} response`); +} + +// GitHub names webhook payloads after the X-GitHub-Event header plus the action, e.g. pull-request-opened. +export function webhookSchemaName(event: string, action?: string): string | undefined { + const base = event.replaceAll('_', '-'); + return [action && `${base}-${action.replaceAll('_', '-')}`, base].find((name) => name && webhooks.has(name)) || undefined; +} + +export function validateWebhook(event: string, payload: JsonObject): string[] { + const name = webhookSchemaName(event, payload.action); + if (!name) { + return [`webhook ${event}${payload.action ? `.${payload.action}` : ''} is not in operations.json`]; + } + return describe(webhooks.get(name)!, payload, `webhook ${name}`); +} + +export function validateGraphQLQuery(query: string): {document?: DocumentNode; errors: string[]} { + let document: DocumentNode; + try { + document = parse(query); + } catch (err) { + return {errors: [(err as Error).message]}; + } + return {document, errors: validate(graphQLSchema, document).map((e) => e.message).filter((m) => !githubAcceptsShapeConflict.test(m))}; +} + +// graphql-js rejects `... on Issue { state } ... on PullRequest { state }` (IssueState vs PullRequestState); +// GitHub's graphql-ruby server answers it, and the plugin's sidebar query depends on that. +const githubAcceptsShapeConflict = /^Fields "state" conflict because they return conflicting types "(IssueState|PullRequestState)!?" and "(IssueState|PullRequestState)!?"\./; + +export const skeletonTimestamp = '2026-01-01T00:00:00Z'; + +// A minimal value for a component schema with every required field filled in. Callers overlay real +// state on top, so GitHub adding a required field changes the fixture instead of breaking it. +export function skeleton(schemaName: string): any { + const schema = rest.components.schemas[schemaName]; + if (!schema) { + throw new Error(`component schema ${schemaName} is not in the trimmed spec; reference it from an operation in operations.json`); + } + return fill(schema, 0); +} + +export function webhookSkeleton(name: string): any { + return fill(jsonSchema(rest['x-webhooks'][name].post.requestBody)!, 0); +} + +function fill(node: JsonObject, depth: number): any { + const schema = resolve(node) ?? {}; + if (schema.nullable || depth > 15) { + return null; + } + if (schema.allOf) { + return schema.allOf.reduce((acc: any, part: JsonObject) => merge(acc, fill(part, depth + 1)), {}); + } + const branches = schema.anyOf ?? schema.oneOf; + if (branches) { + return fill(branches.find((b: JsonObject) => resolve(b)?.type !== 'null') ?? branches[0], depth + 1); + } + if (schema.enum) { + return schema.enum[0]; + } + if (schema.default !== undefined && schema.default !== null) { + return schema.default; + } + switch (schema.type ?? (schema.properties ? 'object' : undefined)) { + case 'object': + return Object.fromEntries((schema.required ?? []).map((key: string) => [key, fill(schema.properties?.[key] ?? {}, depth + 1)])); + case 'array': + return Array.from({length: schema.minItems ?? 0}, () => fill(schema.items ?? {}, depth + 1)); + case 'integer': + case 'number': + return schema.minimum ?? 0; + case 'boolean': + return false; + case 'string': + return ({'date-time': skeletonTimestamp, date: skeletonTimestamp.slice(0, 10), uri: 'https://github.com/', email: 'e2e@example.com'} as Record)[schema.format] ?? ''; + default: + return null; + } +} + +export function merge(base: any, overlay: any): any { + if (!isPlainObject(base) || !isPlainObject(overlay)) { + return overlay === undefined ? base : overlay; + } + const out = {...base}; + for (const [key, value] of Object.entries(overlay)) { + out[key] = merge(base[key], value); + } + return out; +} + +function isPlainObject(value: unknown): value is JsonObject { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function describe(validator: ValidateFunction, value: unknown, label: string): string[] { + if (validator(value)) { + return []; + } + return (validator.errors ?? []).slice(0, 10).map((e: ErrorObject) => `${label}: ${e.instancePath || '/'} ${e.message}${e.params ? ` ${JSON.stringify(e.params)}` : ''}`); +} + +function resolve(node: JsonObject): JsonObject { + if (!node?.$ref) { + return node; + } + const [kind, name] = node.$ref.replace('#/components/', '').split('/'); + return resolve(rest.components[kind][name]); +} + +function jsonSchema(body: JsonObject | undefined): JsonObject | undefined { + const content = resolve(body as JsonObject)?.content; + return content?.['application/json']?.schema ?? content?.['application/scim+json']?.schema; +} + +// OpenAPI 3.0 marks optional nulls with `nullable`; ajv only understands JSON Schema's `type: [.., 'null']`. +function fromOpenAPI(node: any): any { + if (Array.isArray(node)) { + return node.map(fromOpenAPI); + } + if (!node || typeof node !== 'object') { + return node; + } + const out: JsonObject = {}; + for (const [key, value] of Object.entries(node)) { + if (key === '$ref') { + out.$ref = (value as string).replace(/^#\/components\//, 'rest#/components/'); + } else if (key !== 'nullable') { + out[key] = key === 'properties' || key === 'patternProperties' ? + Object.fromEntries(Object.entries(value as JsonObject).map(([k, v]) => [k, fromOpenAPI(v)])) : + fromOpenAPI(value); + } + } + if (node.nullable !== true) { + return out; + } + if (typeof out.type === 'string' && !out.allOf && !out.anyOf && !out.oneOf) { + return {...out, type: [out.type, 'null'], ...(out.enum ? {enum: [...out.enum, null]} : {})}; + } + return {anyOf: [out, {type: 'null'}]}; +} + +function templateToRegExp(template: string): RegExp { + const escaped = template.replace(/[.*+?^$()|[\]\\]/g, '\\$&').replace(/\{[^}]+\}/g, '[^/]+'); + return new RegExp(`^${escaped}$`); +} diff --git a/e2e/integration/fake-github/server.ts b/e2e/integration/fake-github/server.ts new file mode 100644 index 000000000..b5227ae07 --- /dev/null +++ b/e2e/integration/fake-github/server.ts @@ -0,0 +1,821 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import crypto from 'node:crypto'; +import {type IncomingMessage, type Server, type ServerResponse, createServer} from 'node:http'; + +import {GraphQLError, execute} from 'graphql'; + +import {graphQLSchema, validateGraphQLQuery, validateRequest, validateResponse, validateWebhook} from './contract/validator'; +import {type AuthCode, type Hook, type Item, type Org, type Repo, State, type Token, WEB, fullName, hasScope, normalizeScopes, setAvatarBase, sha} from './state'; +import {type Delivery, type WebhookRequest, buildPayload, deliver, pingPayload} from './webhooks'; + +export type OAuthApp = {clientId: string; clientSecret: string; callbackURL: string; name: string; codeTTLms?: number}; +export type JournalEntry = {method: string; path: string; query: Record; status: number; body?: unknown}; +type Fault = {method: string; path: string; pattern: RegExp; status: number; body?: unknown; headers?: Record; times: number}; + +type Ctx = { + req: IncomingMessage; res: ServerResponse; url: URL; params: string[]; body: any; + token: Token | null; send: (status: number, body?: unknown, headers?: Record) => void; +}; +type Handler = (ctx: Ctx) => void | Promise; + +const docs = 'https://docs.github.com/rest'; +const ssoMessage = 'Resource protected by organization SAML enforcement. You must grant your OAuth token access to this organization.'; + +export type FakeGitHub = {state: State; close: () => Promise}; + +export async function startFakeGitHub({port, publicURL, app}: {port: number; publicURL: string; app: OAuthApp}): Promise { + let state = new State(); + let oauthApp = app; + let journal: JournalEntry[] = []; + let violations: string[] = []; + let faults: Fault[] = []; + let pending: Promise[] = []; + let rateLimit = new Map(); + setAvatarBase(`${publicURL}/avatars`); + + const fail = (ctx: Ctx, status: number, message: string, headers?: Record) => ctx.send(status, {message, documentation_url: docs, status: String(status)}, headers); + const notFound = (ctx: Ctx) => fail(ctx, 404, 'Not Found'); + + const requireToken = (ctx: Ctx): Token | null => { + if (!ctx.token) { + fail(ctx, 401, ctx.req.headers.authorization ? 'Bad credentials' : 'Requires authentication'); + return null; + } + return ctx.token; + }; + + const ssoBlocked = (ctx: Ctx, owner: string): boolean => { + const org = state.orgs.get(owner.toLowerCase()); + if (!org?.ssoEnforced || !ctx.token || !org.members.has(ctx.token.login)) { + return false; + } + fail(ctx, 403, ssoMessage, {'X-GitHub-SSO': `required; url=${WEB}/orgs/${org.login}/sso?authorization_request=${crypto.randomBytes(12).toString('hex')}`}); + return true; + }; + + const visibleRepo = (ctx: Ctx, owner: string, name: string): Repo | null => { + const repo = state.repo(owner, name); + if (ssoBlocked(ctx, owner)) { + return null; + } + if (!repo || !state.canSee(ctx.token, repo)) { + notFound(ctx); + return null; + } + return repo; + }; + + const paginate = (ctx: Ctx, all: unknown[], {defaultSize = 30, maxSize = 100} = {}) => { + const perPage = Math.min(Number(ctx.url.searchParams.get('per_page') ?? defaultSize) || defaultSize, maxSize); + const page = Math.max(Number(ctx.url.searchParams.get('page') ?? 1) || 1, 1); + const last = Math.max(Math.ceil(all.length / perPage), 1); + const link = (p: number, rel: string) => { + const next = new URL(`${publicURL}${ctx.url.pathname}${ctx.url.search}`); + next.searchParams.set('page', String(p)); + return `<${next}>; rel="${rel}"`; + }; + const links = [page < last && link(page + 1, 'next'), page < last && link(last, 'last'), page > 1 && link(1, 'first'), page > 1 && link(page - 1, 'prev')].filter(Boolean); + const headers: Record = links.length ? {Link: links.join(', ')} : {}; + return {items: all.slice((page - 1) * perPage, page * perPage), headers}; + }; + + const sendPage = (ctx: Ctx, all: unknown[], sizes?: {defaultSize?: number; maxSize?: number}) => { + const {items, headers} = paginate(ctx, all, sizes); + ctx.send(200, items, headers); + }; + + const track = (delivery: Promise) => pending.push(delivery.catch((err) => violations.push(`fake-github failed to deliver a webhook: ${err}`))); + + const hooksFor = (repo: Repo): Hook[] => state.hooks.filter((h) => h.owner.toLowerCase() === repo.owner.toLowerCase() && (!h.repo || h.repo.toLowerCase() === repo.name.toLowerCase())); + + const emit = (repo: Repo, request: WebhookRequest) => { + const hooks = hooksFor(repo); + if (!hooks.length) { + return; + } + const payload = buildPayload(state, request); + violations.push(...validateWebhook(request.event, payload)); + track(deliver(hooks, request.event, payload)); + }; + + const routes: [string, RegExp, Handler][] = []; + const route = (method: string, pattern: string, handler: Handler) => { + routes.push([method, new RegExp(`^${pattern.replace(/:\w+/g, '([^/]+)')}$`), handler]); + }; + + route('GET', '/login/oauth/authorize', (ctx) => { + const q = ctx.url.searchParams; + if (q.get('client_id') !== oauthApp.clientId) { + return ctx.send(404, htmlPage('404', '

Not Found

')); + } + const state_ = q.get('state') ?? ''; + const redirectURI = q.get('redirect_uri') || oauthApp.callbackURL; + if (!redirectMatches(redirectURI, oauthApp.callbackURL)) { + return redirect(ctx, oauthApp.callbackURL, {error: 'redirect_uri_mismatch', error_description: 'The redirect_uri MUST match the registered callback URL for this application.', error_uri: 'https://docs.github.com/apps/managing-oauth-apps/troubleshooting-authorization-request-errors/#redirect-uri-mismatch', state: state_}); + } + if (!state.signedIn) { + return ctx.send(200, htmlPage('Sign in to GitHub', `
`)); + } + const scopes = parseScopeParam(q.get('scope')); + const previous = state.grants.get(`${oauthApp.clientId}:${state.signedIn.toLowerCase()}`); + if (previous && scopes.every((s) => hasScope(previous, s))) { + return issueCode(ctx, redirectURI, scopes, state_); + } + return ctx.send(200, htmlPage(`Authorize ${oauthApp.name}`, `

Authorize ${escape(oauthApp.name)}

Signed in as ${escape(state.signedIn)}

    ${scopes.map((s) => `
  • ${escape(s)}
  • `).join('')}
${hidden({client_id: oauthApp.clientId, redirect_uri: redirectURI, scope: scopes.join(' '), state: state_})}
`)); + }); + + route('POST', '/login/oauth/authorize', (ctx) => { + const {client_id: clientId, redirect_uri: redirectURI, scope, state: state_, authorize} = ctx.body; + if (clientId !== oauthApp.clientId || !state.signedIn || !redirectURI || !redirectMatches(redirectURI, oauthApp.callbackURL)) { + return ctx.send(422, htmlPage('Unprocessable', '

Unprocessable request

')); + } + if (authorize !== '1') { + return redirect(ctx, redirectURI, {error: 'access_denied', error_description: 'The user has denied your application access.', error_uri: 'https://docs.github.com/apps/managing-oauth-apps/troubleshooting-authorization-request-errors/#access-denied', state: state_}); + } + return issueCode(ctx, redirectURI, parseScopeParam(scope), state_); + }); + + route('POST', '/session', (ctx) => { + state.signedIn = state.login(String(ctx.body.login)).login; + ctx.send(302, '', {Location: String(ctx.body.return_to || '/')}); + }); + + // GitHub answers token-exchange failures with 200 and an error body. + route('POST', '/login/oauth/access_token', (ctx) => { + const basic = /^Basic (.+)$/.exec(ctx.req.headers.authorization ?? '')?.[1]; + const [headerId, headerSecret] = basic ? Buffer.from(basic, 'base64').toString().split(':').map(decodeURIComponent) : []; + const clientId = ctx.body.client_id ?? headerId; + const clientSecret = ctx.body.client_secret ?? headerSecret; + const json = (ctx.req.headers.accept ?? '').includes('application/json'); + const reply = (fields: Record) => (json ? ctx.send(200, fields) : ctx.send(200, new URLSearchParams(fields).toString(), {'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'})); + const oauthError = (error: string, description: string) => reply({error, error_description: description, error_uri: `https://docs.github.com/apps/managing-oauth-apps/troubleshooting-oauth-app-access-token-request-errors/#${error.replaceAll('_', '-')}`}); + + if (clientId !== oauthApp.clientId || clientSecret !== oauthApp.clientSecret) { + return oauthError('incorrect_client_credentials', 'The client_id and/or client_secret passed are incorrect.'); + } + const code = state.codes.get(String(ctx.body.code)); + if (!code || code.used || code.expiresAt <= Date.now() || code.clientId !== clientId) { + return oauthError('bad_verification_code', 'The code passed is incorrect or expired.'); + } + if (ctx.body.redirect_uri && ctx.body.redirect_uri !== code.redirectUri) { + return oauthError('redirect_uri_mismatch', 'The redirect_uri MUST match the registered callback URL for this application.'); + } + code.used = true; + const token = state.issueToken(code.login, clientId, state.grant(code.login, clientId, code.scopes)); + return reply({access_token: token.value, token_type: 'bearer', scope: token.scopes.join(',')}); + }); + + route('GET', '/api/user', (ctx) => { + const token = requireToken(ctx); + if (token) { + ctx.send(200, state.userJSON(state.login(token.login), 'private-user')); + } + }); + + route('GET', '/api/users/:username', (ctx) => { + const account = state.account(ctx.params[0]); + return account ? ctx.send(200, state.userJSON(account, 'public-user')) : notFound(ctx); + }); + + route('GET', '/api/user/orgs', (ctx) => { + const token = requireToken(ctx); + if (!token) { + return; + } + if (!hasScope(token.scopes, 'read:org', 'user')) { + return fail(ctx, 403, 'You need at least read:org scope or user scope to list your organizations.'); + } + sendPage(ctx, [...state.orgs.values()].filter((o) => o.members.has(token.login)).map((o) => state.orgJSON(o))); + }); + + route('GET', '/api/user/repos', (ctx) => { + const token = requireToken(ctx); + if (!token) { + return; + } + const affiliation = (ctx.url.searchParams.get('affiliation') ?? 'owner,collaborator,organization_member').split(','); + const repos = [...state.repos.values()].filter((r) => state.canSee(token, r) && ( + (affiliation.includes('owner') && r.owner === token.login) || + (affiliation.includes('collaborator') && r.writers.has(token.login) && r.owner !== token.login) || + (affiliation.includes('organization_member') && Boolean(state.orgs.get(r.owner.toLowerCase())?.members.has(token.login))))); + sendPage(ctx, sortRepos(repos).map((r) => state.repoJSON(r, token))); + }); + + route('GET', '/api/users/:username/repos', (ctx) => { + const account = state.account(ctx.params[0]); + if (!account) { + return notFound(ctx); + } + sendPage(ctx, sortRepos([...state.repos.values()].filter((r) => r.owner === account.login && !r.private)).map((r) => state.repoJSON(r, ctx.token, 'minimal-repository'))); + }); + + route('GET', '/api/orgs/:org', (ctx) => { + const org = state.orgs.get(ctx.params[0].toLowerCase()); + if (!org) { + return notFound(ctx); + } + if (!ssoBlocked(ctx, org.login)) { + ctx.send(200, state.orgJSON(org, 'organization-full')); + } + }); + + route('GET', '/api/orgs/:org/repos', (ctx) => { + const org = state.orgs.get(ctx.params[0].toLowerCase()); + if (!org) { + return notFound(ctx); + } + if (!ssoBlocked(ctx, org.login)) { + sendPage(ctx, sortRepos([...state.repos.values()].filter((r) => r.owner === org.login && state.canSee(ctx.token, r))).map((r) => state.repoJSON(r, ctx.token, 'minimal-repository'))); + } + }); + + // GitHub redirects non-members to the public membership check; members get a direct answer. + route('GET', '/api/orgs/:org/members/:username', (ctx) => { + const org = state.orgs.get(ctx.params[0].toLowerCase()); + if (!org) { + return notFound(ctx); + } + if (!ctx.token || !org.members.has(ctx.token.login)) { + return ctx.send(302, '', {Location: `${publicURL}/api/orgs/${org.login}/public_members/${ctx.params[1]}`}); + } + return org.members.has(state.account(ctx.params[1])?.login ?? '') ? ctx.send(204) : ctx.send(404, {message: 'User does not exist or is not a member of the organization', documentation_url: docs, status: '404'}); + }); + + route('GET', '/api/orgs/:org/public_members/:username', (ctx) => notFound(ctx)); + + route('GET', '/api/orgs/:org/hooks', (ctx) => { + const org = orgHookAccess(ctx); + if (org) { + sendPage(ctx, state.hooks.filter((h) => !h.repo && h.owner === org.login).map((h) => state.hookJSON(h, 'org-hook'))); + } + }); + + route('POST', '/api/orgs/:org/hooks', (ctx) => { + const org = orgHookAccess(ctx); + if (org) { + createHook(ctx, org.login, undefined); + } + }); + + route('GET', '/api/repos/:owner/:repo', (ctx) => { + const repo = visibleRepo(ctx, ctx.params[0], ctx.params[1]); + if (repo) { + ctx.send(200, state.repoJSON(repo, ctx.token, 'full-repository')); + } + }); + + route('GET', '/api/repos/:owner/:repo/hooks', (ctx) => { + const repo = repoHookAccess(ctx, 'read:repo_hook'); + if (repo) { + sendPage(ctx, state.hooks.filter((h) => h.repo === repo.name && h.owner === repo.owner).map((h) => state.hookJSON(h, 'hook'))); + } + }); + + route('POST', '/api/repos/:owner/:repo/hooks', (ctx) => { + const repo = repoHookAccess(ctx, 'write:repo_hook'); + if (repo) { + createHook(ctx, repo.owner, repo.name); + } + }); + + route('GET', '/api/repos/:owner/:repo/issues/:number', (ctx) => { + const repo = visibleRepo(ctx, ctx.params[0], ctx.params[1]); + const item = repo && state.item(repo.owner, repo.name, Number(ctx.params[2])); + if (repo) { + return item ? ctx.send(200, state.issueJSON(item)) : notFound(ctx); + } + }); + + route('POST', '/api/repos/:owner/:repo/issues', (ctx) => { + const token = requireToken(ctx); + const repo = token && visibleRepo(ctx, ctx.params[0], ctx.params[1]); + if (!token || !repo) { + return; + } + if (!hasScope(token.scopes, repo.private ? 'repo' : 'public_repo')) { + return notFound(ctx); + } + if (!ctx.body?.title) { + return ctx.send(422, {message: 'Validation Failed', errors: [{resource: 'Issue', code: 'missing_field', field: 'title'}], documentation_url: docs, status: '422'}); + } + const canPush = repo.writers.has(token.login); + const item = state.addItem(repo, { + kind: 'issue', title: ctx.body.title, body: ctx.body.body ?? '', author: token.login, + ...(canPush ? { + labels: (ctx.body.labels ?? []).map((l: string | {name: string}) => (typeof l === 'string' ? l : l.name)), + assignees: [...(ctx.body.assignees ?? []), ...(ctx.body.assignee ? [ctx.body.assignee] : [])].filter((a: string) => repo.writers.has(a)), + milestone: ctx.body.milestone ?? null, + } : {}), + }); + ctx.send(201, state.issueJSON(item)); + emit(repo, {event: 'issues', action: 'opened', repo: fullName(repo), number: item.number, sender: token.login}); + }); + + route('POST', '/api/repos/:owner/:repo/issues/:number/comments', (ctx) => { + const token = requireToken(ctx); + const repo = token && visibleRepo(ctx, ctx.params[0], ctx.params[1]); + const item = repo && state.item(repo.owner, repo.name, Number(ctx.params[2])); + if (!token || !repo) { + return; + } + if (!item || !hasScope(token.scopes, repo.private ? 'repo' : 'public_repo')) { + return notFound(ctx); + } + const comment = {id: state.id(), author: token.login, body: String(ctx.body?.body ?? ''), createdAt: state.now()}; + if (!comment.body) { + return ctx.send(422, {message: 'Validation Failed', errors: [{resource: 'IssueComment', code: 'missing_field', field: 'body'}], documentation_url: docs, status: '422'}); + } + item.comments.push(comment); + item.updatedAt = comment.createdAt; + ctx.send(201, state.commentJSON(item, comment)); + emit(repo, {event: 'issue_comment', action: 'created', repo: fullName(repo), number: item.number, sender: token.login, commentId: comment.id}); + }); + + route('GET', '/api/repos/:owner/:repo/labels', (ctx) => { + const repo = visibleRepo(ctx, ctx.params[0], ctx.params[1]); + if (repo) { + sendPage(ctx, repo.labels.map((l) => state.labelJSON(repo, l))); + } + }); + + route('GET', '/api/repos/:owner/:repo/assignees', (ctx) => { + const repo = visibleRepo(ctx, ctx.params[0], ctx.params[1]); + if (repo) { + sendPage(ctx, [...repo.writers].sort().map((login) => state.userJSON(state.login(login)))); + } + }); + + route('GET', '/api/repos/:owner/:repo/milestones', (ctx) => { + const repo = visibleRepo(ctx, ctx.params[0], ctx.params[1]); + if (repo) { + const wanted = ctx.url.searchParams.get('state') ?? 'open'; + sendPage(ctx, repo.milestones.filter((m) => wanted === 'all' || m.state === wanted).map((m) => state.milestoneJSON(repo, m))); + } + }); + + route('GET', '/api/repos/:owner/:repo/pulls/:number', (ctx) => { + const repo = visibleRepo(ctx, ctx.params[0], ctx.params[1]); + const item = repo && state.item(repo.owner, repo.name, Number(ctx.params[2])); + if (repo) { + return item?.kind === 'pull' ? ctx.send(200, state.pullJSON(item, ctx.token)) : notFound(ctx); + } + }); + + route('GET', '/api/repos/:owner/:repo/pulls/:number/reviews', (ctx) => { + const repo = visibleRepo(ctx, ctx.params[0], ctx.params[1]); + const item = repo && state.item(repo.owner, repo.name, Number(ctx.params[2])); + if (repo) { + return item?.kind === 'pull' ? sendPage(ctx, item.reviews.map((r) => state.reviewJSON(item, r))) : notFound(ctx); + } + }); + + route('GET', '/api/repos/:owner/:repo/commits/:ref/status', (ctx) => { + const repo = visibleRepo(ctx, ctx.params[0], ctx.params[1]); + const item = repo && state.items.find((i) => i.repo === fullName(repo) && [sha(i, 'head'), i.head].includes(ctx.params[2])); + if (repo) { + return item ? ctx.send(200, state.combinedStatusJSON(item)) : notFound(ctx); + } + }); + + route('GET', '/api/search/issues', (ctx) => { + const q = ctx.url.searchParams.get('q'); + if (!q) { + return ctx.send(422, {message: 'Validation Failed', errors: [{resource: 'Search', field: 'q', code: 'missing'}], documentation_url: docs, status: '422'}); + } + const {visible, ssoHeader} = searchVisible(ctx, q); + const {items, headers} = paginate(ctx, visible.map((i) => state.issueJSON(i, 'issue-search-result-item'))); + ctx.send(200, {total_count: visible.length, incomplete_results: false, search_type: 'lexical', items}, {...headers, ...ssoHeader}); + }); + + route('GET', '/api/notifications', (ctx) => { + const token = requireToken(ctx); + if (!token) { + return; + } + if (!hasScope(token.scopes, 'notifications', 'repo')) { + return fail(ctx, 403, "Missing the 'notifications' scope."); + } + const all = ctx.url.searchParams.get('all') === 'true'; + const threads = state.threads.filter((t) => t.login === token.login && (all || t.unread) && state.canSee(token, state.repos.get(t.repo.toLowerCase()))); + sendPage(ctx, threads.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).map((t) => state.threadJSON(t, token)), {defaultSize: 50, maxSize: 50}); + }); + + route('POST', '/api/graphql', async (ctx) => { + if (!ctx.token) { + return ctx.send(401, {message: 'This endpoint requires you to be authenticated.', documentation_url: 'https://docs.github.com/graphql/guides/forming-calls-with-graphql#authenticating-with-graphql', status: '401'}); + } + const {document, errors} = validateGraphQLQuery(String(ctx.body?.query ?? '')); + if (!document || errors.length) { + violations.push(...errors.map((e) => `graphql query: ${e}`)); + return ctx.send(200, {errors: errors.map((message) => ({message}))}); + } + const result = await execute({schema: graphQLSchema, document, variableValues: ctx.body.variables, rootValue: graphQLRoot(ctx)}); + for (const error of result.errors ?? []) { + if (!(error.originalError instanceof GitHubGraphQLError)) { + violations.push(`graphql answer: ${error.message} at ${error.path?.join('.')}`); + } + } + ctx.send(200, result); + }); + + route('GET', '/avatars/u/:id', (ctx) => { + const hue = (Number(ctx.params[0]) * 47) % 360; + ctx.send(200, ``, {'Content-Type': 'image/svg+xml', 'Cache-Control': 'max-age=3600'}); + }); + + route('GET', '/', (ctx) => ctx.send(200, 'ok', {'Content-Type': 'text/plain'})); + + route('POST', '/__control/reset', async (ctx) => { + await Promise.allSettled(pending); + state = new State(); + journal = []; + violations = []; + faults = []; + pending = []; + rateLimit = new Map(); + oauthApp = {...app, ...ctx.body}; + ctx.send(200, {}); + }); + route('POST', '/__control/oauth-app', (ctx) => { + oauthApp = {...oauthApp, ...ctx.body}; + ctx.send(200, {}); + }); + route('GET', '/__control/requests', (ctx) => ctx.send(200, journal)); + route('GET', '/__control/contract-violations', (ctx) => ctx.send(200, violations)); + route('POST', '/__control/users', (ctx) => ctx.send(200, state.addUser(ctx.body.login, {name: ctx.body.name ?? null, email: ctx.body.email ?? null}))); + route('POST', '/__control/orgs', (ctx) => { + const org = state.addOrg(ctx.body.login, ctx.body.members ?? [], ctx.body.admins ?? []); + org.ssoEnforced = Boolean(ctx.body.ssoEnforced); + ctx.send(200, {...org, members: [...org.members], admins: [...org.admins]}); + }); + route('POST', '/__control/sso', (ctx) => { + state.orgs.get(String(ctx.body.org).toLowerCase())!.ssoEnforced = Boolean(ctx.body.enforced); + ctx.send(200, {}); + }); + route('POST', '/__control/teams', (ctx) => { + const team = {id: state.id(), org: ctx.body.org, slug: ctx.body.slug, name: ctx.body.name ?? ctx.body.slug, members: new Set(ctx.body.members ?? [])}; + state.teams.push(team); + ctx.send(200, {...team, members: [...team.members]}); + }); + route('POST', '/__control/repos', (ctx) => { + const repo = state.addRepo(ctx.body.fullName, ctx.body); + ctx.send(200, {fullName: fullName(repo), id: repo.id}); + }); + route('POST', '/__control/items', (ctx) => { + const [owner, name] = String(ctx.body.repo).split('/'); + const {repo: _, ...fields} = ctx.body; + ctx.send(200, state.addItem(state.repo(owner, name)!, fields)); + }); + route('PATCH', '/__control/items', (ctx) => { + const [owner, name] = String(ctx.body.repo).split('/'); + const item = state.item(owner, name, ctx.body.number); + if (!item) { + return ctx.send(404, {}); + } + const {repo: _, number: __, ...patch} = ctx.body; + Object.assign(item, patch, {updatedAt: state.now()}); + ctx.send(200, item); + }); + route('GET', '/__control/items', (ctx) => ctx.send(200, state.items)); + route('POST', '/__control/threads', (ctx) => { + const item = ctx.body.number ? state.item(...(String(ctx.body.repo).split('/') as [string, string]), ctx.body.number) : undefined; + const thread = { + id: String(state.id()), login: ctx.body.login, repo: ctx.body.repo, number: ctx.body.number ?? null, reason: ctx.body.reason ?? 'mention', + title: ctx.body.title ?? item?.title ?? 'Notification', type: ctx.body.type ?? (item?.kind === 'pull' ? 'PullRequest' : 'Issue'), unread: true, updatedAt: state.now(), + }; + state.threads.push(thread); + ctx.send(200, thread); + }); + route('POST', '/__control/hooks', (ctx) => { + const hook: Hook = {id: state.id(), owner: ctx.body.owner, repo: ctx.body.repo, url: ctx.body.url, secret: ctx.body.secret ?? '', events: ctx.body.events ?? ['*'], active: true, contentType: ctx.body.contentType ?? 'json', createdAt: state.now()}; + state.hooks.push(hook); + ctx.send(200, hook); + }); + route('GET', '/__control/hooks', (ctx) => ctx.send(200, state.hooks)); + route('POST', '/__control/tokens', (ctx) => { + const {login} = state.login(ctx.body.login); + const scopes = state.grant(login, oauthApp.clientId, ctx.body.scopes ?? defaultScopes); + ctx.send(200, state.issueToken(login, oauthApp.clientId, scopes)); + }); + route('POST', '/__control/grants', (ctx) => ctx.send(200, {scopes: state.grant(ctx.body.login, oauthApp.clientId, ctx.body.scopes)})); + route('POST', '/__control/revoke', (ctx) => { + const token = state.tokens.get(ctx.body.token); + if (token) { + token.revoked = true; + } + ctx.send(200, {}); + }); + route('POST', '/__control/sign-in', (ctx) => { + state.signedIn = ctx.body.login ? state.login(ctx.body.login).login : null; + ctx.send(200, {}); + }); + route('POST', '/__control/fault', (ctx) => { + let pattern: RegExp; + try { + pattern = new RegExp(`^${ctx.body.path}$`); + } catch (err) { + return ctx.send(400, {message: String(err)}); + } + faults.push({times: Infinity, ...ctx.body, pattern}); + return ctx.send(200, {}); + }); + route('POST', '/__control/webhooks', async (ctx) => { + const request = ctx.body as WebhookRequest & {deliver?: boolean}; + const payload = buildPayload(state, request); + violations.push(...validateWebhook(request.event, payload)); + let deliveries: Delivery[] = []; + if (request.deliver !== false) { + const [owner, name] = request.repo.split('/'); + deliveries = await deliver(hooksFor(state.repo(owner, name)!), request.event, payload); + } + ctx.send(200, {payload, deliveries}); + }); + route('POST', '/__control/settle', async (ctx) => { + await Promise.allSettled(pending); + ctx.send(200, {}); + }); + + function orgHookAccess(ctx: Ctx): Org | null { + const token = requireToken(ctx); + const org = state.orgs.get(ctx.params[0].toLowerCase()); + if (!token || ssoBlocked(ctx, ctx.params[0])) { + return null; + } + if (!org || !org.admins.has(token.login) || !hasScope(token.scopes, 'admin:org_hook')) { + notFound(ctx); + return null; + } + return org; + } + + function repoHookAccess(ctx: Ctx, scope: string): Repo | null { + const token = requireToken(ctx); + const repo = token && visibleRepo(ctx, ctx.params[0], ctx.params[1]); + if (!token || !repo) { + return null; + } + if (!repo.admins.has(token.login) || !hasScope(token.scopes, scope, 'repo')) { + notFound(ctx); + return null; + } + return repo; + } + + function createHook(ctx: Ctx, owner: string, repo: string | undefined) { + const config = ctx.body?.config ?? {}; + if (!config.url) { + return ctx.send(422, {message: 'Validation Failed', errors: [{resource: 'Hook', code: 'custom', message: 'Config must contain URL for webhooks'}], documentation_url: docs, status: '422'}); + } + const existing = state.hooks.find((h) => h.owner === owner && h.repo === repo && h.url === config.url); + if (existing) { + return ctx.send(422, {message: 'Validation Failed', errors: [{resource: 'Hook', code: 'custom', message: `Hook already exists on this ${repo ? 'repository' : 'organization'}`}], documentation_url: docs, status: '422'}); + } + const hook: Hook = {id: state.id(), owner, repo, url: config.url, secret: config.secret ?? '', events: ctx.body.events ?? ['push'], active: ctx.body.active ?? true, contentType: config.content_type ?? 'form', createdAt: state.now()}; + state.hooks.push(hook); + ctx.send(201, state.hookJSON(hook, repo ? 'hook' : 'org-hook')); + const payload = pingPayload(state, hook, ctx.token!.login); + violations.push(...validateWebhook('ping', payload)); + track(deliver([hook], 'ping', payload)); + } + + function issueCode(ctx: Ctx, redirectURI: string, scopes: string[], stateParam: string) { + const code: AuthCode = {code: crypto.randomBytes(10).toString('hex'), login: state.signedIn!, clientId: oauthApp.clientId, redirectUri: redirectURI, scopes, expiresAt: Date.now() + (oauthApp.codeTTLms ?? 10 * 60_000), used: false}; + state.codes.set(code.code, code); + redirect(ctx, redirectURI, {code: code.code, state: stateParam}); + } + + // Search drops results from SAML-protected orgs the token isn't authorized for, and says so in a header. + function searchVisible(ctx: Ctx, query: string): {visible: Item[]; ssoHeader: Record} { + const hits = state.search(query, ctx.token); + const blocked = new Set(); + const visible = hits.filter((item) => { + const org = state.orgs.get(state.repoOf(item).owner.toLowerCase()); + if (org?.ssoEnforced && ctx.token && org.members.has(ctx.token.login)) { + blocked.add(org); + return false; + } + return true; + }); + return {visible, ssoHeader: blocked.size ? {'X-GitHub-SSO': `partial-results; organizations=${[...blocked].map((o) => o.id).join(',')}`} : {}}; + } + + function graphQLRoot(ctx: Ctx) { + return { + search: ({query, type, first, last, after}: {query: string; type: string; first?: number; last?: number; after?: string}) => { + if (first === undefined && last === undefined) { + throw new GitHubGraphQLError('You must provide a `first` or `last` value to properly paginate the `search` connection.'); + } + if (last !== undefined) { + throw new Error('fake-github does not implement `last` on search; add it if the plugin starts using it'); + } + const limit = first!; + if (limit > 100) { + throw new GitHubGraphQLError(`Requesting ${limit} records on the \`search\` connection exceeds the \`${first === undefined ? 'last' : 'first'}\` limit of 100 records.`); + } + const hits = type === 'ISSUE' ? searchVisible(ctx, query).visible : []; + const start = after ? Number(Buffer.from(after, 'base64').toString().replace('cursor:', '')) : 0; + const nodes = hits.slice(start, start + limit); + const end = start + nodes.length; + return { + issueCount: hits.length, + nodes: nodes.map(graphQLItem), + pageInfo: {endCursor: nodes.length ? Buffer.from(`cursor:${end}`).toString('base64') : null, hasNextPage: end < hits.length}, + }; + }, + }; + } + + function graphQLItem(item: Item) { + const repo = state.repoOf(item); + const actor = (login: string) => ({__typename: state.account(login)?.type === 'Organization' ? 'Organization' : 'User', login}); + const issue = state.issueJSON(item); + const milestone = repo.milestones.find((m) => m.number === item.milestone); + return { + __typename: item.kind === 'pull' ? 'PullRequest' : 'Issue', + number: item.number, title: item.title, body: item.body, url: issue.html_url, + state: item.merged ? 'MERGED' : item.state.toUpperCase(), createdAt: item.createdAt, updatedAt: item.updatedAt, + authorAssociation: state.association(repo, item.author), author: actor(item.author), + repository: {name: repo.name, nameWithOwner: fullName(repo), url: `${WEB}/${fullName(repo)}`, owner: actor(repo.owner)}, + milestone: milestone ? {title: milestone.title} : null, + labels: ({first}: {first?: number}) => ({nodes: item.labels.slice(0, first ?? 100).map((name) => ({name, color: repo.labels.find((l) => l.name === name)?.color ?? 'ededed'}))}), + additions: item.additions, deletions: item.deletions, changedFiles: item.changedFiles, + reviewRequests: ({first}: {first?: number}) => ({ + nodes: [ + ...item.reviewers.map((login) => ({requestedReviewer: {__typename: 'User', login}})), + ...item.teamReviewers.map((slug) => ({requestedReviewer: {__typename: 'Team', slug, organization: {__typename: 'Organization', login: repo.owner}}})), + ].slice(0, first ?? 100), + }), + }; + } + + const listener = async (req: IncomingMessage, res: ServerResponse) => { + const url = new URL(req.url ?? '/', publicURL); + const rawBody = await readBody(req); + const isControl = url.pathname.startsWith('/__control/'); + let body: any = {}; + if (rawBody) { + body = (req.headers['content-type'] ?? '').includes('json') ? safeJSON(rawBody) : Object.fromEntries(new URLSearchParams(rawBody)); + } + const bearer = /^(?:Bearer|token) (.+)$/i.exec(req.headers.authorization ?? '')?.[1]; + const known = bearer ? state.tokens.get(bearer) : undefined; + const token = known && !known.revoked ? known : null; + const apiPath = url.pathname.startsWith('/api/') ? url.pathname.slice(4) : null; + + let skipContract = false; + const send: Ctx['send'] = (status, payload, headers = {}) => { + if (res.headersSent) { + return; + } + const isText = typeof payload === 'string'; + const out = payload === undefined ? '' : isText ? payload : JSON.stringify(payload); + if (apiPath && apiPath !== '/graphql' && !isText && !skipContract) { + violations.push(...validateResponse(req.method!, apiPath, status, payload)); + } + res.writeHead(status, { + 'Content-Type': isText ? 'text/html; charset=utf-8' : 'application/json; charset=utf-8', + ...(apiPath ? apiHeaders(token, rateLimit) : {}), + ...headers, + }); + res.end(out); + if (!isControl) { + journal.push({method: req.method!, path: url.pathname, query: Object.fromEntries(url.searchParams), status, body: rawBody ? body : undefined}); + } + }; + const ctx: Ctx = {req, res, url, params: [], body, token, send}; + + try { + if (apiPath && bearer && !token) { + return send(401, {message: 'Bad credentials', documentation_url: docs, status: '401'}); + } + if (body === invalidJSON) { + violations.push(`${req.method} ${url.pathname}: the plugin sent a body that is not JSON`); + skipContract = true; + return send(400, {message: 'Problems parsing JSON', documentation_url: docs, status: '400'}); + } + if (apiPath && apiPath !== '/graphql' && !isControl && req.method !== 'GET' && rawBody) { + violations.push(...validateRequest(req.method!, apiPath, body)); + } + const fault = !isControl && faults.find((f) => f.times > 0 && f.method === req.method && f.pattern.test(url.pathname)); + if (fault) { + fault.times--; + skipContract = true; + return send(fault.status, fault.body ?? {message: 'Injected fault', documentation_url: docs, status: String(fault.status)}, fault.headers); + } + for (const [method, pattern, handler] of routes) { + const match = method === req.method && pattern.exec(url.pathname); + if (match) { + ctx.params = match.slice(1).map(decodeURIComponent); + await handler(ctx); + return; + } + } + if (apiPath) { + violations.push(`${req.method} ${apiPath} is not served by fake-github`); + skipContract = true; + } + send(404, {message: 'Not Found', documentation_url: docs, status: '404'}); + } catch (err) { + violations.push(`fake-github crashed on ${req.method} ${url.pathname}: ${(err as Error).stack}`); + send(500, {message: String(err)}); + } + }; + + const server: Server = createServer((req, res) => { + listener(req, res).catch((err) => { + res.statusCode = 500; + res.end(String(err)); + }); + }); + await new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(port, resolve); + }); + + return { + get state() { + return state; + }, + close: () => new Promise((resolve) => server.close(() => resolve())), + }; +} + +export const defaultScopes = ['public_repo', 'notifications', 'read:org', 'admin:org_hook']; + +class GitHubGraphQLError extends GraphQLError {} + +function apiHeaders(token: Token | null, rateLimit: Map): Record { + const key = token?.value ?? 'anonymous'; + const used = (rateLimit.get(key) ?? 0) + 1; + rateLimit.set(key, used); + const limit = token ? 5000 : 60; + return { + 'X-GitHub-Media-Type': 'github.v3; format=json', + 'X-GitHub-Api-Version-Selected': '2022-11-28', + 'X-GitHub-Request-Id': crypto.randomBytes(8).toString('hex').toUpperCase(), + 'X-RateLimit-Limit': String(limit), + 'X-RateLimit-Remaining': String(Math.max(limit - used, 0)), + 'X-RateLimit-Reset': String(Math.floor(Date.now() / 1000) + 3600), + 'X-RateLimit-Used': String(used), + 'X-RateLimit-Resource': 'core', + ...(token ? {'X-OAuth-Scopes': normalizeScopes(token.scopes).join(', '), 'X-Accepted-OAuth-Scopes': ''} : {}), + }; +} + +function redirectMatches(requested: string, registered: string): boolean { + try { + const a = new URL(requested); + const b = new URL(registered); + return a.protocol === b.protocol && a.host === b.host && (a.pathname === b.pathname || a.pathname.startsWith(`${b.pathname.replace(/\/$/, '')}/`)); + } catch { + return false; + } +} + +function redirect(ctx: Ctx, target: string, params: Record) { + const url = new URL(target); + for (const [k, v] of Object.entries(params)) { + url.searchParams.set(k, v); + } + ctx.send(302, '', {Location: url.toString()}); +} + +function parseScopeParam(scope: string | null | undefined): string[] { + return normalizeScopes((scope ?? '').split(/[\s,]+/)); +} + +function sortRepos(repos: Repo[]): Repo[] { + return [...repos].sort((a, b) => fullName(a).localeCompare(fullName(b))); +} + +function readBody(req: IncomingMessage): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + req.on('data', (c) => chunks.push(c)); + req.on('end', () => resolve(Buffer.concat(chunks).toString())); + req.on('error', reject); + }); +} + +const invalidJSON = Symbol('invalid JSON'); + +function safeJSON(raw: string): unknown { + try { + return JSON.parse(raw); + } catch { + return invalidJSON; + } +} + +function escape(value: string): string { + return value.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`); +} + +function hidden(fields: Record): string { + return Object.entries(fields).map(([k, v]) => ``).join(''); +} + +function htmlPage(title: string, content: string): string { + return `${escape(title)}
${content}
`; +} diff --git a/e2e/integration/fake-github/state.ts b/e2e/integration/fake-github/state.ts new file mode 100644 index 000000000..4c24a94e2 --- /dev/null +++ b/e2e/integration/fake-github/state.ts @@ -0,0 +1,467 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import crypto from 'node:crypto'; + +import {merge, skeleton} from './contract/validator'; + +export const API = 'https://api.github.com'; +export const WEB = 'https://github.com'; + +export type Account = {id: number; login: string; type: 'User' | 'Organization'; name: string | null; email: string | null; createdAt: string}; +export type Org = Account & {members: Set; admins: Set; ssoEnforced: boolean}; +export type Team = {id: number; org: string; slug: string; name: string; members: Set}; +export type Label = {id: number; name: string; color: string; description: string | null}; +export type Milestone = {id: number; number: number; title: string; state: 'open' | 'closed'}; +export type Hook = {id: number; owner: string; repo?: string; url: string; secret: string; events: string[]; active: boolean; contentType: string; createdAt: string}; +export type Comment = {id: number; author: string; body: string; createdAt: string}; +export type Repo = { + id: number; owner: string; name: string; private: boolean; archived: boolean; description: string | null; + admins: Set; writers: Set; labels: Label[]; milestones: Milestone[]; nextNumber: number; createdAt: string; +}; +export type Item = { + id: number; repo: string; number: number; kind: 'issue' | 'pull'; title: string; body: string; + state: 'open' | 'closed'; merged: boolean; draft: boolean; author: string; assignees: string[]; labels: string[]; + milestone: number | null; reviewers: string[]; teamReviewers: string[]; head: string; base: string; + additions: number; deletions: number; changedFiles: number; comments: Comment[]; createdAt: string; updatedAt: string; + reviews: Review[]; ciState: 'success' | 'pending' | 'failure'; +}; +export type Review = {id: number; author: string; state: 'APPROVED' | 'CHANGES_REQUESTED' | 'COMMENTED'; submittedAt: string}; +export type Thread = {id: string; login: string; repo: string; number: number | null; title: string; type: 'Issue' | 'PullRequest' | 'Commit' | 'Release'; reason: string; unread: boolean; updatedAt: string}; +export type Token = {value: string; login: string; clientId: string; scopes: string[]; revoked: boolean}; +export type AuthCode = {code: string; login: string; clientId: string; redirectUri: string; scopes: string[]; expiresAt: number; used: boolean}; + +// GitHub folds scopes that another granted scope already covers. +const impliedBy: Record = { + public_repo: 'repo', 'repo:status': 'repo', repo_deployment: 'repo', 'repo:invite': 'repo', security_events: 'repo', + 'read:org': 'write:org', 'write:org': 'admin:org', 'read:user': 'user', 'user:email': 'user', 'user:follow': 'user', + 'read:repo_hook': 'write:repo_hook', 'write:repo_hook': 'admin:repo_hook', 'read:public_key': 'write:public_key', 'write:public_key': 'admin:public_key', +}; + +export function normalizeScopes(scopes: Iterable): string[] { + const set = new Set([...scopes].map((s) => s.trim()).filter(Boolean)); + const covered = (scope: string): boolean => { + const parent = impliedBy[scope]; + return Boolean(parent) && (set.has(parent) || covered(parent)); + }; + return [...set].filter((s) => !covered(s)).sort(); +} + +export function hasScope(granted: string[], ...accepted: string[]): boolean { + return accepted.some((scope) => { + for (let s: string | undefined = scope; s; s = impliedBy[s]) { + if (granted.includes(s)) { + return true; + } + } + return false; + }); +} + +export class State { + accounts = new Map(); + orgs = new Map(); + teams: Team[] = []; + repos = new Map(); + items: Item[] = []; + hooks: Hook[] = []; + threads: Thread[] = []; + tokens = new Map(); + codes = new Map(); + grants = new Map(); + signedIn: string | null = null; + private nextId = 1000; + + id(): number { + return this.nextId++; + } + + now(): string { + return new Date().toISOString().replace(/\.\d+Z$/, 'Z'); + } + + addUser(login: string, extra: Partial = {}): Account { + const account: Account = {id: this.id(), login, type: 'User', name: null, email: null, createdAt: this.now(), ...extra}; + this.accounts.set(login.toLowerCase(), account); + return account; + } + + addOrg(login: string, members: string[] = [], admins: string[] = []): Org { + const org: Org = {id: this.id(), login, type: 'Organization', name: null, email: null, createdAt: this.now(), members: new Set([...members, ...admins]), admins: new Set(admins), ssoEnforced: false}; + this.accounts.set(login.toLowerCase(), org); + this.orgs.set(login.toLowerCase(), org); + return org; + } + + account(login: string): Account | undefined { + return this.accounts.get(login.toLowerCase()); + } + + addRepo(repoName: string, options: Partial> & {admins?: string[]; writers?: string[]; labels?: string[]; milestones?: string[]} = {}): Repo { + const [owner, name] = repoName.split('/'); + const ownerAccount = this.account(owner); + if (!ownerAccount) { + throw new Error(`unknown owner ${owner}`); + } + const admins = new Set(options.admins ?? (ownerAccount.type === 'User' ? [owner] : [...this.orgs.get(owner.toLowerCase())!.admins])); + const repo: Repo = { + id: this.id(), owner: ownerAccount.login, name, private: options.private ?? false, archived: options.archived ?? false, + description: options.description ?? null, admins, writers: new Set([...admins, ...(options.writers ?? [])]), + labels: (options.labels ?? ['bug', 'enhancement']).map((l, i) => ({id: this.id(), name: l, color: ['d73a4a', 'a2eeef', '0075ca', '7057ff'][i % 4], description: null})), + milestones: (options.milestones ?? []).map((title, i) => ({id: this.id(), number: i + 1, title, state: 'open' as const})), + nextNumber: 1, createdAt: this.now(), + }; + this.repos.set(repoName.toLowerCase(), repo); + return repo; + } + + repo(owner: string, name: string): Repo | undefined { + return this.repos.get(`${owner}/${name}`.toLowerCase()); + } + + addItem(repo: Repo, fields: Partial & Pick): Item { + const now = this.now(); + const number = repo.nextNumber++; + const item: Item = { + id: this.id(), repo: fullName(repo), number, body: '', state: 'open', merged: false, draft: false, + assignees: [], labels: [], milestone: null, reviewers: [], teamReviewers: [], head: `feature-${number}`, base: 'main', + additions: 10, deletions: 2, changedFiles: 1, comments: [], reviews: [], ciState: 'success', createdAt: now, updatedAt: now, ...fields, + }; + this.items.push(item); + return item; + } + + item(owner: string, name: string, number: number): Item | undefined { + const key = `${owner}/${name}`.toLowerCase(); + return this.items.find((i) => i.repo.toLowerCase() === key && i.number === number); + } + + repoOf(item: Item): Repo { + return this.repos.get(item.repo.toLowerCase())!; + } + + issueToken(login: string, clientId: string, scopes: string[]): Token { + const value = `gho_${crypto.randomBytes(27).toString('base64url').replace(/[-_]/g, 'x').slice(0, 36)}`; + const token: Token = {value, login, clientId, scopes: normalizeScopes(scopes), revoked: false}; + this.tokens.set(value, token); + return token; + } + + // A classic OAuth app's token carries every scope the user ever granted that app. + grant(login: string, clientId: string, requested: string[]): string[] { + const key = `${clientId}:${login.toLowerCase()}`; + const scopes = normalizeScopes([...(this.grants.get(key) ?? []), ...requested]); + this.grants.set(key, scopes); + return scopes; + } + + canSee(viewer: Token | null, repo: Repo | undefined): boolean { + if (!repo) { + return false; + } + if (!repo.private) { + return true; + } + if (!viewer || !hasScope(viewer.scopes, 'repo')) { + return false; + } + const org = this.orgs.get(repo.owner.toLowerCase()); + return repo.writers.has(viewer.login) || repo.owner === viewer.login || Boolean(org?.members.has(viewer.login)); + } + + permissions(viewer: Token | null, repo: Repo) { + const login = viewer?.login ?? ''; + const admin = repo.admins.has(login); + const push = repo.writers.has(login); + return {admin, maintain: admin, push, triage: push, pull: true}; + } + + userJSON(account: Account, schema: 'simple-user' | 'public-user' | 'private-user' = 'simple-user') { + const login = account.login; + const detail = schema === 'simple-user' ? {} : { + name: account.name, email: account.email, created_at: account.createdAt, updated_at: account.createdAt, + public_repos: [...this.repos.values()].filter((r) => r.owner === login && !r.private).length, + }; + return merge(skeleton(schema), { + login, id: account.id, node_id: nodeId(account.type === 'User' ? 'U' : 'O', account.id), type: account.type, gravatar_id: '', + avatar_url: `${avatarBase}/u/${account.id}?v=4`, html_url: `${WEB}/${login}`, url: `${API}/users/${login}`, + repos_url: `${API}/users/${login}/repos`, organizations_url: `${API}/users/${login}/orgs`, + ...detail, + }); + } + + login(login: string): Account { + return this.account(login) ?? this.addUser(login); + } + + orgJSON(org: Org, schema: 'organization-simple' | 'organization-full' = 'organization-simple') { + return merge(skeleton(schema), { + login: org.login, id: org.id, node_id: nodeId('O', org.id), url: `${API}/orgs/${org.login}`, + repos_url: `${API}/orgs/${org.login}/repos`, hooks_url: `${API}/orgs/${org.login}/hooks`, + avatar_url: `${avatarBase}/u/${org.id}?v=4`, description: null, + ...(schema === 'organization-full' ? {html_url: `${WEB}/${org.login}`, type: 'Organization', created_at: org.createdAt, updated_at: org.createdAt} : {}), + }); + } + + repoJSON(repo: Repo, viewer: Token | null, schema: 'minimal-repository' | 'repository' | 'full-repository' = 'repository') { + const name = fullName(repo); + return merge(skeleton(schema), { + id: repo.id, node_id: nodeId('R', repo.id), name: repo.name, full_name: name, private: repo.private, archived: repo.archived, + visibility: repo.private ? 'private' : 'public', description: repo.description, default_branch: 'main', + owner: this.userJSON(this.account(repo.owner)!), + html_url: `${WEB}/${name}`, url: `${API}/repos/${name}`, hooks_url: `${API}/repos/${name}/hooks`, + issues_url: `${API}/repos/${name}/issues{/number}`, pulls_url: `${API}/repos/${name}/pulls{/number}`, + clone_url: `${WEB}/${name}.git`, svn_url: `${WEB}/${name}`, git_url: `git://github.com/${name}.git`, ssh_url: `git@github.com:${name}.git`, + has_issues: true, open_issues_count: this.items.filter((i) => i.repo === name && i.state === 'open').length, + created_at: repo.createdAt, updated_at: repo.createdAt, pushed_at: repo.createdAt, + permissions: this.permissions(viewer, repo), + }); + } + + labelJSON(repo: Repo, label: Label) { + return merge(skeleton('label'), {id: label.id, node_id: nodeId('LA', label.id), url: `${API}/repos/${fullName(repo)}/labels/${encodeURIComponent(label.name)}`, name: label.name, color: label.color, description: label.description}); + } + + milestoneJSON(repo: Repo, milestone: Milestone) { + const base = `${API}/repos/${fullName(repo)}/milestones/${milestone.number}`; + return merge(skeleton('milestone'), { + id: milestone.id, node_id: nodeId('MI', milestone.id), number: milestone.number, title: milestone.title, state: milestone.state, + url: base, labels_url: `${base}/labels`, html_url: `${WEB}/${fullName(repo)}/milestone/${milestone.number}`, + created_at: repo.createdAt, updated_at: repo.createdAt, + }); + } + + issueJSON(item: Item, schema: 'issue' | 'issue-search-result-item' = 'issue') { + const repo = this.repoOf(item); + const name = fullName(repo); + const kindPath = item.kind === 'pull' ? 'pull' : 'issues'; + const milestone = repo.milestones.find((m) => m.number === item.milestone); + return merge(skeleton(schema), { + id: item.id, node_id: nodeId(item.kind === 'pull' ? 'PR' : 'I', item.id), number: item.number, title: item.title, body: item.body, + state: item.state, state_reason: null, draft: item.kind === 'pull' ? item.draft : undefined, locked: false, comments: item.comments.length, + user: this.userJSON(this.login(item.author)), assignee: item.assignees[0] ? this.userJSON(this.login(item.assignees[0])) : null, + assignees: item.assignees.map((a) => this.userJSON(this.login(a))), + labels: item.labels.map((l) => this.labelJSON(repo, repo.labels.find((x) => x.name === l) ?? {id: 0, name: l, color: 'ededed', description: null})), + milestone: milestone ? this.milestoneJSON(repo, milestone) : null, + url: `${API}/repos/${name}/issues/${item.number}`, html_url: `${WEB}/${name}/${kindPath}/${item.number}`, + repository_url: `${API}/repos/${name}`, comments_url: `${API}/repos/${name}/issues/${item.number}/comments`, + labels_url: `${API}/repos/${name}/issues/${item.number}/labels{/name}`, events_url: `${API}/repos/${name}/issues/${item.number}/events`, + author_association: this.association(repo, item.author), + closed_at: item.state === 'closed' ? item.updatedAt : null, created_at: item.createdAt, updated_at: item.updatedAt, + ...(item.kind === 'pull' ? {pull_request: {url: `${API}/repos/${name}/pulls/${item.number}`, html_url: `${WEB}/${name}/pull/${item.number}`, diff_url: `${WEB}/${name}/pull/${item.number}.diff`, patch_url: `${WEB}/${name}/pull/${item.number}.patch`, merged_at: item.merged ? item.updatedAt : null}} : {}), + ...(schema === 'issue-search-result-item' ? {score: 1} : {}), + }); + } + + pullJSON(item: Item, viewer: Token | null) { + const repo = this.repoOf(item); + const name = fullName(repo); + const base = `${API}/repos/${name}/pulls/${item.number}`; + const ref = (branch: string, commit: string) => ({label: `${repo.owner}:${branch}`, ref: branch, sha: commit, user: this.userJSON(this.account(repo.owner)!), repo: this.repoJSON(repo, viewer)}); + const link = (href: string) => ({href}); + const issue = this.issueJSON(item); + return merge(skeleton('pull-request'), { + ...issue, pull_request: undefined, repository_url: undefined, labels_url: undefined, events_url: undefined, comments: item.comments.length, + url: base, html_url: `${WEB}/${name}/pull/${item.number}`, diff_url: `${WEB}/${name}/pull/${item.number}.diff`, patch_url: `${WEB}/${name}/pull/${item.number}.patch`, + issue_url: `${API}/repos/${name}/issues/${item.number}`, commits_url: `${base}/commits`, review_comments_url: `${base}/comments`, + review_comment_url: `${API}/repos/${name}/pulls/comments{/number}`, comments_url: `${API}/repos/${name}/issues/${item.number}/comments`, + statuses_url: `${API}/repos/${name}/statuses/${sha(item, 'head')}`, + head: ref(item.head, sha(item, 'head')), base: ref(item.base, sha(item, 'base')), + _links: {self: link(base), html: link(`${WEB}/${name}/pull/${item.number}`), issue: link(`${API}/repos/${name}/issues/${item.number}`), comments: link(`${API}/repos/${name}/issues/${item.number}/comments`), review_comments: link(`${base}/comments`), review_comment: link(`${API}/repos/${name}/pulls/comments{/number}`), commits: link(`${base}/commits`), statuses: link(`${API}/repos/${name}/statuses/${sha(item, 'head')}`)}, + requested_reviewers: item.reviewers.map((r) => this.userJSON(this.login(r))), + requested_teams: item.teamReviewers.map((slug) => this.teamJSON(this.teams.find((t) => t.org.toLowerCase() === repo.owner.toLowerCase() && t.slug === slug)!)), + merged: item.merged, merged_at: item.merged ? item.updatedAt : null, mergeable: item.state === 'open' ? true : null, mergeable_state: 'clean', + merge_commit_sha: null, draft: item.draft, auto_merge: null, merged_by: null, + additions: item.additions, deletions: item.deletions, changed_files: item.changedFiles, commits: 1, review_comments: 0, maintainer_can_modify: false, + }); + } + + reviewJSON(item: Item, review: Review) { + const html = `${WEB}/${item.repo}/pull/${item.number}#pullrequestreview-${review.id}`; + return merge(skeleton('pull-request-review'), { + id: review.id, node_id: nodeId('PRR', review.id), user: this.userJSON(this.login(review.author)), body: '', state: review.state, + html_url: html, pull_request_url: `${API}/repos/${item.repo}/pulls/${item.number}`, submitted_at: review.submittedAt, commit_id: sha(item, 'head'), + author_association: this.association(this.repoOf(item), review.author), + _links: {html: {href: html}, pull_request: {href: `${API}/repos/${item.repo}/pulls/${item.number}`}}, + }); + } + + combinedStatusJSON(item: Item) { + const repo = this.repoOf(item); + const head = sha(item, 'head'); + const url = `${API}/repos/${item.repo}/commits/${head}/status`; + return merge(skeleton('combined-commit-status'), { + state: item.ciState, sha: head, total_count: 1, commit_url: `${API}/repos/${item.repo}/commits/${head}`, url, + repository: this.repoJSON(repo, null, 'minimal-repository'), + statuses: [merge(skeleton('simple-commit-status'), {id: item.id, node_id: nodeId('SC', item.id), state: item.ciState, context: 'ci/e2e', description: null, target_url: null, avatar_url: null, url, created_at: item.updatedAt, updated_at: item.updatedAt})], + }); + } + + teamJSON(team: Team) { + const url = `${API}/orgs/${team.org}/teams/${team.slug}`; + return merge(skeleton('team-simple'), {id: team.id, node_id: nodeId('T', team.id), name: team.name, slug: team.slug, url, html_url: `${WEB}/orgs/${team.org}/teams/${team.slug}`, members_url: `${url}/members{/member}`, repositories_url: `${url}/repos`, permission: 'pull', type: 'organization', description: null}); + } + + hookJSON(hook: Hook, schema: 'hook' | 'org-hook') { + const base = hook.repo ? `${API}/repos/${hook.owner}/${hook.repo}/hooks/${hook.id}` : `${API}/orgs/${hook.owner}/hooks/${hook.id}`; + return merge(skeleton(schema), { + id: hook.id, type: hook.repo ? 'Repository' : 'Organization', name: 'web', active: hook.active, events: hook.events, + config: {url: hook.url, content_type: hook.contentType, insecure_ssl: '0', ...(hook.secret ? {secret: '********'} : {})}, + url: base, ping_url: `${base}/pings`, deliveries_url: `${base}/deliveries`, created_at: hook.createdAt, updated_at: hook.createdAt, + ...(schema === 'hook' ? {test_url: `${base}/test`, last_response: {code: null, status: 'unused', message: null}} : {}), + }); + } + + threadJSON(thread: Thread, viewer: Token | null) { + const repo = this.repos.get(thread.repo.toLowerCase())!; + const subjectURL = thread.number ? `${API}/repos/${thread.repo}/${thread.type === 'PullRequest' ? 'pulls' : 'issues'}/${thread.number}` : `${API}/repos/${thread.repo}`; + return merge(skeleton('thread'), { + id: thread.id, unread: thread.unread, reason: thread.reason, updated_at: thread.updatedAt, last_read_at: null, + subject: {title: thread.title, type: thread.type, url: subjectURL, latest_comment_url: subjectURL}, + repository: this.repoJSON(repo, viewer, 'minimal-repository'), + url: `${API}/notifications/threads/${thread.id}`, subscription_url: `${API}/notifications/threads/${thread.id}/subscription`, + }); + } + + commentJSON(item: Item, comment: Comment) { + const name = item.repo; + return merge(skeleton('issue-comment'), { + id: comment.id, node_id: nodeId('IC', comment.id), body: comment.body, user: this.userJSON(this.login(comment.author)), + url: `${API}/repos/${name}/issues/comments/${comment.id}`, html_url: `${WEB}/${name}/${item.kind === 'pull' ? 'pull' : 'issues'}/${item.number}#issuecomment-${comment.id}`, + issue_url: `${API}/repos/${name}/issues/${item.number}`, author_association: this.association(this.repoOf(item), comment.author), + created_at: comment.createdAt, updated_at: comment.createdAt, + }); + } + + association(repo: Repo, login: string): string { + if (repo.owner === login) { + return 'OWNER'; + } + if (this.orgs.get(repo.owner.toLowerCase())?.members.has(login)) { + return 'MEMBER'; + } + return repo.writers.has(login) ? 'COLLABORATOR' : 'NONE'; + } + + search(query: string, viewer: Token | null): Item[] { + const terms = parseSearch(query, viewer?.login ?? ''); + return this.items. + filter((item) => this.canSee(viewer, this.repoOf(item)) && terms.every((term) => term.negated !== this.matches(item, term))). + sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) || b.id - a.id); + } + + private matches(item: Item, {qualifier, values}: SearchTerm): boolean { + const repo = this.repoOf(item); + const eq = (a: string, b: string) => a.toLowerCase() === b.toLowerCase(); + return values.some((value) => { + switch (qualifier) { + case 'is': + case 'type': + case 'state': + return matchesIs(item, repo, value.toLowerCase()); + case 'author': + return eq(item.author, value); + case 'assignee': + return item.assignees.some((a) => eq(a, value)); + case 'review-requested': + return item.reviewers.some((r) => eq(r, value)) || + item.teamReviewers.some((slug) => this.teams.some((t) => t.org.toLowerCase() === repo.owner.toLowerCase() && t.slug === slug && [...t.members].some((m) => eq(m, value)))); + case 'mentions': + return new RegExp(`@${escapeRegExp(value)}\\b`, 'i').test(`${item.body}\n${item.comments.map((c) => c.body).join('\n')}`); + case 'involves': + return eq(item.author, value) || item.assignees.some((a) => eq(a, value)) || new RegExp(`@${escapeRegExp(value)}\\b`, 'i').test(item.body); + case 'org': + case 'user': + return eq(repo.owner, value); + case 'repo': + return eq(item.repo, value); + case 'archived': + return String(repo.archived) === value.toLowerCase(); + case 'draft': + return item.kind === 'pull' && String(item.draft) === value.toLowerCase(); + case 'label': + return item.labels.some((l) => eq(l, value)); + case 'text': + return `${item.title}\n${item.body}`.toLowerCase().includes(value.toLowerCase()); + default: + return false; + } + }); + } +} + +type SearchTerm = {qualifier: string; values: string[]; negated: boolean}; + +const knownQualifiers = new Set(['is', 'type', 'state', 'author', 'assignee', 'review-requested', 'mentions', 'involves', 'org', 'user', 'repo', 'archived', 'draft', 'label']); +const orQualifiers = new Set(['org', 'user', 'repo']); + +// GitHub ANDs qualifiers, except repeated org:/user:/repo:, which it ORs. Qualifier values are case-insensitive. +export function parseSearch(query: string, viewer: string): SearchTerm[] { + const terms: SearchTerm[] = []; + for (const raw of query.match(/(?:[^\s"]+|"[^"]*")+/g) ?? []) { + const negated = raw.startsWith('-'); + const token = negated ? raw.slice(1) : raw; + const colon = token.indexOf(':'); + const qualifier = colon > 0 ? token.slice(0, colon).toLowerCase() : ''; + let value = (knownQualifiers.has(qualifier) ? token.slice(colon + 1) : token).replace(/^"|"$/g, ''); + if (value === '@me') { + value = viewer; + } + const key = knownQualifiers.has(qualifier) ? qualifier : 'text'; + const existing = orQualifiers.has(key) && !negated ? terms.find((t) => t.qualifier === key && !t.negated) : undefined; + if (existing) { + existing.values.push(value); + } else { + terms.push({qualifier: key, values: [value], negated}); + } + } + return terms; +} + +function matchesIs(item: Item, repo: Repo, value: string): boolean { + switch (value) { + case 'pr': + case 'pull-request': + return item.kind === 'pull'; + case 'issue': + return item.kind === 'issue'; + case 'open': + return item.state === 'open'; + case 'closed': + return item.state === 'closed'; + case 'merged': + return item.merged; + case 'unmerged': + return item.kind === 'pull' && !item.merged; + case 'draft': + return item.draft; + case 'public': + return !repo.private; + case 'private': + return repo.private; + default: + return false; + } +} + +export function fullName(repo: Repo): string { + return `${repo.owner}/${repo.name}`; +} + +export function sha(item: Item, side: 'head' | 'base'): string { + return crypto.createHash('sha1').update(`${item.repo}#${item.number}:${side}`).digest('hex'); +} + +function nodeId(prefix: string, id: number): string { + return `${prefix}_kgDO${Buffer.from(String(id)).toString('base64url')}`; +} + +let avatarBase = 'http://fake-github:8093/avatars'; + +export function setAvatarBase(url: string) { + avatarBase = url; +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} diff --git a/e2e/integration/fake-github/webhooks.ts b/e2e/integration/fake-github/webhooks.ts new file mode 100644 index 000000000..36504fd42 --- /dev/null +++ b/e2e/integration/fake-github/webhooks.ts @@ -0,0 +1,128 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import crypto from 'node:crypto'; + +import {merge, skeleton, webhookSchemaName, webhookSkeleton} from './contract/validator'; +import {type Hook, type Item, type Repo, type State, WEB, fullName, sha} from './state'; + +export type WebhookRequest = {event: string; action?: string; repo: string; number?: number; sender: string; requestedReviewer?: string; comment?: string; commentId?: number; ref?: string; overrides?: Record}; + +export function buildPayload(state: State, req: WebhookRequest): Record { + const [owner, name] = req.repo.split('/'); + const repo = state.repo(owner, name); + if (!repo) { + throw new Error(`unknown repo ${req.repo}`); + } + const schemaName = webhookSchemaName(req.event, req.action); + if (!schemaName) { + throw new Error(`webhook ${req.event}.${req.action} is not in operations.json`); + } + const item = req.number === undefined ? undefined : state.item(owner, name, req.number); + const common = { + repository: repositoryWebhook(state, repo), + sender: state.userJSON(state.login(req.sender)), + ...(state.orgs.has(repo.owner.toLowerCase()) ? {organization: merge(skeleton('organization-simple-webhooks'), state.orgJSON(state.orgs.get(repo.owner.toLowerCase())!))} : {}), + }; + + let specific: Record = {}; + switch (req.event) { + case 'pull_request': + specific = { + action: req.action, number: item!.number, pull_request: merge(skeleton('pull-request-webhook'), state.pullJSON(item!, null)), + ...(req.requestedReviewer ? {requested_reviewer: state.userJSON(state.login(req.requestedReviewer))} : {}), + }; + break; + case 'issues': + specific = {action: req.action, issue: issueWebhook(state, item!)}; + break; + case 'issue_comment': { + let comment = item!.comments.find((c) => c.id === req.commentId); + if (!comment) { + comment = {id: state.id(), author: req.sender, body: req.comment ?? 'A comment', createdAt: state.now()}; + item!.comments.push(comment); + } + specific = {action: req.action, issue: issueWebhook(state, item!), comment: state.commentJSON(item!, comment)}; + break; + } + case 'push': + specific = push(state, repo, req.sender, req.ref ?? 'refs/heads/main'); + break; + default: + throw new Error(`no payload builder for ${req.event}`); + } + + return merge(merge(webhookSkeleton(schemaName), {...common, ...specific}), req.overrides ?? {}); +} + +export function pingPayload(state: State, hook: Hook, sender: string): Record { + const repo = hook.repo ? state.repo(hook.owner, hook.repo) : undefined; + const org = state.orgs.get(hook.owner.toLowerCase()); + return merge(webhookSkeleton('ping'), { + zen: 'Keep it logically awesome.', + hook_id: hook.id, + hook: state.hookJSON(hook, hook.repo ? 'hook' : 'org-hook'), + ...(repo ? {repository: repositoryWebhook(state, repo)} : {}), + ...(org ? {organization: merge(skeleton('organization-simple-webhooks'), state.orgJSON(org))} : {}), + sender: state.userJSON(state.login(sender)), + }); +} + +export type Delivery = {hookId: number; url: string; status: number; body: string}; + +export async function deliver(hooks: Hook[], event: string, payload: Record): Promise { + const body = JSON.stringify(payload); + return Promise.all(hooks.filter((h) => h.active && (event === 'ping' || h.events.includes(event) || h.events.includes('*'))).map(async (hook) => { + const sent = hook.contentType === 'form' ? `payload=${encodeURIComponent(body)}` : body; + const headers: Record = { + 'Content-Type': hook.contentType === 'form' ? 'application/x-www-form-urlencoded' : 'application/json', + 'User-Agent': 'GitHub-Hookshot/e2e0000', + 'X-GitHub-Event': event, + 'X-GitHub-Delivery': crypto.randomUUID(), + 'X-GitHub-Hook-ID': String(hook.id), + 'X-GitHub-Hook-Installation-Target-Type': hook.repo ? 'repository' : 'organization', + }; + if (hook.secret) { + Object.assign(headers, signatures(hook.secret, sent)); + } + try { + const res = await fetch(hook.url, {method: 'POST', headers, body: sent, signal: AbortSignal.timeout(10_000)}); + return {hookId: hook.id, url: hook.url, status: res.status, body: await res.text()}; + } catch (err) { + return {hookId: hook.id, url: hook.url, status: 0, body: String(err)}; + } + })); +} + +export function signatures(secret: string, body: string) { + return { + 'X-Hub-Signature': `sha1=${crypto.createHmac('sha1', secret).update(body).digest('hex')}`, + 'X-Hub-Signature-256': `sha256=${crypto.createHmac('sha256', secret).update(body).digest('hex')}`, + }; +} + +function repositoryWebhook(state: State, repo: Repo) { + return merge(skeleton('repository-webhooks'), state.repoJSON(repo, null)); +} + +function issueWebhook(state: State, item: Item) { + const issue = state.issueJSON(item); + return {...issue, reactions: merge(skeleton('reaction-rollup'), {url: `${issue.url}/reactions`})}; +} + +function push(state: State, repo: Repo, pusher: string, ref: string) { + const name = fullName(repo); + const after = sha({repo: name, number: Date.now()} as Item, 'head'); + const before = sha({repo: name, number: Date.now() - 1} as Item, 'base'); + const account = state.login(pusher); + const person = {name: account.name ?? account.login, email: account.email ?? `${account.login}@users.noreply.github.com`, username: account.login}; + const commit = { + id: after, tree_id: after, distinct: true, message: 'Update README', timestamp: state.now(), url: `${WEB}/${name}/commit/${after}`, + author: person, committer: person, added: [], removed: [], modified: ['README.md'], + }; + return { + ref, before, after, created: false, deleted: false, forced: false, base_ref: null, compare: `${WEB}/${name}/compare/${before.slice(0, 12)}...${after.slice(0, 12)}`, + commits: [commit], head_commit: commit, pusher: {name: account.login, email: person.email}, + repository: {...repositoryWebhook(state, repo), url: `${WEB}/${name}`, created_at: Math.floor(Date.parse(repo.createdAt) / 1000), pushed_at: Math.floor(Date.now() / 1000), stargazers: 0, master_branch: 'main'}, + }; +} diff --git a/e2e/integration/global_setup.ts b/e2e/integration/global_setup.ts new file mode 100644 index 000000000..d1b9d326c --- /dev/null +++ b/e2e/integration/global_setup.ts @@ -0,0 +1,20 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {baseGlobalSetup} from '@mattermost/playwright-lib'; + +import {startFakeGitHub} from './fake-github/server'; +import {loginAdmin} from './support/admin'; +import {fakeGitHubPort, fakeGitHubURL, oauthApp} from './support/constants'; +import {installPlugin} from './support/plugin'; + +export default async function globalSetup() { + await baseGlobalSetup(); + + const fake = await startFakeGitHub({port: fakeGitHubPort, publicURL: fakeGitHubURL, app: oauthApp}); + + const adminClient = await loginAdmin(); + await installPlugin(adminClient); + + return fake.close; +} diff --git a/e2e/integration/package-lock.json b/e2e/integration/package-lock.json new file mode 100644 index 000000000..7d8659bcd --- /dev/null +++ b/e2e/integration/package-lock.json @@ -0,0 +1,1954 @@ +{ + "name": "mattermost-plugin-github-e2e", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mattermost-plugin-github-e2e", + "devDependencies": { + "@mattermost/client": "11.3.0", + "@mattermost/playwright-lib": "11.4.0", + "@mattermost/types": "11.3.0", + "@playwright/test": "1.64.1", + "@types/node": "22.20.5", + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "cross-env": "7.0.3", + "graphql": "16.14.2", + "typescript": "5.9.3" + } + }, + "node_modules/@axe-core/playwright": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.0.tgz", + "integrity": "sha512-70vBT/Ylqpm65RQz2iCG2o0JJCEG/WCNyefTr2xcOcr1CoSee60gNQYUMZZ7YukoKkFLv26I/jjlsvwwp532oQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "axe-core": "~4.11.0" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@mattermost/client": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@mattermost/client/-/client-11.3.0.tgz", + "integrity": "sha512-ulDcKXRcmxwqsqqRJemyq47UlpSVnPNKQrg/FHfEEzgLdhTlnrdMnRGZUssuCeZ9DXRkSw+uZwudiJoJ0bc47A==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@mattermost/types": "11.3.0", + "typescript": "^4.3.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@mattermost/playwright-lib": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/@mattermost/playwright-lib/-/playwright-lib-11.4.0.tgz", + "integrity": "sha512-rzKROUwzDK3qMoV4wXZBW3RfHePmH0GhhqYNJtt74qT1JMKSqpLbSFYtcrRsF9MgPQRc5ZxwWHSKerj31Zx9qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@axe-core/playwright": "4.11.0", + "@mattermost/client": "11.3.0", + "@mattermost/types": "11.3.0", + "@percy/cli": "1.31.8", + "@percy/playwright": "1.0.10", + "async-wait-until": "2.0.31", + "axe-core": "4.11.1", + "chalk": "4.1.2", + "deepmerge": "4.3.1", + "dotenv": "17.2.3", + "luxon": "3.7.2", + "mime-types": "3.0.2", + "uuid": "13.0.0" + }, + "peerDependencies": { + "@playwright/test": ">=1.55.0" + } + }, + "node_modules/@mattermost/types": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@mattermost/types/-/types-11.3.0.tgz", + "integrity": "sha512-ZjkF1D28Jn+7Agki2de1FIFIWx7LbdqqIaaZ5JgOGpqtkkr9PQ0AW+WLOoEzokWEZ54dXB9INSuGLEczTfrt8w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": "^4.3.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@percy/cli": { + "version": "1.31.8", + "resolved": "https://registry.npmjs.org/@percy/cli/-/cli-1.31.8.tgz", + "integrity": "sha512-soyU/AgK3dkKIv1tNKIaU4oi+JBroWhpvm29LlQMeLk87cItkt/Lp3aTs3HHXUKZqYGuhLQlci11n2uLtUvmVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/cli-app": "1.31.8", + "@percy/cli-build": "1.31.8", + "@percy/cli-command": "1.31.8", + "@percy/cli-config": "1.31.8", + "@percy/cli-exec": "1.31.8", + "@percy/cli-snapshot": "1.31.8", + "@percy/cli-upload": "1.31.8", + "@percy/client": "1.31.8", + "@percy/logger": "1.31.8" + }, + "bin": { + "percy": "bin/run.cjs" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/cli-app": { + "version": "1.31.8", + "resolved": "https://registry.npmjs.org/@percy/cli-app/-/cli-app-1.31.8.tgz", + "integrity": "sha512-kbSv+ZVf/fpk5R8ewLb8LwHftTOHT2XJ6MNU9s81sKEv9iDqsf6vIiDF+/nKMSsMX+ns1evw+4I2vEjASmDzoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/cli-command": "1.31.8", + "@percy/cli-exec": "1.31.8" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/cli-build": { + "version": "1.31.8", + "resolved": "https://registry.npmjs.org/@percy/cli-build/-/cli-build-1.31.8.tgz", + "integrity": "sha512-aPZV6Lv7BRbGFqdGGPgfV8mMjgnoYQQQ7E1GFyVbZvsOrmqOif3osrmvwSwO7kd730cmKfhzg2iHbVBzEg+z4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/cli-command": "1.31.8" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/cli-command": { + "version": "1.31.8", + "resolved": "https://registry.npmjs.org/@percy/cli-command/-/cli-command-1.31.8.tgz", + "integrity": "sha512-nANwIfbLS78OJ9LV7WE4A6Tp1bN9OLpIPsZR7RgfNxG8YZFkyprbDA2zV+p6IYcfKoSnJBBD9isIUL+ITK+cgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/config": "1.31.8", + "@percy/core": "1.31.8", + "@percy/logger": "1.31.8" + }, + "bin": { + "percy-cli-readme": "bin/readme.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/cli-config": { + "version": "1.31.8", + "resolved": "https://registry.npmjs.org/@percy/cli-config/-/cli-config-1.31.8.tgz", + "integrity": "sha512-VmJkkwUkckbQVXecgQAewHR1lMWh/t8mEbVR9W/8iX/H1hOg1ONADFxNC/qfi3PhOO5kdWktS4r51jc0BgIEDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/cli-command": "1.31.8" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/cli-exec": { + "version": "1.31.8", + "resolved": "https://registry.npmjs.org/@percy/cli-exec/-/cli-exec-1.31.8.tgz", + "integrity": "sha512-SRZNwIZCyh38OtGWwPcQuwMVxeViHy4kP/Y0QJleDaOdH34MITwQuNMquf4Q7vZmEJ1mCamZpy/WUCDJICTuWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/cli-command": "1.31.8", + "@percy/logger": "1.31.8", + "cross-spawn": "^7.0.3", + "which": "^2.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/cli-snapshot": { + "version": "1.31.8", + "resolved": "https://registry.npmjs.org/@percy/cli-snapshot/-/cli-snapshot-1.31.8.tgz", + "integrity": "sha512-RQNx7eUq7Xml/EtQjsYvb9nWP/Wk4vf5SqAYHjAXSDDiK1K4BHdcxT7+GO1F+qq720/KgjwnC/JmBTulpC5a5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/cli-command": "1.31.8", + "yaml": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/cli-upload": { + "version": "1.31.8", + "resolved": "https://registry.npmjs.org/@percy/cli-upload/-/cli-upload-1.31.8.tgz", + "integrity": "sha512-yuegEGVcxd/PneheBfd0D9HE36EkdPS6u3m9m6Y8GPkN0UN7eOVxoOqfPHT9wCRfUjR3K9qcdBvo11S6biLWTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/cli-command": "1.31.8", + "fast-glob": "^3.2.11", + "image-size": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/client": { + "version": "1.31.8", + "resolved": "https://registry.npmjs.org/@percy/client/-/client-1.31.8.tgz", + "integrity": "sha512-MxhOY5DWJnW3dsmzYICgYOyKCFLwJcaC19jTsonfmQHBa8/cKs4mUKnrUgvtJlOsW6mSk6WrAn8vUUwZ4438xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/config": "1.31.8", + "@percy/env": "1.31.8", + "@percy/logger": "1.31.8", + "pac-proxy-agent": "^7.0.2", + "pako": "^2.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/config": { + "version": "1.31.8", + "resolved": "https://registry.npmjs.org/@percy/config/-/config-1.31.8.tgz", + "integrity": "sha512-iK7n0YOlmk4ITvQ8l4BeaInrS/cE8MqD+368cAEGVhPjGjarDKZ06FvEZXoxU8J+4LZbNyN9h/o+2UDtFRPYyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/logger": "1.31.8", + "ajv": "^8.6.2", + "cosmiconfig": "^8.0.0", + "yaml": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/core": { + "version": "1.31.8", + "resolved": "https://registry.npmjs.org/@percy/core/-/core-1.31.8.tgz", + "integrity": "sha512-p5KWNvbU8GUlA/DxV8q9N0alSzaEKx8aJxgO8TkmMA3HalMl/HogYw5+B7tvOUU0CAw9ar5vXyPXuOIm4wpqvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@percy/client": "1.31.8", + "@percy/config": "1.31.8", + "@percy/dom": "1.31.8", + "@percy/logger": "1.31.8", + "@percy/monitoring": "1.31.8", + "@percy/webdriver-utils": "1.31.8", + "content-disposition": "^0.5.4", + "cross-spawn": "^7.0.3", + "extract-zip": "^2.0.1", + "fast-glob": "^3.2.11", + "micromatch": "^4.0.8", + "mime-types": "^2.1.34", + "pako": "^2.1.0", + "path-to-regexp": "^6.3.0", + "rimraf": "^3.0.2", + "ws": "^8.17.1", + "yaml": "^2.4.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/core/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@percy/core/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@percy/dom": { + "version": "1.31.8", + "resolved": "https://registry.npmjs.org/@percy/dom/-/dom-1.31.8.tgz", + "integrity": "sha512-uw4aOTTXWgj3dZPrDlM+j0+MzwDz9u8EfE92fi2HsyqDXgp0hw957b3F/YEE9TMR27s9OgwXKnOCd8zauoj0Ew==", + "dev": true, + "license": "MIT" + }, + "node_modules/@percy/env": { + "version": "1.31.8", + "resolved": "https://registry.npmjs.org/@percy/env/-/env-1.31.8.tgz", + "integrity": "sha512-nIwSkwIijMCxvRZE2MV1rn51NGHM3EgRayuKxgkhevzc1s4Y785GTS8CZyJaPshs1fWVBork87Tm9Tbuh3u1PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/logger": "1.31.8" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/logger": { + "version": "1.31.8", + "resolved": "https://registry.npmjs.org/@percy/logger/-/logger-1.31.8.tgz", + "integrity": "sha512-OqnrtfmCdKW+2Z8LYk8Jc9HK0P89TJqp2qWnM913eDeoYZOKJX+2IyAZfsaFTcfEBAqHn0v1eynfldRedbTk3A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/monitoring": { + "version": "1.31.8", + "resolved": "https://registry.npmjs.org/@percy/monitoring/-/monitoring-1.31.8.tgz", + "integrity": "sha512-NBLQIoXWZb1Oi8M6Q6o8rmA6PzZi+65cxgyoKXHboxF7wAb8i8Vyc2JM7zTvay6RZ6JGuVHpqkxPcQaarxM1IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/config": "1.31.8", + "@percy/logger": "1.31.8", + "@percy/sdk-utils": "1.31.8", + "systeminformation": "^5.25.11" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/playwright": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@percy/playwright/-/playwright-1.0.10.tgz", + "integrity": "sha512-lq2Mbqz/SfguQn4PdbNwApmzZpA/3gWO7STLlyLNYd0r4btGd7Nfxyxkf/t78rgh2ErwGcLUuPbxGPpZ3XXLVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "playwright-core": ">=1" + } + }, + "node_modules/@percy/sdk-utils": { + "version": "1.31.8", + "resolved": "https://registry.npmjs.org/@percy/sdk-utils/-/sdk-utils-1.31.8.tgz", + "integrity": "sha512-S+qxi4TIOvToAD5j89nkdDj0Xj5CH8YJxpI6ZRVJE/UQE+amHIP34KiTdrWKw5aPlYEwNPeNn9UlXz5HUr5Z9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pac-proxy-agent": "^7.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/webdriver-utils": { + "version": "1.31.8", + "resolved": "https://registry.npmjs.org/@percy/webdriver-utils/-/webdriver-utils-1.31.8.tgz", + "integrity": "sha512-mfKR5EaXTTTLXX2JBFKmz9OphhHHBIMwe6lkI/hVgl1jvnTV2FXSSS2xb2Sume86uGFyj53NRH+HTNf/tepQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@percy/config": "1.31.8", + "@percy/sdk-utils": "1.31.8" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@playwright/test": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.64.1.tgz", + "integrity": "sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.64.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/async-wait-until": { + "version": "2.0.31", + "resolved": "https://registry.npmjs.org/async-wait-until/-/async-wait-until-2.0.31.tgz", + "integrity": "sha512-9VCfHvc4f36oT6sG5p16aKc9zojf3wF4FrjNDxU3Db51SJ1bQ5lWAWtQDDZPysTwSLKBDzNZ083qPkTIj6XnrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.14.0", + "npm": ">= 1.0.0" + }, + "funding": { + "type": "individual", + "url": "http://paypal.me/devlatoau" + } + }, + "node_modules/axe-core": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", + "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/graphql": { + "version": "16.14.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz", + "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "dev": true, + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.7.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.5.tgz", + "integrity": "sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pako": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz", + "integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.1.tgz", + "integrity": "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.64.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz", + "integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", + "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/systeminformation": { + "version": "5.33.19", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.33.19.tgz", + "integrity": "sha512-Zm7wGeCG7PqfvzegMJIzrxICopWqYCeLATVmvxOOci1isLcNVKgY4Fr0hhYQzvc7cgtK3377jQVChTFAnznOiA==", + "dev": true, + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=10.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/e2e/integration/package.json b/e2e/integration/package.json new file mode 100644 index 000000000..2b855a350 --- /dev/null +++ b/e2e/integration/package.json @@ -0,0 +1,30 @@ +{ + "name": "mattermost-plugin-github-e2e", + "private": true, + "scripts": { + "env:up": "docker compose up -d --wait", + "env:down": "docker compose --profile visual down -v", + "test": "playwright test", + "test:ci": "cross-env CI=true playwright test --grep-invert @visual", + "test:visual": "./scripts/visual.sh", + "test:visual-update-snapshots": "./scripts/visual.sh --update-snapshots", + "test:headed": "cross-env PW_HEADLESS=false playwright test", + "show-report": "playwright show-report", + "tsc": "tsc --noEmit", + "contract:update": "node fake-github/contract/update_specs.mjs", + "contract:check": "node fake-github/contract/update_specs.mjs --check", + "test:real": "playwright test -c playwright.real.config.ts" + }, + "devDependencies": { + "@mattermost/client": "11.3.0", + "@mattermost/playwright-lib": "11.4.0", + "@mattermost/types": "11.3.0", + "@playwright/test": "1.64.1", + "@types/node": "22.20.5", + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "cross-env": "7.0.3", + "graphql": "16.14.2", + "typescript": "5.9.3" + } +} diff --git a/e2e/integration/playwright.config.ts b/e2e/integration/playwright.config.ts new file mode 100644 index 000000000..0962f83f2 --- /dev/null +++ b/e2e/integration/playwright.config.ts @@ -0,0 +1,49 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +/* eslint-disable no-process-env */ + +import {defineConfig, devices} from '@playwright/test'; + +import {fakeGitHubHost, mattermostURL} from './support/constants'; +import type {FailureScreenshotOptions} from './support/fixture'; + +const isCI = Boolean(process.env.CI); +const visualBrowser = process.env.PW_VISUAL_BROWSER_WS; + +export default defineConfig({ + testDir: './tests', + outputDir: './test-results', + + // No platform suffix: baselines always come from the dockerized browser. + snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}', + globalSetup: require.resolve('./global_setup'), + fullyParallel: false, + workers: 1, + forbidOnly: isCI, + retries: isCI ? 1 : 0, + timeout: 60_000, + expect: {timeout: 15_000, toHaveScreenshot: {stylePath: './support/visual.css'}}, + reporter: isCI ? [['github'], ['html', {open: 'never', outputFolder: 'playwright-report'}]] : [['list'], ['html', {open: 'never'}]], + use: { + baseURL: mattermostURL, + headless: process.env.PW_HEADLESS !== 'false', + // Only fake-github tokens and a throwaway server's sessions end up in these, so CI keeps them too. + trace: 'retain-on-failure', + screenshot: 'off', + failureScreenshots: true, + + // The dockerized visual browser shares Mattermost's network and /etc/hosts, where fake-github already resolves. + launchOptions: { + slowMo: Number(process.env.PW_SLOWMO ?? 0), + args: visualBrowser ? [] : [`--host-resolver-rules=MAP ${fakeGitHubHost} 127.0.0.1`], + }, + connectOptions: visualBrowser ? {wsEndpoint: visualBrowser} : undefined, + }, + projects: [ + { + name: 'chrome', + use: {...devices['Desktop Chrome'], viewport: {width: 1280, height: 1024}}, + }, + ], +}); diff --git a/e2e/integration/playwright.real.config.ts b/e2e/integration/playwright.real.config.ts new file mode 100644 index 000000000..468004e4a --- /dev/null +++ b/e2e/integration/playwright.real.config.ts @@ -0,0 +1,42 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +/* eslint-disable no-process-env */ + +import {defineConfig, devices} from '@playwright/test'; + +import {mattermostURL} from './support/constants'; + +const isCI = Boolean(process.env.CI); + +// Journey against real github.com. Needs a normal `make dist` build, not `make e2e-dist`. +export default defineConfig({ + testDir: './real-github', + testMatch: /journey\.spec\.ts$/, + outputDir: './test-results/real-github', + globalSetup: require.resolve('./real-github/global_setup'), + fullyParallel: false, + workers: 1, + forbidOnly: isCI, + + // Retries would repeat GitHub writes. + retries: 0, + timeout: 5 * 60_000, + expect: {timeout: 30_000}, + reporter: isCI ? [['github'], ['list']] : [['list'], ['html', {open: 'never', outputFolder: 'playwright-report/real-github'}]], + use: { + baseURL: mattermostURL, + headless: isCI || process.env.PW_HEADLESS !== 'false', + + // Real tokens, GitHub cookies and the login password end up in traces and screenshots, so they stay off in CI. + trace: isCI ? 'off' : 'retain-on-failure', + screenshot: 'off', + video: 'off', + }, + projects: [ + { + name: 'real-github', + use: {...devices['Desktop Chrome'], viewport: {width: 1280, height: 1024}}, + }, + ], +}); diff --git a/e2e/integration/real-github/api.ts b/e2e/integration/real-github/api.ts new file mode 100644 index 000000000..6c8de42c9 --- /dev/null +++ b/e2e/integration/real-github/api.ts @@ -0,0 +1,57 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {expect} from '@playwright/test'; + +import {realGitHub} from './env'; +import {reserveWrite} from './rate_limit'; + +export type GitHubResponse = {status: number; body: T; headers: Headers}; + +export async function gh(method: string, path: string, {body, token = realGitHub.machineToken, auth}: {body?: unknown; token?: string; auth?: string} = {}): Promise> { + if (method !== 'GET') { + await reserveWrite(); + } + for (let attempt = 0; ; attempt++) { + const res = await fetch(`${realGitHub.apiURL}${path}`, { + method, + headers: { + Accept: 'application/vnd.github+json', + Authorization: auth ?? `Bearer ${token}`, + 'X-GitHub-Api-Version': '2022-11-28', + 'User-Agent': 'mattermost-plugin-github-e2e', + ...(body === undefined ? {} : {'Content-Type': 'application/json'}), + }, + body: body === undefined ? undefined : JSON.stringify(body), + }); + + // Secondary limits answer 403 or 429 with retry-after; primary ones send x-ratelimit-reset. + const retryAfter = Number(res.headers.get('retry-after') ?? 0); + if ((res.status === 429 || (res.status === 403 && (retryAfter || res.headers.get('x-ratelimit-remaining') === '0'))) && attempt < 2) { + const resetMs = Number(res.headers.get('x-ratelimit-reset') ?? 0) * 1000 - Date.now(); + const waitMs = retryAfter ? retryAfter * 1000 : resetMs; + if (waitMs > 0 && waitMs <= 2 * 60_000) { + await new Promise((r) => setTimeout(r, waitMs)); + continue; + } + } + const text = await res.text(); + return {status: res.status, body: (text ? JSON.parse(text) : undefined) as T, headers: res.headers}; + } +} + +export async function ghOK(method: string, path: string, options?: Parameters[2]): Promise { + const res = await gh(method, path, options); + if (res.status >= 300) { + throw new Error(`${method} ${path} answered ${res.status}: ${JSON.stringify(res.body).slice(0, 300)}`); + } + return res.body; +} + +// The search index trails writes by seconds to minutes. +export async function waitForSearch(query: string, matches: (item: {number: number; title: string}) => boolean, timeoutMs = 3 * 60_000) { + await expect.poll(async () => { + const res = await gh<{items: Array<{number: number; title: string}>}>('GET', `/search/issues?q=${encodeURIComponent(query)}&per_page=50`); + return res.status === 200 && res.body.items.some(matches); + }, {message: `GitHub search never returned a match for "${query}"`, timeout: timeoutMs, intervals: [5_000, 10_000, 15_000]}).toBe(true); +} diff --git a/e2e/integration/real-github/env.ts b/e2e/integration/real-github/env.ts new file mode 100644 index 000000000..fbe75e513 --- /dev/null +++ b/e2e/integration/real-github/env.ts @@ -0,0 +1,46 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +/* eslint-disable no-process-env */ + +import path from 'node:path'; + +import '../support/constants'; + +const required = [ + 'E2E_GITHUB_OAUTH_CLIENT_ID', + 'E2E_GITHUB_OAUTH_CLIENT_SECRET', + 'E2E_GITHUB_REPO', + 'E2E_GITHUB_MACHINE_TOKEN', +] as const; + +export function missingRealGitHubEnv() { + return required.filter((name) => !process.env[name]); +} + +const env = (name: (typeof required)[number]) => process.env[name] ?? ''; + +const [org, repo] = env('E2E_GITHUB_REPO').split('/'); + +export const realGitHub = { + apiURL: 'https://api.github.com', + webURL: 'https://github.com', + org, + repo, + fullName: `${org}/${repo}`, + oauth: {clientId: env('E2E_GITHUB_OAUTH_CLIENT_ID'), clientSecret: env('E2E_GITHUB_OAUTH_CLIENT_SECRET')}, + machineToken: env('E2E_GITHUB_MACHINE_TOKEN'), + + // Optional: the real-login test skips without them. + login: { + username: process.env.E2E_GITHUB_LOGIN_USERNAME || undefined, + password: process.env.E2E_GITHUB_LOGIN_PASSWORD || undefined, + totpSecret: process.env.E2E_GITHUB_LOGIN_TOTP_SECRET || undefined, + }, + + // Shared between global setup and the tests, which run in separate processes. + statePath: path.join(__dirname, '../.auth/real-github-state.json'), + writeLogPath: path.join(__dirname, '../.auth/real-github-writes.json'), +}; + +export const canSignInWithPassword = () => Boolean(realGitHub.login.username && realGitHub.login.password && realGitHub.login.totpSecret); diff --git a/e2e/integration/real-github/fixture.ts b/e2e/integration/real-github/fixture.ts new file mode 100644 index 000000000..bf194d49d --- /dev/null +++ b/e2e/integration/real-github/fixture.ts @@ -0,0 +1,45 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import type {Client4} from '@mattermost/client'; +import type {Channel} from '@mattermost/types/channels'; +import type {Team} from '@mattermost/types/teams'; +import type {UserProfile} from '@mattermost/types/users'; +import {expect, test as base} from '@mattermost/playwright-lib'; + +import {seedUserInfo} from '../support/kv'; + +import {realGitHub} from './env'; +import {readState, type RealState} from './fixtures'; + +export {expect}; + +export type RealGitHubFixture = { + adminClient: Client4; + user: UserProfile; + userClient: Client4; + team: Team; + channel: Channel; + state: RealState; + connectMachine: () => void; +}; + +// pw.initSetup() is deliberately avoided: it resets server config, which drops the plugin's settings. +export const test = base.extend<{real: RealGitHubFixture}>({ + real: async ({pw}, use) => { + const {adminClient} = await pw.getAdminClient(); + const team = await pw.createNewTeam(adminClient, {name: 'github', displayName: 'GitHub', type: 'O', unique: true}); + const user = await pw.createNewUserProfile(adminClient, {prefix: 'github', disableTutorial: true, disableOnboarding: true}); + await adminClient.addToTeam(team.id, user.id); + const {client: userClient} = await pw.makeClient(user); + if (!userClient) { + throw new Error(`Failed to log in ${user.username}`); + } + const channel = await adminClient.getChannelByName(team.id, 'town-square'); + const state = readState(); + + const connectMachine = () => seedUserInfo(user.id, state.login, realGitHub.machineToken, {allowedPrivateRepos: true}); + + await use({adminClient, user, userClient, team, channel, state, connectMachine}); + }, +}); diff --git a/e2e/integration/real-github/fixtures.ts b/e2e/integration/real-github/fixtures.ts new file mode 100644 index 000000000..2411b6d4b --- /dev/null +++ b/e2e/integration/real-github/fixtures.ts @@ -0,0 +1,99 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {randomBytes} from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; + +import {gh, ghOK, waitForSearch} from './api'; +import {realGitHub} from './env'; + +export type RealState = { + login: string; + issue: {number: number; title: string}; + pull: {number: number; title: string}; + hookId: number; + webhookSecret: string; +}; + +export const fixtureLabel = 'e2e-fixture'; +const fixtureBranch = 'e2e-fixture'; +const issueTitle = 'E2E fixture: assigned issue'; +const pullTitle = 'E2E fixture: open pull request'; + +// GitHub refuses hooks to private addresses, so this one never delivers; the tests replay its recorded deliveries instead. +export const fixtureHookURL = 'https://example.com/mattermost-plugin-github-e2e'; + +export function readState(): RealState { + return JSON.parse(fs.readFileSync(realGitHub.statePath, 'utf8')); +} + +// Long-lived fixtures are created once and reused, so a nightly run costs a handful of writes. +export async function frontLoad(): Promise { + const {fullName} = realGitHub; + const {login} = await ghOK<{login: string}>('GET', '/user'); + + if ((await gh('GET', `/repos/${fullName}/labels/${fixtureLabel}`)).status === 404) { + await ghOK('POST', `/repos/${fullName}/labels`, {body: {name: fixtureLabel, color: 'ededed', description: 'Kept open for the Mattermost plugin E2E suite'}}); + } + + const issue = await ensureIssue(login); + const pull = await ensurePull(login); + + await waitForSearch(`repo:${fullName} is:issue is:open assignee:${login} label:${fixtureLabel}`, (i) => i.number === issue.number); + await waitForSearch(`repo:${fullName} is:pr is:open author:${login} label:${fixtureLabel}`, (i) => i.number === pull.number); + + const hookId = await ensureHook(); + const webhookSecret = randomBytes(20).toString('hex'); + await ghOK('PATCH', `/repos/${fullName}/hooks/${hookId}/config`, {body: {url: fixtureHookURL, content_type: 'json', secret: webhookSecret}}); + const state: RealState = {login, issue, pull, hookId, webhookSecret}; + + fs.mkdirSync(path.dirname(realGitHub.statePath), {recursive: true}); + fs.writeFileSync(realGitHub.statePath, JSON.stringify(state)); + return state; +} + +async function ensureIssue(login: string) { + const open = await ghOK>('GET', `/repos/${realGitHub.fullName}/issues?labels=${fixtureLabel}&state=open&per_page=100`); + const found = open.find((i) => !i.pull_request && i.title === issueTitle); + if (found) { + return {number: found.number, title: found.title}; + } + const created = await ghOK('POST', `/repos/${realGitHub.fullName}/issues`, {body: {title: issueTitle, body: 'Kept open for the E2E suite.', labels: [fixtureLabel], assignees: [login]}}); + return {number: created.number, title: issueTitle}; +} + +async function ensurePull(login: string) { + const {fullName, org} = realGitHub; + const open = await ghOK>('GET', `/repos/${fullName}/pulls?state=open&head=${org}:${fixtureBranch}`); + if (open.length) { + return {number: open[0].number, title: pullTitle}; + } + + const {default_branch: base} = await ghOK('GET', `/repos/${fullName}`); + if ((await gh('GET', `/repos/${fullName}/git/ref/heads/${fixtureBranch}`)).status === 404) { + const {object} = await ghOK('GET', `/repos/${fullName}/git/ref/heads/${base}`); + await ghOK('POST', `/repos/${fullName}/git/refs`, {body: {ref: `refs/heads/${fixtureBranch}`, sha: object.sha}}); + } + if ((await gh('GET', `/repos/${fullName}/contents/e2e-fixture.md?ref=${fixtureBranch}`)).status === 404) { + await ghOK('PUT', `/repos/${fullName}/contents/e2e-fixture.md`, {body: {message: 'Add E2E fixture file', content: Buffer.from('Kept open for the E2E suite.\n').toString('base64'), branch: fixtureBranch}}); + } + const created = await ghOK('POST', `/repos/${fullName}/pulls`, {body: {title: pullTitle, head: fixtureBranch, base, body: 'Kept open for the E2E suite.'}}); + await ghOK('POST', `/repos/${fullName}/issues/${created.number}/labels`, {body: {labels: [fixtureLabel]}}); + if (created.user.login !== login) { + throw new Error('The fixture pull request must be authored by the machine account'); + } + return {number: created.number, title: pullTitle}; +} + +async function ensureHook() { + const hooks = await ghOK>('GET', `/repos/${realGitHub.fullName}/hooks?per_page=100`); + const found = hooks.find((h) => h.config.url === fixtureHookURL); + if (found) { + return found.id; + } + const created = await ghOK('POST', `/repos/${realGitHub.fullName}/hooks`, { + body: {name: 'web', active: true, events: ['issues', 'issue_comment', 'pull_request'], config: {url: fixtureHookURL, content_type: 'json'}}, + }); + return created.id as number; +} diff --git a/e2e/integration/real-github/global_setup.ts b/e2e/integration/real-github/global_setup.ts new file mode 100644 index 000000000..d6eb5c5d3 --- /dev/null +++ b/e2e/integration/real-github/global_setup.ts @@ -0,0 +1,28 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {baseGlobalSetup} from '@mattermost/playwright-lib'; + +import {loginAdmin} from '../support/admin'; +import {installPlugin} from '../support/plugin'; + +import {missingRealGitHubEnv, realGitHub} from './env'; +import {frontLoad} from './fixtures'; + +export default async function globalSetup() { + const missing = missingRealGitHubEnv(); + if (missing.length) { + throw new Error(`Set ${missing.join(', ')} in e2e/integration/.env (see .env.example). Do not paste the values into chat.`); + } + const state = await frontLoad(); + await baseGlobalSetup(); + + const adminClient = await loginAdmin(); + await installPlugin(adminClient, { + githuboauthclientid: realGitHub.oauth.clientId, + githuboauthclientsecret: realGitHub.oauth.clientSecret, + webhooksecret: state.webhookSecret, + githuborg: realGitHub.org, + enableprivaterepo: true, + }); +} diff --git a/e2e/integration/real-github/journey.spec.ts b/e2e/integration/real-github/journey.spec.ts new file mode 100644 index 000000000..1e5c5ecf9 --- /dev/null +++ b/e2e/integration/real-github/journey.spec.ts @@ -0,0 +1,153 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {validateGraphQLQuery, validateResponse, validateWebhook} from '../fake-github/contract/validator'; +import {signatures} from '../fake-github/webhooks'; +import {mattermostURL, webhookURL} from '../support/constants'; +import {readUserInfo} from '../support/kv'; +import {channelPosts, chooseOption, openChannel, openCreateIssueModal, runSlashCommand, selectField, sidebarButton, waitForPost} from '../support/ui'; + +import {gh, ghOK} from './api'; +import {canSignInWithPassword, realGitHub} from './env'; +import {expect, test, type RealGitHubFixture} from './fixture'; +import {readState} from './fixtures'; +import {completeGitHubSignIn} from './login/github_login'; +import {reserveWrite} from './rate_limit'; + +const {fullName} = realGitHub; + +type Delivery = {id: number; event: string; action: string | null; delivered_at: string}; +type DeliveryDetail = Delivery & {request: {headers: Record; payload: Record}}; + +// GitHub can't reach this Mattermost, so the hook's recorded delivery is fetched and posted to the plugin. +async function replayDelivery(real: RealGitHubFixture, event: string, match: (payload: Record) => boolean) { + const path = `/repos/${fullName}/hooks/${real.state.hookId}/deliveries`; + let detail: DeliveryDetail | undefined; + await expect.poll(async () => { + const recent = await ghOK('GET', `${path}?per_page=20`); + for (const d of recent.filter((r) => r.event === event)) { + const candidate = await ghOK('GET', `${path}/${d.id}`); + if (match(candidate.request.payload)) { + detail = candidate; + return true; + } + } + return false; + }, {message: `no ${event} delivery recorded`, timeout: 2 * 60_000, intervals: [5_000]}).toBe(true); + + const {payload, headers} = detail!.request; + + // The API hands back the parsed payload, not the bytes GitHub signed, so it is re-signed with the hook's secret. + const body = JSON.stringify(payload); + const res = await fetch(webhookURL, { + method: 'POST', + body, + headers: {'Content-Type': 'application/json', 'X-GitHub-Event': event, 'X-GitHub-Delivery': headers['X-GitHub-Delivery'] ?? '', ...signatures(real.state.webhookSecret, body)}, + }); + return {status: res.status, payload}; +} + +test.describe.serial('real GitHub with a KV-seeded machine token', () => { + test('GitHub still behaves the way fake-github models it', async () => { + const hooks = await gh('GET', `/repos/${fullName}/hooks`); + const accepted = (hooks.headers.get('x-accepted-oauth-scopes') ?? '').split(',').map((s) => s.trim()).filter(Boolean); + expect.soft(accepted, 'fake-github answers repo hooks with 404 unless the token has one of these scopes').toEqual(expect.arrayContaining(['admin:repo_hook', 'read:repo_hook', 'repo', 'write:repo_hook'])); + expect.soft(accepted).not.toContain('public_repo'); + + const query = `{search(type: ISSUE, first: 1, query: "repo:${fullName} label:e2e-fixture") {nodes {... on Issue {state} ... on PullRequest {state}}}}`; + expect(validateGraphQLQuery(query).errors, 'the validator lets this shape conflict through').toEqual([]); + const graphql = await ghOK<{data?: unknown; errors?: unknown[]}>('POST', '/graphql', {body: {query}}); + expect.soft(graphql.errors, 'GitHub answers overlapping Issue/PullRequest state fields').toBeUndefined(); + + const token = await fetch(`${realGitHub.webURL}/login/oauth/access_token`, { + method: 'POST', + headers: {Accept: 'application/json', 'Content-Type': 'application/json'}, + body: JSON.stringify({client_id: realGitHub.oauth.clientId, client_secret: realGitHub.oauth.clientSecret, code: 'not-a-real-code'}), + }); + expect.soft(token.status, 'token errors come back as HTTP 200').toBe(200); + expect.soft(await token.json()).toMatchObject({error: 'bad_verification_code'}); + + const {issue, pull} = readState(); + for (const path of ['/user', `/repos/${fullName}`, `/repos/${fullName}/issues/${issue.number}`, `/repos/${fullName}/pulls/${pull.number}`, `/repos/${fullName}/labels`]) { + expect.soft(validateResponse('GET', path, 200, await ghOK('GET', path)), `real ${path} matches the committed spec`).toEqual([]); + } + }); + + test('the sidebar and tooltip show the front-loaded issue and pull request', async ({pw, real}) => { + real.connectMachine(); + const {page, channelsPage} = await openChannel(pw, real.user, real.team.name); + + await runSlashCommand(channelsPage, '/github me'); + await expect(page.getByText(real.state.login).last()).toBeVisible(); + + const rhs = page.locator('#rhsContainer'); + await sidebarButton(page, 'fa-compress').click(); + await expect(rhs.getByRole('link', {name: real.state.pull.title})).toBeVisible(); + await sidebarButton(page, 'fa-list-ol').click(); + await expect(rhs.getByRole('link', {name: real.state.issue.title})).toBeVisible(); + + const url = `${realGitHub.webURL}/${fullName}/issues/${real.state.issue.number}`; + await real.userClient.createPost({channel_id: real.channel.id, message: url} as any); + await page.getByRole('link', {name: url}).last().hover(); + await expect(page.locator('.github-tooltip .box').first()).toContainText(real.state.issue.title); + }); + + test('an issue created from a post reaches GitHub, and its webhook posts back to the channel', async ({pw, real}) => { + real.connectMachine(); + const title = `E2E journey ${Date.now()}`; + await real.userClient.executeCommand(`/github subscriptions add ${fullName} --features issues`, {channel_id: real.channel.id, team_id: real.team.id}); + await real.userClient.createPost({channel_id: real.channel.id, message: 'File this from Mattermost'} as any); + const {channelsPage, page} = await openChannel(pw, real.user, real.team.name); + + const modal = await openCreateIssueModal(channelsPage, page); + await expect(selectField(modal, 'Organization')).toContainText(realGitHub.org); + await chooseOption(page, selectField(modal, 'Repository'), realGitHub.repo); + await modal.getByLabel('Title for the GitHub Issue').fill(title); + await reserveWrite(); + await modal.getByRole('button', {name: 'Submit'}).click(); + await expect(modal).toBeHidden(); + + const reply = await waitForPost(() => channelPosts(real.adminClient, real.channel.id), (p) => p.message.startsWith('Created GitHub issue'), 'issue created reply'); + const number = Number(/\/issues\/(\d+)/.exec(reply.message)?.[1]); + try { + expect(await ghOK('GET', `/repos/${fullName}/issues/${number}`)).toMatchObject({title, user: {login: real.state.login}}); + + const {status, payload} = await replayDelivery(real, 'issues', (p) => p.action === 'opened' && p.issue?.number === number); + expect.soft(validateWebhook('issues', payload), 'the real payload matches the schema fake-github validates against').toEqual([]); + expect(status).toBe(200); + const post = await waitForPost(() => channelPosts(real.adminClient, real.channel.id), (p) => p.message.includes('#new-issue') && p.message.includes(title), 'issues.opened post'); + expect(post.message).toContain(`/${fullName}/issues/${number}`); + } finally { + await ghOK('PATCH', `/repos/${fullName}/issues/${number}`, {body: {state: 'closed', state_reason: 'not_planned'}}); + } + }); +}); + +test.describe('real GitHub sign-in', () => { + test.skip(!canSignInWithPassword(), 'needs E2E_GITHUB_LOGIN_USERNAME, _PASSWORD and _TOTP_SECRET'); + + test('a user connects through github.com with password and TOTP', async ({pw, real}) => { + const {page} = await openChannel(pw, real.user, real.team.name); + const complete = `${mattermostURL}/plugins/github/oauth/complete`; + + await page.goto(`${mattermostURL}/plugins/github/oauth/connect`); + await completeGitHubSignIn(page, () => page.url().startsWith(complete)); + await expect(page.getByText(/Completed connecting to GitHub/i)).toBeVisible(); + + const info = readUserInfo(real.user.id); + expect(info?.GitHubUsername.toLowerCase()).toBe(realGitHub.login.username!.toLowerCase()); + const token = info!.Token.access_token; + try { + const me = await gh('GET', '/user', {token}); + const scopes = (me.headers.get('x-oauth-scopes') ?? '').split(',').map((s) => s.trim()).filter(Boolean).sort(); + expect(scopes, 'the plugin asks for these scopes without /github connect private').toEqual(['admin:org_hook', 'notifications', 'public_repo', 'read:org']); + + const hooks = await gh('GET', `/repos/${fullName}/hooks`, {token}); + expect.soft(hooks.status, 'fake-github hides repo hooks from public_repo tokens, so /github subscriptions add always notes a missing webhook').toBe(404); + } finally { + // Revoking the grant brings back the consent screen and drops any widened scopes before the next run. + const basic = `Basic ${Buffer.from(`${realGitHub.oauth.clientId}:${realGitHub.oauth.clientSecret}`).toString('base64')}`; + await gh('DELETE', `/applications/${realGitHub.oauth.clientId}/grant`, {auth: basic, body: {access_token: token}}); + } + }); +}); diff --git a/e2e/integration/real-github/login/github_login.ts b/e2e/integration/real-github/login/github_login.ts new file mode 100644 index 000000000..1a49cc2c2 --- /dev/null +++ b/e2e/integration/real-github/login/github_login.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import type {Locator, Page} from '@playwright/test'; + +import {realGitHub} from '../env'; + +import {generateTotp} from './totp'; + +// Walks github.com sign-in, 2FA and OAuth consent until isDone. GitHub skips the emailed device check for 2FA accounts. +export async function completeGitHubSignIn(page: Page, isDone: () => boolean, timeoutMs = 2 * 60_000) { + const {username, password, totpSecret} = realGitHub.login; + let lastCode = ''; + + const steps: Array<[Locator, (el: Locator) => Promise]> = [ + [page.locator('input#login_field'), async (el) => { + await el.fill(username!); + await page.locator('input#password').fill(password!); + await page.locator('input[name="commit"]').click(); + }], + [page.locator('a[href="/sessions/two-factor/app"]'), (el) => el.click()], + [page.locator('input#app_totp'), async (el) => { + let code = generateTotp(totpSecret!); + while (code === lastCode) { + await page.waitForTimeout(2000); + code = generateTotp(totpSecret!); + } + lastCode = code; + + // The field submits itself once six digits are in. + await el.fill(code); + }], + [page.getByRole('button', {name: /^(remind me later|not now|skip)/i}).first(), (el) => el.click()], + [page.locator('button#js-oauth-authorize-btn:not([disabled])'), (el) => el.click()], + ]; + + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (isDone()) { + return; + } + if (new URL(page.url()).pathname.startsWith('/sessions/verified-device')) { + throw new Error('GitHub asked for an emailed device code; the login account needs 2FA with an authenticator app'); + } + for (const [el, act] of steps) { + if (await el.isVisible().catch(() => false)) { + await act(el); + await page.waitForLoadState('domcontentloaded').catch(() => undefined); + break; + } + } + await page.waitForTimeout(1000); + } + throw new Error(`GitHub sign-in did not finish; last page was ${new URL(page.url()).pathname}`); +} diff --git a/e2e/integration/real-github/login/totp.ts b/e2e/integration/real-github/login/totp.ts new file mode 100644 index 000000000..9ef21d596 --- /dev/null +++ b/e2e/integration/real-github/login/totp.ts @@ -0,0 +1,37 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {createHmac} from 'node:crypto'; + +// RFC 6238 TOTP. +export function generateTotp(secret: string, stepSeconds = 30): string { + const key = base32Decode(secret); + const counter = Math.floor(Date.now() / 1000 / stepSeconds); + const buf = Buffer.alloc(8); + buf.writeUInt32BE(Math.floor(counter / 0x100000000), 0); + buf.writeUInt32BE(counter >>> 0, 4); + const hmac = createHmac('sha1', key).update(buf).digest(); + const offset = hmac[hmac.length - 1] & 0x0f; + const code = + ((hmac[offset] & 0x7f) << 24) | + ((hmac[offset + 1] & 0xff) << 16) | + ((hmac[offset + 2] & 0xff) << 8) | + (hmac[offset + 3] & 0xff); + return (code % 1_000_000).toString().padStart(6, '0'); +} + +function base32Decode(secret: string): Buffer { + const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + let bits = ''; + for (const char of secret.toUpperCase().replace(/[=\s-]/g, '')) { + const value = alphabet.indexOf(char); + if (value >= 0) { + bits += value.toString(2).padStart(5, '0'); + } + } + const bytes: number[] = []; + for (let i = 0; i + 8 <= bits.length; i += 8) { + bytes.push(parseInt(bits.slice(i, i + 8), 2)); + } + return Buffer.from(bytes); +} diff --git a/e2e/integration/real-github/rate_limit.ts b/e2e/integration/real-github/rate_limit.ts new file mode 100644 index 000000000..39ebac2b9 --- /dev/null +++ b/e2e/integration/real-github/rate_limit.ts @@ -0,0 +1,42 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import fs from 'node:fs'; +import path from 'node:path'; + +import {realGitHub} from './env'; + +// GitHub's secondary limits for content-creating requests. Kept on disk so back-to-back local runs share one budget. +const limits = [ + {windowMs: 60_000, max: 80}, + {windowMs: 60 * 60_000, max: 500}, +]; + +function readLog(): number[] { + try { + return JSON.parse(fs.readFileSync(realGitHub.writeLogPath, 'utf8')); + } catch { + return []; + } +} + +// Waits until one more write fits in every window, then records it. Call before each write, including ones the plugin makes. +export async function reserveWrite() { + for (;;) { + const now = Date.now(); + const log = readLog().filter((t) => now - t < limits.at(-1)!.windowMs); + const waitMs = Math.max(0, ...limits.map(({windowMs, max}) => { + const inWindow = log.filter((t) => now - t < windowMs); + return inWindow.length < max ? 0 : inWindow[inWindow.length - max] + windowMs - now; + })); + if (waitMs === 0) { + fs.mkdirSync(path.dirname(realGitHub.writeLogPath), {recursive: true}); + fs.writeFileSync(realGitHub.writeLogPath, JSON.stringify([...log, now])); + return; + } + if (waitMs > 5 * 60_000) { + throw new Error(`GitHub write budget exhausted for ${Math.ceil(waitMs / 60_000)} more minutes`); + } + await new Promise((r) => setTimeout(r, waitMs)); + } +} diff --git a/e2e/integration/scripts/visual.sh b/e2e/integration/scripts/visual.sh new file mode 100755 index 000000000..2d61420e8 --- /dev/null +++ b/e2e/integration/scripts/visual.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +PW_VERSION="$(node -p "require('@playwright/test/package.json').version")" +export PW_VERSION +# Baselines are tied to this exact server build; bump it together with test:visual-update-snapshots. +export MM_IMAGE=mattermost/mattermost-enterprise-edition:12.0.1 + +# A separate, throwaway stack: the pinned server must not start on a database another image already migrated. +export COMPOSE_PROJECT_NAME=github-plugin-e2e-visual +export MM_PORT="${VISUAL_MM_PORT:-8077}" +export PW_BROWSER_PORT="${VISUAL_BROWSER_PORT:-3337}" +trap 'docker compose --profile visual down -v >/dev/null 2>&1' EXIT + +docker compose --profile visual up -d --wait + +PW_VISUAL_BROWSER_WS="ws://127.0.0.1:${PW_BROWSER_PORT}/" npx playwright test --grep @visual "$@" diff --git a/e2e/integration/support/admin.ts b/e2e/integration/support/admin.ts new file mode 100644 index 000000000..e750b5b01 --- /dev/null +++ b/e2e/integration/support/admin.ts @@ -0,0 +1,12 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Client4} from '@mattermost/client'; +import {testConfig} from '@mattermost/playwright-lib'; + +export async function loginAdmin(): Promise { + const client = new Client4(); + client.setUrl(testConfig.baseURL); + await client.login(testConfig.adminUsername, testConfig.adminPassword); + return client; +} diff --git a/e2e/integration/support/constants.ts b/e2e/integration/support/constants.ts new file mode 100644 index 000000000..178850ac4 --- /dev/null +++ b/e2e/integration/support/constants.ts @@ -0,0 +1,43 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +/* eslint-disable no-process-env */ + +import fs from 'node:fs'; +import path from 'node:path'; + +// docker compose reads the same file, so MM_PORT/MM_IMAGE stay in sync between the stack and the tests. +const envFile = path.join(__dirname, '../.env'); +if (fs.existsSync(envFile)) { + process.loadEnvFile(envFile); +} + +export const pluginId = 'github'; +export const pluginDistDir = path.join(__dirname, '../../../dist'); + +// Must match the server's SiteURL in docker-compose.yml. +process.env.PW_BASE_URL ??= `http://127.0.0.1:${process.env.MM_PORT ?? 8069}`; +export const mattermostURL = process.env.PW_BASE_URL; + +// Baked into the plugin by `make e2e-dist`. fake-github resolves from the Mattermost container (extra_hosts) +// and from Chromium (--host-resolver-rules); the tests reach it on localhost. +export const fakeGitHubHost = 'fake-github'; +export const fakeGitHubPort = 8093; +export const fakeGitHubURL = `http://${fakeGitHubHost}:${fakeGitHubPort}`; +export const fakeGitHubControlURL = `http://localhost:${fakeGitHubPort}`; + +export const oauthClientId = 'Iv1.e2e0123456789abc'; +export const oauthClientSecret = 'e2e-github-oauth-client-secret-0000000000'; +export const webhookSecret = 'e2e-github-webhook-secret'; + +// AES-256 needs a 32-byte key. +export const encryptionKey = 'e2e-encryption-key-0123456789abc'; + +export const webhookURL = `${mattermostURL}/plugins/${pluginId}/webhook`; + +export const oauthApp = { + clientId: oauthClientId, + clientSecret: oauthClientSecret, + callbackURL: `${mattermostURL}/plugins/${pluginId}/oauth/complete`, + name: 'Mattermost E2E', +}; diff --git a/e2e/integration/support/fake_github.ts b/e2e/integration/support/fake_github.ts new file mode 100644 index 000000000..52a1d6735 --- /dev/null +++ b/e2e/integration/support/fake_github.ts @@ -0,0 +1,48 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import type {JournalEntry} from '../fake-github/server'; +import type {Account, Hook, Item, Thread, Token} from '../fake-github/state'; +import type {Delivery, WebhookRequest} from '../fake-github/webhooks'; + +import {fakeGitHubControlURL} from './constants'; + +async function control(method: string, path: string, body?: unknown): Promise { + const res = await fetch(`${fakeGitHubControlURL}/__control${path}`, { + method, + headers: {'Content-Type': 'application/json'}, + body: body === undefined ? undefined : JSON.stringify(body), + }); + if (!res.ok) { + throw new Error(`fake-github control ${method} ${path} failed: ${res.status} ${await res.text()}`); + } + return res.json() as Promise; +} + +type ItemFields = Partial> & Pick; + +export const fakeGitHub = { + reset: () => control('POST', '/reset'), + requests: () => control('GET', '/requests'), + calls: async () => (await fakeGitHub.requests()).map((r) => `${r.method} ${r.path}`), + contractViolations: () => control('GET', '/contract-violations'), + settle: () => control('POST', '/settle'), + createUser: (login: string, extra: {name?: string; email?: string} = {}) => control('POST', '/users', {login, ...extra}), + createOrg: (login: string, options: {members?: string[]; admins?: string[]; ssoEnforced?: boolean} = {}) => control('POST', '/orgs', {login, ...options}), + setSSO: (org: string, enforced: boolean) => control('POST', '/sso', {org, enforced}), + createTeam: (org: string, slug: string, members: string[]) => control('POST', '/teams', {org, slug, members}), + createRepo: (fullName: string, options: {private?: boolean; archived?: boolean; admins?: string[]; writers?: string[]; labels?: string[]; milestones?: string[]} = {}) => control<{fullName: string; id: number}>('POST', '/repos', {fullName, ...options}), + createItem: (repo: string, fields: ItemFields) => control('POST', '/items', {repo, ...fields}), + updateItem: (repo: string, number: number, patch: Partial) => control('PATCH', '/items', {repo, number, ...patch}), + items: () => control('GET', '/items'), + createThread: (login: string, repo: string, fields: Partial = {}) => control('POST', '/threads', {login, repo, ...fields}), + createHook: (owner: string, repo: string | undefined, url: string, secret: string, events = ['*']) => control('POST', '/hooks', {owner, repo, url, secret, events}), + hooks: () => control('GET', '/hooks'), + issueToken: (login: string, scopes?: string[]) => control('POST', '/tokens', {login, scopes}), + grant: (login: string, scopes: string[]) => control<{scopes: string[]}>('POST', '/grants', {login, scopes}), + revoke: (token: string) => control('POST', '/revoke', {token}), + signIn: (login: string | null) => control('POST', '/sign-in', {login}), + setOAuthApp: (patch: {codeTTLms?: number; clientSecret?: string}) => control('POST', '/oauth-app', patch), + fault: (method: string, path: string, status: number, options: {body?: unknown; headers?: Record; times?: number} = {}) => control('POST', '/fault', {method, path, status, ...options}), + webhook: (request: WebhookRequest & {deliver?: boolean}) => control<{payload: Record; deliveries: Delivery[]}>('POST', '/webhooks', request), +}; diff --git a/e2e/integration/support/fixture.ts b/e2e/integration/support/fixture.ts new file mode 100644 index 000000000..578e05dca --- /dev/null +++ b/e2e/integration/support/fixture.ts @@ -0,0 +1,78 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import type {Client4} from '@mattermost/client'; +import type {Channel} from '@mattermost/types/channels'; +import type {Team} from '@mattermost/types/teams'; +import type {UserProfile} from '@mattermost/types/users'; +import {expect, test as base} from '@mattermost/playwright-lib'; + +import type {Token} from '../fake-github/state'; + +import {fakeGitHub} from './fake_github'; +import {seedUserInfo} from './kv'; + +export {expect}; + +type GitHubConnection = {login: string; token: Token}; + +export type GitHubFixture = { + adminClient: Client4; + user: UserProfile; + userClient: Client4; + team: Team; + channel: Channel; + login: string; + connect: (mattermostUser?: UserProfile, options?: {login?: string; scopes?: string[]; allowedPrivateRepos?: boolean}) => Promise; +}; + +export type FailureScreenshotOptions = {failureScreenshots: boolean}; + +// pw.initSetup() is deliberately avoided: it resets server config, which drops the plugin's settings and PluginStates. +export const test = base.extend<{github: GitHubFixture; failureScreenshot: void}, FailureScreenshotOptions>({ + failureScreenshots: [false, {option: true, scope: 'worker'}], + + // Playwright's own failure screenshot runs after pw closes the context and only sees a blank page. + failureScreenshot: [async ({pw, failureScreenshots}, use, testInfo) => { + await use(); + if (!failureScreenshots || testInfo.status === testInfo.expectedStatus) { + return; + } + for (const [i, page] of (pw.testBrowser.context?.pages() ?? []).entries()) { + const body = await page.screenshot({fullPage: true}).catch(() => undefined); + if (body) { + await testInfo.attach(`failure-page-${i}`, {body, contentType: 'image/png'}); + } + } + }, {auto: true}], + + github: async ({pw}, use) => { + await fakeGitHub.reset(); + + const {adminClient} = await pw.getAdminClient(); + const team = await pw.createNewTeam(adminClient, {name: 'github', displayName: 'GitHub', type: 'O', unique: true}); + const user = await pw.createNewUserProfile(adminClient, {prefix: 'github', disableTutorial: true, disableOnboarding: true}); + await adminClient.addToTeam(team.id, user.id); + const {client: userClient} = await pw.makeClient(user); + if (!userClient) { + throw new Error(`Failed to log in ${user.username}`); + } + const channel = await adminClient.getChannelByName(team.id, 'town-square'); + + // GitHub logins are global; deriving them from the random Mattermost username keeps tests independent. + const login = `gh-${user.username}`; + await fakeGitHub.createUser(login); + + const connect: GitHubFixture['connect'] = async (mattermostUser = user, options = {}) => { + const ghLogin = options.login ?? (mattermostUser.id === user.id ? login : `gh-${mattermostUser.username}`); + const token = await fakeGitHub.issueToken(ghLogin, options.scopes); + seedUserInfo(mattermostUser.id, ghLogin, token.value, {allowedPrivateRepos: options.allowedPrivateRepos}); + return {login: ghLogin, token}; + }; + + await use({adminClient, user, userClient, team, channel, login, connect}); + + await fakeGitHub.settle(); + expect(await fakeGitHub.contractViolations(), 'fake-github traffic must match GitHub\'s REST, GraphQL and webhook schemas').toEqual([]); + }, +}); diff --git a/e2e/integration/support/kv.ts b/e2e/integration/support/kv.ts new file mode 100644 index 000000000..f2a699cb3 --- /dev/null +++ b/e2e/integration/support/kv.ts @@ -0,0 +1,105 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +/* eslint-disable no-process-env */ + +import {execFileSync} from 'node:child_process'; +import {createCipheriv, createDecipheriv, randomBytes} from 'node:crypto'; +import path from 'node:path'; + +import {encryptionKey, pluginId} from './constants'; + +export type StoredUserInfo = { + UserID: string; + Token: {access_token: string; token_type: string; expiry: string}; + GitHubUsername: string; + LastToDoPostAt: number; + Settings: {sidebar_buttons: string; daily_reminder: boolean; daily_reminder_on_change: boolean; notifications: boolean}; + AllowedPrivateRepos: boolean; + MM34646ResetTokenDone: boolean; +}; + +const composeDir = path.join(__dirname, '..'); + +// Keys and format must match storeGitHubUserInfo and storeGitHubToUserIDMapping (server/plugin/plugin.go). +export function seedUserInfo(mattermostUserId: string, githubLogin: string, accessToken: string, options: {allowedPrivateRepos?: boolean; settings?: Partial; lastToDoPostAt?: number; key?: string} = {}) { + const info: StoredUserInfo = { + UserID: mattermostUserId, + + // Classic OAuth app tokens never expire; oauth2 stores the zero time. + Token: {access_token: encrypt(accessToken, options.key), token_type: 'bearer', expiry: '0001-01-01T00:00:00Z'}, + GitHubUsername: githubLogin, + + // A recent value keeps the daily reminder from posting into the DM channel tests assert on. + LastToDoPostAt: options.lastToDoPostAt ?? Date.now(), + Settings: {sidebar_buttons: 'team', daily_reminder: true, daily_reminder_on_change: false, notifications: true, ...options.settings}, + AllowedPrivateRepos: options.allowedPrivateRepos ?? false, + MM34646ResetTokenDone: true, + }; + upsert({ + [`${mattermostUserId}_githubtoken`]: JSON.stringify(info), + [`${githubLogin}_githubusername`]: mattermostUserId, + }); +} + +export function readUserInfo(mattermostUserId: string): StoredUserInfo | undefined { + const raw = get(`${mattermostUserId}_githubtoken`); + if (!raw) { + return undefined; + } + const info = JSON.parse(raw) as StoredUserInfo; + return {...info, Token: {...info.Token, access_token: decrypt(info.Token.access_token)}}; +} + +export function seedTokenResetDone() { + upsert({mm34646_token_reset_done: 'done'}); +} + +export function hasKey(key: string) { + return Boolean(get(key)); +} + +export function deleteKey(key: string) { + psql(`DELETE FROM pluginkeyvaluestore WHERE pluginid = '${pluginId}' AND pkey = ${quote(key)};`); +} + +function get(key: string) { + return psql(`SELECT convert_from(pvalue, 'UTF8') FROM pluginkeyvaluestore WHERE pluginid = '${pluginId}' AND pkey = ${quote(key)};`, ['-A', '-t']).trim(); +} + +function upsert(entries: Record) { + const values = Object.entries(entries).map(([key, value]) => `('${pluginId}', ${quote(key)}, convert_to(${quote(value)}, 'UTF8'), 0)`); + psql(`INSERT INTO pluginkeyvaluestore (pluginid, pkey, pvalue, expireat) VALUES ${values.join(', ')} + ON CONFLICT (pluginid, pkey) DO UPDATE SET pvalue = EXCLUDED.pvalue, expireat = 0;`); +} + +function psql(sql: string, flags: string[] = []) { + return execFileSync('docker', ['compose', 'exec', '-T', 'postgres', 'psql', '-U', 'mmuser', '-d', 'mattermost_test', '-v', 'ON_ERROR_STOP=1', '-q', ...flags], { + cwd: composeDir, + input: sql, + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'inherit'], + env: process.env, + }); +} + +function quote(value: string) { + return `'${value.replaceAll("'", "''")}'`; +} + +// Mirrors encrypt/decrypt in server/plugin/utils.go: PKCS#7 padding, AES-CFB with a random IV prefix, padded URL-safe base64. +export function encrypt(text: string, key = encryptionKey) { + const iv = randomBytes(16); + const plain = Buffer.from(text); + const padding = 16 - (plain.length % 16); + const cipher = createCipheriv('aes-256-cfb', Buffer.from(key), iv); + const body = Buffer.concat([cipher.update(Buffer.concat([plain, Buffer.alloc(padding, padding)])), cipher.final()]); + return Buffer.concat([iv, body]).toString('base64').replaceAll('+', '-').replaceAll('/', '_'); +} + +export function decrypt(text: string, key = encryptionKey) { + const data = Buffer.from(text.replaceAll('-', '+').replaceAll('_', '/'), 'base64'); + const decipher = createDecipheriv('aes-256-cfb', Buffer.from(key), data.subarray(0, 16)); + const plain = Buffer.concat([decipher.update(data.subarray(16)), decipher.final()]); + return plain.subarray(0, plain.length - plain[plain.length - 1]).toString(); +} diff --git a/e2e/integration/support/layout.ts b/e2e/integration/support/layout.ts new file mode 100644 index 000000000..d7f8caa6e --- /dev/null +++ b/e2e/integration/support/layout.ts @@ -0,0 +1,47 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {expect, type Locator} from '@playwright/test'; + +// Sub-pixel rounding differs between fonts and zoom levels. +const tolerance = 1; + +async function box(locator: Locator) { + const b = await locator.boundingBox(); + if (!b) { + throw new Error(`${locator} has no bounding box; is it visible?`); + } + return b; +} + +export async function expectInside(child: Locator, parent: Locator) { + const [c, p] = await Promise.all([box(child), box(parent)]); + expect({ + left: c.x >= p.x - tolerance, + top: c.y >= p.y - tolerance, + right: c.x + c.width <= p.x + p.width + tolerance, + bottom: c.y + c.height <= p.y + p.height + tolerance, + }, `${child} should sit inside ${parent}`).toEqual({left: true, top: true, right: true, bottom: true}); +} + +export async function expectNoOverlap(a: Locator, b: Locator) { + const [x, y] = await Promise.all([box(a), box(b)]); + const overlaps = x.x + tolerance < y.x + y.width && y.x + tolerance < x.x + x.width && + x.y + tolerance < y.y + y.height && y.y + tolerance < x.y + x.height; + expect(overlaps, `${a} and ${b} should not overlap`).toBe(false); +} + +export async function expectNothingClipped(root: Locator) { + const clipped = await root.evaluate((el, tol) => { + const offenders: string[] = []; + for (const node of [el, ...Array.from(el.querySelectorAll('*'))]) { + const style = getComputedStyle(node); + const clips = style.overflowX !== 'visible' || style.overflowY !== 'visible' || style.textOverflow === 'ellipsis'; + if (clips && (node.scrollWidth > node.clientWidth + tol || node.scrollHeight > node.clientHeight + tol)) { + offenders.push(`<${node.tagName.toLowerCase()} class="${node.className}"> ${node.scrollWidth}x${node.scrollHeight} in ${node.clientWidth}x${node.clientHeight}`); + } + } + return offenders; + }, tolerance); + expect(clipped, `content under ${root} should not be clipped`).toEqual([]); +} diff --git a/e2e/integration/support/plugin.ts b/e2e/integration/support/plugin.ts new file mode 100644 index 000000000..6c8e2f73d --- /dev/null +++ b/e2e/integration/support/plugin.ts @@ -0,0 +1,94 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import fs from 'node:fs/promises'; +import path from 'node:path'; + +import type {Client4} from '@mattermost/client'; + +import {encryptionKey, oauthClientId, oauthClientSecret, pluginDistDir, pluginId, webhookSecret} from './constants'; +import {seedTokenResetDone} from './kv'; + +export const defaultPluginSettings = { + usepreregisteredapplication: false, + githuboauthclientid: oauthClientId, + githuboauthclientsecret: oauthClientSecret, + webhooksecret: webhookSecret, + encryptionkey: encryptionKey, + githuborg: '', + enterprisebaseurl: '', + enterpriseuploadurl: '', + enableleftsidebar: true, + enableprivaterepo: false, + connecttoprivatebydefault: false, + enablecodepreview: 'public', + enablewebhookeventlogging: false, + getnotificationfordraftprs: false, +}; + +export async function findPluginBundle(): Promise { + const files = await fs.readdir(pluginDistDir).catch(() => [] as string[]); + const bundles = await Promise.all( + files. + filter((name) => name.startsWith(`${pluginId}-`) && name.endsWith('.tar.gz')). + map(async (name) => { + const fullPath = path.join(pluginDistDir, name); + return {fullPath, mtime: (await fs.stat(fullPath)).mtimeMs}; + }), + ); + if (!bundles.length) { + throw new Error(`No ${pluginId} bundle found in ${pluginDistDir}; run \`make e2e-dist\` first`); + } + return bundles.sort((a, b) => b.mtime - a.mtime)[0].fullPath; +} + +export async function installPlugin(adminClient: Client4, settings: Partial = {}) { + const bundlePath = await findPluginBundle(); + const data = await fs.readFile(bundlePath); + const file = new File([data], path.basename(bundlePath), {type: 'application/gzip'}); + + await adminClient.uploadPlugin(file as unknown as globalThis.File, true); + await configureServer(adminClient, settings); + + // Otherwise the plugin resets every stored token through GitHub a minute after it starts. + seedTokenResetDone(); + await adminClient.enablePlugin(pluginId); + await waitForPluginRunning(adminClient); +} + +export async function patchPluginSettings(adminClient: Client4, settings: Partial) { + const config = await adminClient.getConfig(); + await adminClient.patchConfig({ + PluginSettings: { + Plugins: { + ...config.PluginSettings.Plugins, + [pluginId]: {...config.PluginSettings.Plugins[pluginId], ...settings}, + }, + }, + }); +} + +async function configureServer(adminClient: Client4, settings: Partial) { + await adminClient.patchConfig({ + ServiceSettings: {EnableTutorial: false, EnableOnboardingFlow: false}, + + // playwright-lib's random users use a short password that only passes these relaxed rules. + PasswordSettings: {MinimumLength: 5, Lowercase: false, Number: false, Uppercase: false, Symbol: false}, + }); + await patchPluginSettings(adminClient, {...defaultPluginSettings, ...settings}); +} + +export async function waitForPluginRunning(adminClient: Client4, timeoutMs = 60_000) { + const deadline = Date.now() + timeoutMs; + let lastState = 'unknown'; + while (Date.now() < deadline) { + const statuses = await adminClient.getPluginStatuses(); + const status = statuses.find((s) => s.plugin_id === pluginId); + if (status?.state === 2) { + return; + } + lastState = status ? `state=${status.state}` : 'not installed'; + await new Promise((r) => setTimeout(r, 500)); + } + throw new Error(`Plugin ${pluginId} did not reach Running: ${lastState}`); +} diff --git a/e2e/integration/support/seed.ts b/e2e/integration/support/seed.ts new file mode 100644 index 000000000..a145ed5e2 --- /dev/null +++ b/e2e/integration/support/seed.ts @@ -0,0 +1,25 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import type {GitHubFixture} from './fixture'; +import {fakeGitHub} from './fake_github'; + +export const repo = 'acme/widgets'; +export const labels = ['bug', 'enhancement', 'needs-design-review-from-the-platform-team']; +export const longTitle = 'Refactor the widget polishing pipeline so that it handles extremely long pull request titles gracefully '.repeat(2).trim(); + +// A fixed date keeps the tooltip's date line identical between runs. +const createdAt = '2024-01-15T12:00:00Z'; + +export async function seedWork(github: GitHubFixture) { + const dates = {createdAt, updatedAt: createdAt}; + await fakeGitHub.createUser('octo-author'); + await fakeGitHub.createOrg('acme', {members: [github.login, 'octo-author']}); + await fakeGitHub.createRepo(repo, {writers: [github.login, 'octo-author'], labels, milestones: ['v1.0']}); + const pr = await fakeGitHub.createItem(repo, {kind: 'pull', title: longTitle, author: github.login, labels, body: 'Splits polishing into stages.', ...dates}); + await fakeGitHub.createItem(repo, {kind: 'pull', title: 'Please review the gizmo', author: 'octo-author', reviewers: [github.login], ...dates}); + await fakeGitHub.createItem(repo, {kind: 'issue', title: 'Widgets squeak on Mondays', author: 'octo-author', assignees: [github.login], milestone: 1, ...dates}); + await fakeGitHub.createThread(github.login, repo, {number: pr.number, reason: 'mention'}); + await github.connect(); + return pr; +} diff --git a/e2e/integration/support/ui.ts b/e2e/integration/support/ui.ts new file mode 100644 index 000000000..12dfc96be --- /dev/null +++ b/e2e/integration/support/ui.ts @@ -0,0 +1,92 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import type {Client4} from '@mattermost/client'; +import type {Post} from '@mattermost/types/posts'; +import type {UserProfile} from '@mattermost/types/users'; +import {expect, type ChannelsPage, type PlaywrightExtended} from '@mattermost/playwright-lib'; +import type {Locator, Page} from '@playwright/test'; + +export async function openChannel(pw: PlaywrightExtended, user: UserProfile, teamName: string, channelName = 'town-square') { + const {channelsPage, page} = await pw.testBrowser.login(user); + await channelsPage.goto(teamName, channelName); + await channelsPage.toBeVisible(); + return {channelsPage, page}; +} + +export async function runSlashCommand(channelsPage: ChannelsPage, command: string) { + const postCreate = channelsPage.centerView.postCreate; + await postCreate.input.fill(command); + + // Enter would pick the highlighted slash-command autocomplete suggestion instead of sending. + await postCreate.sendMessageButton.click(); + await expect(postCreate.input).toHaveValue(''); +} + +export function sidebarButton(page: Page, icon: 'fa-compress' | 'fa-code-fork' | 'fa-list-ol' | 'fa-envelope') { + return page.locator(`a:has(> i.${icon})`).first(); +} + +export function sidebarButtonBar(page: Page) { + return sidebarButton(page, 'fa-compress').locator('xpath=ancestor::div[.//i[contains(@class, "fa-envelope")]][1]'); +} + +// Swaps per-run values (random usernames) for fixed text so screenshots are comparable. +export async function replaceText(target: Locator, replacements: Array<[string | RegExp, string]>) { + await target.evaluate((root, pairs) => { + const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); + for (let node = walker.nextNode(); node; node = walker.nextNode()) { + for (const [source, flags, to] of pairs) { + node.textContent = node.textContent!.replace(new RegExp(source, flags), to); + } + } + }, replacements.map(([from, to]) => (typeof from === 'string' ? [from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g', to] : [from.source, from.flags, to]))); +} + +export async function openCreateIssueModal(channelsPage: ChannelsPage, page: Page) { + const post = await channelsPage.getLastPost(); + await post.hover(); + await post.postMenu.actionsButton.click(); + await page.getByRole('menuitem', {name: 'Create GitHub Issue'}).click(); + const modal = page.getByRole('dialog').filter({hasText: 'Create GitHub Issue'}); + await expect(modal.getByLabel('Title for the GitHub Issue')).toBeVisible(); + await page.evaluate(() => Promise.all(document.getAnimations().filter((a) => a.effect?.getComputedTiming().endTime !== Infinity).map((a) => a.finished))); + return modal; +} + +// The react-select menus in the plugin don't expose role=option, so pick by filtering to one match. +export async function chooseOption(page: Page, field: Locator, option: string) { + await field.getByRole('combobox').click(); + await page.keyboard.type(option); + await expect(field.getByRole('log')).toContainText(/1 result available|option .* focused/); + await page.keyboard.press('Enter'); + await expect(field).toContainText(option); +} + +export function selectField(modal: Locator, label: string) { + return modal.getByText(label, {exact: true}).locator('xpath=..'); +} + +export function postWithText(page: Page, text: string | RegExp) { + return page.locator('.post').filter({hasText: text}).last(); +} + +export async function channelPosts(client: Client4, channelId: string): Promise { + const {order, posts} = await client.getPosts(channelId); + return order.map((id) => posts[id]); +} + +export async function botDMPosts(client: Client4, userId: string): Promise { + const bot = await client.getUserByUsername('github'); + const channel = await client.createDirectChannel([userId, bot.id]); + return channelPosts(client, channel.id); +} + +export async function waitForPost(load: () => Promise, match: (post: Post) => boolean, message: string): Promise { + let found: Post | undefined; + await expect.poll(async () => { + found = (await load()).find(match); + return Boolean(found); + }, {message, timeout: 15_000}).toBe(true); + return found!; +} diff --git a/e2e/integration/support/visual.css b/e2e/integration/support/visual.css new file mode 100644 index 000000000..cf371e8f3 --- /dev/null +++ b/e2e/integration/support/visual.css @@ -0,0 +1,4 @@ +/* Core UI that overlaps plugin posts and changes between runs. */ +.post .status { + visibility: hidden !important; +} diff --git a/e2e/integration/tests/__screenshots__/visual.spec.ts/attach-issue-modal.png b/e2e/integration/tests/__screenshots__/visual.spec.ts/attach-issue-modal.png new file mode 100644 index 0000000000000000000000000000000000000000..de6ecf46c17f599df09e7d712e1c00ad895c6b86 GIT binary patch literal 20631 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 BFqZ%T literal 0 HcmV?d00001 diff --git a/e2e/integration/tests/__screenshots__/visual.spec.ts/profile-popover.png b/e2e/integration/tests/__screenshots__/visual.spec.ts/profile-popover.png new file mode 100644 index 0000000000000000000000000000000000000000..c78e069edf14b09ca4f14efc30c7a0facc291481 GIT binary patch literal 1437 zcmV;O1!DS%P)3kcu*z1}Njjg5|eO5S)3S~c@$SX`2 zP#k>nzSivL&*mDv)M0_+#ChY#8-qhcepbHY=Ztz2 zvpP$NJvgSrCt_k)RvOz@tumOHf;7Hs6vwfA8DC>$w$~_W2J<6~9c#R7!X7Bo;S+IJ z@b8-R6-PC)IBvy;Adk6ME0rzn=4~s*g`=VnxYljkU#C&)>5g6-v`PYntlcGni(|Nu zK=T$*XWC5iE=!n%T$w5KFmz$I+7BGpLcZ1*n7BxGX`v`)HrqAc)N0ZVGl@~0_t#6d z*QhSEo1IoqFX5N17Ej(5vOoOP>XTYfXz1ubW`8ALL=xDRN~MQS3^DzXCodS`Ek2Ut zJ(oC#1EKa~i*aFiRyvb}UlJNfz{tW!xgfU~!cUNF635JTNIKqgMvG&RV2xRXN&*Nz7jL>^ z%1q?-sOYP=tZ%MxD`ISf`!k=*GMdM6+pVHOV-lq9D`O%!T?01n@%y)Jzh(XEl&-Va zCZt#cLv);!jUoI53HN<#rv)ToNi5f64$>36Mu&~bfKcnay-NI6jS38T9DL{6Kxe7s zgg0Ju$-xs!nJX{Wc8Y zCrD_XsdO}~K;QU6wQLcUlj7)#HC!BS_`~@7zmD{`JWb$Xb53sF^vsO4Ig>4PR%Hp` zaGgF9L=r5lO<_rj8WExPE=$IIDqKQ9K!6SGk^ON?AN_MTR>e=I!XI+#E7DoWu}_J2Mg=5K={txud5jU2rD@=*^yOGd-2c2N{FY z?|(DE;%-hTiZjGrz^D3Xq}Irk9aKZz6EEdJkq(Q>lH35l?~@`PesJ^@F2Xk;HA47^ r{{a91|Nr|6C+7eF00v1!K~w_(@T|r>QmNH~00000NkvXXu0mjfXo#t) literal 0 HcmV?d00001 diff --git a/e2e/integration/tests/__screenshots__/visual.spec.ts/reply-thread.png b/e2e/integration/tests/__screenshots__/visual.spec.ts/reply-thread.png new file mode 100644 index 0000000000000000000000000000000000000000..18e5fe12375be2c36a408a9adab02882c6e62885 GIT binary patch literal 24959 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 literal 0 HcmV?d00001 diff --git a/e2e/integration/tests/__screenshots__/visual.spec.ts/rhs-review-requests.png b/e2e/integration/tests/__screenshots__/visual.spec.ts/rhs-review-requests.png new file mode 100644 index 0000000000000000000000000000000000000000..ecaa04890dd1c0420a409568cfa71fecb0e85529 GIT binary patch literal 19211 zcmeHvX*8SN+pj)-s)MRNZBg^ns``+u`_uBhfzrFVL`|WFA9}M-i z*e~&3Vqsxn*M9!&B@4@$I2M+(o)`Z3-SRs0S}P07RTk}MY9;~c+c-8WV=C@wH~h|{ z&wpwc|D~bECU!~RE2nBX=4A;5R8~@6Jy;Ga%Ro*R%+0DW%W#v2fs-%@1T$y@YMN{u zgq49bH2QWTD4OTM%~^)$qHnuo-AVcMCq0uz?5W|inh@MNozVBiYp_T0*0I;1_D=)f zwSJy+EaeE|4VF)DEY5s-`~PTaxmIWOr%u7{eV4naYv-s3@<$dWi&ZoH986osrN8&T zATHl^rsM#6CxH8jw3w-pj6h^LXRF-mF%UzZ2D?`W&NqG<7M$GqIem*>rqpTu@}=WN zxh;$4tiIC3?rOq7)%xC+l=*`+wcRe7vC@I;eK3I_aojsiO;r81-JiSP-w}4!LbhXu z{bCG9APs#5FrjP>LD0>UW1~qNjoPm2&io5wSD==`RUwuOOb-m`(_m6^Qe#L3aa#kf zq5bv*|27?mJmJ%&3SQ&^LfjK7^VlG`R@0_z^;x*7KDQvOfLXGfC9_xo=X;@pDzvUz z_j~P)l4|hR=O$74_v_Yd#Cqzm%Fn3Juq6#tw_(-OK2HvDk3tC&v(8rh=P!m*uB2%a zTAh^|bBUvRqu5>JJS-TU5qkP$&|72Vz18HN1;t9*rQpQ{o+1)NAw%jk`*e&x7@ioR z7PUX4not0LBoR0~Zi>q;q))mT6aT_xfmmHtPP_{fCzPkHso~2gb@`4ODc{;y-*mMpEh+6mz zVvK86Sne{YW8F7m)^=@S=+V6V9)~V7^;}#zONPk9_D#B9s(tX#r|JBrdjszJy7DrP z4v5wHCDVBIZ4c!b;q-D-TC;qfb^n_ztqK2q;x)8lD%tE?l~~J84Q;zL8!sc}W|5PG zXR=bm!0u!Od+2dq;3w$WA^ruQWi95xm8OC*wzF=M3e!-fiXx{Oh@;djrQ&fhsZ#77 ze8x$l81`&jFZASl?{$t--BgB4K_tTGrK$W}E3vkSAmgg(V?y6WOUf1-jJM7QqqG`= z!AEagoQwOcQJLuBaS6rm7>77SPka0jQ9L@)!Y1baG>?h5pAC zr@2fW|00YHvY9aR{!lN9k!lMIatzbQkzu z9^;GIM(p}}iie&<_Z3iL8O5t??i6zvIW@l9dQM&`DN!ux3{iL-UtZ>#7dlZ#(6ihw z(McS>73AMD(K$E~nfTA=TSNLIs010|#)Kh@4GUAV>rn4GB+m4*4D>AT9ju_^1v~WQF+nc?8%bH8 zUt4sJzn<2lO44{T4lj{I$12Wm+RH1|AvY_v&JngJ_@X&=Yi@yBT-QYalLrB+9vj&7 zhWBz4)^0EX6@f=jp#5;NSh@Cxoo%IZLBNqGcYEqeh*zjpJS2wRJ-Ag-qhI2-S{kyK z`lV4y!br^kiB(5sD1wqp1$`$7%6ERfC)1+5MnS%#CDZb=UG9%nm$c`2BJC9$wti~G>K`0Ia?lH#91FVOSALV zt-i_DAO0CVQmqbDq++j?8C>*jZX7aI0Ktu0A#v5Xe{MBu#=TpU2XW(7CV$N*l2y2j zs`~`5#^(Tvu&0NC?rDT@SRTBlPR};_JA2NA^9&`tH0>WW@w|pBUC1!t2BzR#w_^-g z%a^R2f75azx=2v>UDxO$HS}5VPOkbNTX4SGGn$aB^7@hf0j$%MgJYUxS&JGFg7%+I zHYGDl#FXcD$ClHu{cJqr2Y_ZZva>X^?b1VvtV)nB#Q>nh6~Z&WvAOQ-d&#mnb%4yZ zDSJkbS~>^67_>~A@Z~b4Q-zBCxb%8&?04YDYNO7NJd!~cc)z-F1@N&>?SRg+a z{Rqtv(r1u79L>z01bI`?r)f`PU_l9k2}1S7-;|~n74t{tXJicD`jINelD?vma)x|z zPF-IaYU{0=KZlO~ro3tD-k5Zp!pd2^$Kl_;nQWoX*9$w8M9zlr>%XFp94_0FY)`8Yz|8Gsp|Ru zNN*{g8Tbu>*?EzHOw8WU3d23uk{hZ|9}$VQk=kN_{bS^R6qtI>`iRvWz4-CBI{B_Q zG-TmTO32&B;GyE}F&%FEQc*+hvJs+h!TQvFE{fNec+g&hvZoe3x$$SVw$ZwA-t?aI zq<~l9#Vu!#z~Y3uV0fYCb%>G5Kt&KKvp3Sb%O0chBmtm(BQ1m$=mYMYxq%XAOpAeIeV_briijXVyLorvPU({@ilb8-`l^ zn$1=5m#X`tYg!lTG;k1d22p1s1Z*ngg23a|M0+GNKUtHS94>r};|L1Qshpv{tleR7 zTycTAhFU)R?rW&Zu}U1ychfkj$2nO@#yxXPXHv*HWZK6!H!oi&8#!0d)mRjK$@7 zYE)5EA;B{7a#C4aEMpxb6GKE*s;71tbHJ5!axzd;ankG)QuKOoI(Ce>nkgy)KiP?e za{UtDRev$IB1deII}gjSIBK9%3J;|&kJ^mT$#XXbvTiA@Fx<4vXbBIoz?1=x(x5-e zksnvb)hu7RCv?Hh+(y`|@6U9esT@gzQUqWwx=BA(vSZZ|aY8@Mg8p6!gC6hzymDcr9iF{$@0@p@hgt*1=z2$zK54w&XT_5 zT+$67+D>l|49-yfdv>K!mXMlv@2Grf3Bz>hIF8`*9{k1SxpC56<2Sj($4kkCo^9CS zGGT&QJi9V6KPU6aWXWyxeT3x}c0<+AotfRsnF|6aQL+Mbyhu4v6)V3z^-gNtP|BGI zXF<7zt}V|`YphDw{`68`$+KgTHWdHwo68mc`-Uuwjor?Xe|&l?toGmk(|xTiSoyM>Z60Ecw^Y$fdE|a_fh=)-AB`s#(JdgGqvDj_o#6JyUa8z*;44jwCtP zL3M3HY`8FW^?as9rHl;z(f#rf*;sW~wuJyV{p~e_$elL(b!8>w^FsZW**X9brvZ_H z3)P7VVa_PU($IFIYG8m}sC_k?tCx6Sh4S7D{q`ZD ztz`P-9AsaeY`sJKutd7+%frz^`57-%56xt*se8AUHb#R~WY#Ct)k7$~z86=2_xp8e zB@Q3rngi8HTgb`t>tsGY^wA!T4m~7_&(t-`t^-nIL;S~@_L;jsI@pxQ7zgbJC+8>1 z4Uc2b4XIYwOqbnI4WR~U%&k`fGBZptk*kseRy$jq5Ng8xTXZdhOn|d{XkRC(Fv7hF z+)6p@{fKm2k3D{R?=kI=pV;I)HDA9`c5Jou-nKb8-P($7yfZ*s6;|cJ-5a;7-H@#N#Fo)(OB>CVa#9Go zgOj;6nRA5d!-*d!p?i0&Q_q>!mVnc$m)^|r17TT|(GudfLo# ziS?lko+)h#clgAmI2X|kY>5=s!RMmSIFj~5KtLBGp7 z^@pldyCkc-j>tDgOQS(yEr7DB7}s2h>3bLG3nh~ms_hs+9uZG}0Z`%A zr=XwGC*1}N_n`DIv^FR@AW*kq)D9{*ot5vVIdqGv#SVT`lJBn-M({1{1^KyX5x#J0 z-RPq(BiGA#)78AWJq+fyE@qzW7eF#iw3keML#mZM@CUarDKTfsK|!x@CY4>t)Hvvj zu|ree@?l1384%-s<;gR6g8z%2VrF!atQUzI$&EfY8 ze%kZg#4pxl3|^Z6^yZk(PZqunUnWqC-I(lAnban&uHs!@k6Ps;cTKg*0p;glzfBK6 zp`n2a!d1Z1;n({mb`8j%quczJiTZF@rT<49jbocMSB=6VD+>0fBgq0jo?~GV{j>@7 zp6n>$Dm!v$UedFHfD>=L&Fgd$#GmX#{>z}so)*<2*jJdTO5ZT_&;RvSt?oR&-zwGG zCt7JNuc+vRmhItIEJ#9?vVe_C+)P_nrevHak*+)nlpd`}fD&q3A!K^Z$Xz!x?c2e; zzXhtSx9m?vSOpR?GAn?H_MkWuaD@25QSzZHy$)M@P*fxi?2bVzAO@dN0vAr7_Qz1y zZ$}#Ny0jkYiCw@pG_{x2xbXQ~pFBHPHe`;0X<6>Zj;q+OLTjgQhlIY5p z3`EppfiF(;K+(41dJay3BDaUehT|KEOhM(>qhSSt){Wx(&nFDh%zt+Bpfbh%UiqYx zV)_$#vd^J+MzoE(l7{$3RkNEssc&nBN)mQ9=@L%XPo;2!^B-}x;@LfPKHNwB5!VS} zwYbqgx{};;n-uIfC2=YDl2ue{j+>f;yK~?m{)?ys+GaAX?xKCN$`V0oTO4*DH(`#e zQ$H4o@_r?y8&6w*yC7WLJV6i@^JsbkkHL^wen%u}uEfBS=ur(3oNwQ#E%a!AWr)*& z4`(c@Xk{Y!<*~h8K;cB81KAoY-*Ihaq2VC>^rWE19y%L^Y%KKh&&-kawQa~D3VKcw z+UZhXv3YWUvcWvII6YDUGDKJNgI$Jv?@*h`{yyOW-L4}sPmxwbCzH2;U!p#8z_Fj% zII3Imn5(Gfh2eZUGjQ;f(Nv_1wq370_@`wZ&M_c(b9{c>?b{0rt@&kr!srn zuI{aU9rwIrpv*!Lvd6~%US(#Y$&xBN4KxLWkE~5Gs4<-93WB+7ZqcFIXnFsqKB3ir zoJMAYSR59gUww0V{L+7J9=0Vd8WJZ*XISbVfK5s;pgAaVdHU?W0sn(?sqw*PLPMyS zF-uliZ7XQzn1n5>=GdBI1T;{oLZHGa_tQ~|ZAGRZA`vt7TYFVD4J6d~m1MVj*=!tyR`w7~ zetAA0I_mXK1g%+mf6!|c^gIb`YGJoNxo4J&I@yC1(Zo4xvL~kqNjH&&iaT1N_xpZ& z&99x*fw9v{*txL4jByjkZO^qcs}#Y)%NN+kHqzw~+h_i={$paMI1b#h zV`Q4G24El}fH||APCy1tnfA6T?t+0U=wvdj{!us(B30#sjtWb^hVbRCNH2IGRq$h8 zH6PW6@64~Uju#64)!MdJQYDR#()HY59_Sn>N5Wfx7Ui57qVzs8piy!&j;F4 zjj8bMhJ}WX7NapIjGe4*5~0v0ck0}!D@o5l@*w6Rr_d_xS^U~zS+RJ_`@uIV*0T*n zl06zG3bK`*gFkB7VTR4}xt%5K2G?!;7&|6CrKPGQz1jCwGWZqZ-LB;RG-bAU_LE}V znq;7drM^+3-KA(<9-v}nWSemvB?F)$S!i^n`S_Pkr|hfwe~h<48BGdaC-(42hn9U; zSMtJOsPnpjsq^-=WS{k4Kgw%4LY$Y@YE0)2xf4wx{jZUcv1#e^!0EsT_9I`?%dfBL z6Rd3u_D;8}J{@Ui)aXenMOTCKoYoSR+z)5z8pz}yLWAkBYT=auk@p==c)&2Ct#P9K zBeQJB8nLl5r9z2C$p}xHAY>d-3s&hsueGxsTe?IE9CH_vYOWC@zw~s_eg-lMTE8)C zMJz*JyNl1bCSVo1+L+${5}YOSSSfIM`p1QM^2Xjk99st>aKH7z{Sr{}47I5vr=E6d6)LU=fR8(-Btv>@&mjQUV?CL4R5v@KqdcrI9 zE=Ju{Ae3ugeC>=WW8{+!s}@c>1_fo-7e-7o+LHF;l2z}2Xkuo&A}2P}Y00fE^QqEc zy4nSJaRMef*~UC(?abCCm)1VHxR&);ZknJoJ$ zR=CZ{SDK|~HR2ozb0Kd%@6KX_4WqHl)9wS-KV{LKA@$2|j|(nAu!AAQqsUs@?PFxm zmQlV4M_MEApj0pMajwfBe%$3*-AS1c4s8YMX1k4+8mQcH{$sZ@j|W1&^tr*nhCCwp z+4>J5)vwLf3Qf$9=Obu>y;18<9p|{?iYDe7)U?xOS9Vo2Y3KtHNK$E4zmZVJV7o-0 zjJRFFU@A{}B{$N#c+-T+KXOxD2}N0-%#j6PntiRln$I2Ct+tsm`~70mb_1-Op}T`3 z(RHc$lPVj^&K#OGdQ(I*8B6aX2SAQ1byB)&M>oeA--WwR+xlFg6lDh)dtD#W;Rz956aE2 z-SD)4dkWEw`I2g zu!9sWz&a4y6rv>a5^;OIdVE5o{3m%bq+}*DjXB|RaNUn9pN8ze*{G=*Fy<3CV;`<} zX|}yK1e+-OU?Vkft$XEc42WJq<~TO@1KOu7O>v|#r8{3KhPuGlrjrjb`-uzTVXWtD zuRcc1Ou_Wl2lMyiE$lRLpQk&nxySORC%E`?a+%g_2Y>3~1;9>U;XnTM!M>Ynx>wbD znBlO~m>$*j>dHm=LafO7A5+Q#?ABVRJFSRFY9TqY`i^r&uBBCi>D*CHw~fy@&ou$k z9QBbILesK2FRwhQ)L^Tw{aUw}8>I-tjN5-45onyKso}X0Nz> zg~SW=+`a(Om?V9fwNHA|_dVf}x3xr)pm*+hEdsDBhL^KKpKB6&B%v(l*tx2&N_{gq z3sfnGr*vL*C(d9ZM{G=%+}H9GlW^~ppo2y$|5OUwcl5VShI|km(J=y{%SQAd638aE zUJfBsXld0QO#e+9j)cw$ds#p(Fx-&UgvrS_idRJ)c&~WIaLAmqM2N)dM#)YPGXON> zrfE0XU(`pr(tc5f+@`~8&I1fz>%1XHC>k_(Pe*hV^P9m_# z?MU%VdO;u8D(BwjzNcW z5`<+HPLp;4QZ~}e+U>XD*W;JRwP0h46S;MdovDwI{d!VvdKvvXNkPxAI&YRHgtlN` z3QZ0awjA`TAx*v=1Ohm!MmpBf%$`(n3f-Uh5JcKLU?8})7n3z zToP75R;TeT)MWcQ036lTQf1~*aFb0H*H?u|1JK(6S4mZOfM47&6rc-~&KWNJ%JKUr6euc5AXF90O*0O1*md%yPx=*G;1 z@>N&j1UO(J#kN^5QN{NbO2s}tWULAryL-LKd>_CUTsGoTlg9PyJHGL#1q6xBVElx3 z3G@uie>)lIALz&*Xw9X*FAzw7-?Xp1WZw}^x4s`hPTSUo%};%^JQI_ILL7qz;{6_7@?WGwx1AJ9jrIE7jccXHHa8kc-<%9wgSaJM6*Kf_V zuBy|pe0d?rR1y!o%wIkxK~Dn7xJRufK3xxUoUra@#b?*A6$G;CT@b|QSx-Hrq#S%y z#_oN1Z`z#$WUQjI&>g0fC$aulmsj+#DPAZL-(or&=Q4UKK@^y*1 zbyD3o&Px3UyRxv6axkU(Q|viG03YOURrnuTz)m(eP@&+-hxBpfX|hJAsfFQdK?Kvj zNBOdZH745QJ*Y%s=v=S4ksGzn>pEc_;9q$^@={;=MK1MladnAaM!Pg3ERXaH**TIc z*)ecSB8KrEwf#8Fws=#n5lYmL?l!U@?6{y>Hn~Pq+8^|`(eui?QeFI@kQkh&e3H<7 z{K_tjkJD|KAWVyJpKStmZ|+&U*St`_7Z1RGJKa7}5RV$dr!qp_2FfF_nOT|LAG;kf zh}sOwrP=3OLzamNQ^CxJ6#}=eyh%&#>_Qdv-2AwPkiF47uU;<;vUKyK-SC3^6V?00 zNU-`FXaymr*xlfjP#JA(JnFb_oY`vbU!TA9MK7ZXWpz)-BRQxXLdlTerG38E)Q#5= zg1YL@7MBXw_3pKoxt8q7BG1F2H5{fEUSD6@7X!!QTb48Xs)NZp#iwEzL%zhTHq1g# zKcn3MWgD>CY`d*0V=nWz-3w(-Q2~HJ_PL>6*^aB%JP)}XoYuHXB83Q^8j}nkXgFSq zUH+Ch64#*T@~zGS;%6rW9Zajqc)yxhXMfyqK01StsC*t@`m0o%YHc3)0IoNizc;)T zu5NiVQ4>`v7kSgFJ4y#xQ0#8&F=t`IL<6w^+*3kxhrMi|xG`WgQ4wPBHzyaruY3!J zvrMF_Df_68emFes6%V!cjFnCZDQdlPeWJ`;xP~Cw<^>*13w7i!fj3tNz(H*d=bdC! zYu7-z$CQ?~kKrl5_wRj}0efh2)pDoQT?c`)| z3+lu*MIV*ZxPw=g?xlv+uWW#iX)WINtZsX-P>&56;`Ur=uT*_uKolUfv9WahV`lyj zjg%TegsaWkvg0NYT8?ZeYw&NP-99^pvMZQ3Z+$>t0o#5PCkjX8-RV}?8n5BRJ(fvDb061aP8pafbjN{E`v+tvYY+8#?E3}V}Q_|*)Kkw*94KY z(#lp`WYRWCZo#2**kgJFg||LU<{pm+PNvM=Gl(*3GRN-eK^yZp0O|)ncw`P-OBjqU zpX8G*9o!+%89oMRbjACvp>Z{H9vQif6O3XbA;G^+LIox?-=k2Em1@?GM8~%KMw6JU zd7V1l8()nu-3o3)+OkYzwtWomk~RA*TSyqk9Zo6Fla$?-^SO(K`Lv_*=D(+%EL5X@ zV5>5)Ar+4m^_i=;;9ili=*(|*zbwK1#*O|xw%83tTHDg=GN5hVIAcbq1G7}aNjXQR zF5qUUr?l*ZUZn2Ai>VhGG7@`LaS>iIR%MxK$}}?BQ(iCN)k~?6LytU)Rv4y>&nfPa z#~c_}WWTsi#gDf`tsj;7d-ZxD2OCSb$WqNULGjwgjR2+#y*xXpzS08=kZqLVnA)12 zI4&|>^O(0XY1+iLYQKcAGb{FgR-VnJV8JvTFBE8ncyQfO1e^4)a!Wa!m>QBesA;K&Xy zB_}BF4;FBnBXf0N(!(60I`;}=yFHd49v-vOwV>qS)$Ehu&GoglReGoVe7DB@_fU7j z#S(?J^W2Rb8Gt2p7E?@SV6=&v7;f_3=Jf1JmtSRs_4A|my#Y}G@q`DkEV_H z8mD52@p5yQVnK?DPmI?|yBNKC;qraVX?HGq@6=f)`p)0Jg*E!mT*b6$LC-5McB@}$=_mx_q1;M~xPw_gNTlrOIn!H9j1iFCqO zD+w^-`0AEL%%19^#NY zNj&LiTRb-NCE!wi!QSFp+^F=G^n z4`Dcg)8AW;O(Ko#FyMqFFhW*9*iLhx9=JgG&7>SHy3luXW8`1WWz;Q(&xiTK^EaRQ z&iRC(#i%EGrkffCM4cxlHA|$|5yOo`?q$U`*iDm@Ta=*A(Si=MGDyenSoz0ZJL4ub z%uOg8yF09Fe=z;ApWCV=wRNSSB_DRS^|~MRiFKsdR8wEZYkQ@I$sX#ZmWlQ##;&XWx)GIr zyLbCkNnDu@)lF;=ZHb5cymnW=ZR)SPDbLOo@Mc-KNL2X})e3&4(%8|$Y} z;PG=!$n~Q4d}jrh`$Ind#ju~D*NTf?*==a-Kg|v68h7s>5xm;K@7XJM8~OD$Dxk7= zq9<><8@l_4H@YZwedOg4$|l`Tz5epcAvQJDna|1ekow2YL->3(` zASIY#KV37~>OI+CO3(A2dvybhHxp%Chc0 zaNSNZ!`q;{_}z;8aS5Dh{8N2)9;e>85T~gCWi)H}ybFcbpSxUEwV(>I$FVFNj3fXa z=h-oH(jU*g`BOLC(N*bCAkhTw+!d$OTh<~IPxt6kBwZ(M7k{@jK4=LWSN{C{^Th=u zq?nrFCKTG6VO_)nXi&$_j*JGg7q`BuMWMVm&gGVz^V=9_H2I&0<7!?LmFoy&QFx1E z&YKVa&h8kVt=>MdnO#(uJ0!?>B4jX@O_tn;8CjJrvJ%*H{|q-;P@0bdOKP_!0mX@$ zP}u-os~Fbx0|l{+sBBLwK$*(XJPXvKMQy99q1gti9eBJn!Dee~s~W#BUkiKI5^)_e zTLJ{OF3Ago+=Vr6>zD1$KWt+S?_q8^pzSSHPZYG2U$zk2%Yp-);Esj`PJD}L|OZpen&PTOa#T#NJG)rnjSWG<3mJgcbwg6vS(4^WBz-sy2 ziQLVsF*M!*RwgH3X*qar-`3oDG@zwxx@atCJY2o|Lxkm5xtr`!C7uq$`z6Sw44%@3 z)GHO&)|`K1y%Cb0_U4ry($23D#=7d4NBc-VIV3(HqLfqIoVvzrp=H4$w3a9D zy&gQ+V8S_2F0bH58RgF7wOT@Jd>k{2za1n2;&$1YGzpq;ZXzF(VVN*7 zVwnFKUT9W!V}4a5<0L;VwbGjUY+v_-e|&n;V+SVUaMSXOo^QKcV=^pc+sVpbWI&*$&n0B$ zFY<%=B>(XnxTzdDzvvHak~7V(GY4|fEMk2_be~YyYJ07t3^V1VXxZQve>m}>*PyU@ z>Tyiu?SlkIDY|TTx^zqGu0K0&*{(Jc;3z145LX0DC5DzYOKJ$&$(1x24K6F!o@H8Z zFKpacm?-p?OX(tS3u?K(pL*4}cw|23@hYsr!5Bn*_#VAC-~Rv+VD)vieHBnI6Agy~ zdAFNn=$?IwjrxtZ+T{3{#NGElw&*^|a|?M$lSmfI$y5h2Uvr<1I-rhl|BemG^ ztw+!y=7@uIMCXrAd^-LBU1lfCv!=@va?@D z)Oum-d_ty*C7izX494pdRIqqL*1ZFw|d&f z5ORPO#WQV7C!Y~SHtx9>0<-Taqe#H`3M~Xdl(;yHnVQM$y)?Y_M^GdLF-doPr|H|a z*Cfi2!?ef5K2qkS?eAGzi|nz!_zx{0Uh|oBd1gQ|-RAILwDpmwuvFlJ4sv8Ck2|!a z#*jEy!EWpnhZV$!5c%4?cER4hye%Zygd!VQ%G^^ZcO*u>#$-_RRS zBMai(ELRr0l~!ikqG`EqVk7{$6vfZwKajalJ}Mn~>+!fnsD7`h&6;$|N3eV^4pS@f zgs*;ESdC)1`l{`gBd#;w=$_#{9vT+SvV@6;o^%jH~|Rls6N=>dNlc!1@X-GMQS^v+B+i$m7$ zv#T|Gebu+!H=5@v2Hnw2IKiZojasE?(`ab{XHy2RbqL#U@a!L(_=59RSg|i;S z#cYCKS{#n{K}}48bFGBZ&lO1x*&5S|M9Y_(!}Ii4(`a>`-1U!spOA@z$|qc8z?S-71y2`s(pKD|`GT@ee@Zq|n8+ zbs^pxs*CfsQW(fod8l8e6m=Wo8RYCec#mmjyFFsFHBxa|`Q8vJ*9!5!I!zT@zj{-= zBuD5RD?rgo)DcTq4u6z_?-969)%9dIbFib2yBr>Y(xGlkiGlk+61&5Ba>>XKw-a!b7C()`USXI}@( z33S+N4%Gw88E22jQI<6mY1g2Z*e*ctWOHgbd!&?EUTe8OTXOO}x{H$)alIu_M|$D3 z-fOy_W+z5~0-oXn#O)GF;auxi(iIVA&Sftv_lZ-g-gh4Fz0yfht0bG(Ey~b8p6~YG zKbhNMoBG=3%=!nQb#h9q(SWQ>+q-MPTS77|;x@=sDYMz;#xq4r11&QPIdh~Kk{O6M^44T~2%(tzrjPeyU}4aE*q{0G6=aGVTTCHiHs=Q~OA z=K-NSp)#S5C4bvH5{$@j)O=JXPuZ&CR^7y%{xm)?;OeB#P>QxUJ8-d5$OcpDTKeeTMo`#+7wkz*}9}hDv79C-aggC*ahKL^7YP%NY;YtgabJ( z;@7><1J}7g#ukP->;{#^d<;02Z{d~RjYU=6-zt}0S^zC|%$Y%c4^}-1cz=?4@;+$w z$F6mY?H>-CpS>(!z7sb<9tP=9olW1}Ex7Z{OugMM;tB_Rbz5S8TrYU; z=b!#|#?Kv?=(`m-(o31o$yQp|MCJ>Qar@zz?*jCFRQnb6aFve;^@PzMeVFWi!-|B3oJs2k!)Y7_G(m~LDrz7ri zu&$*$yg}hj9xz>@2v*NrsR&x9d_0*LM>S1^{Ma~m9RqIWZ2haz3RZCUrynf*l%s1E zs#)g$#BK^FG!*LS@ojSJVpN7w*>3lQYglQR5rntXNg+&p;-b$*(N78C!`FOV*8v(ZwMO1@1M3yeof# zzhIq&78X2Jv+8ih?tjB`QrWFn%f4mOn;k0fZS|)Eg)V31EReqh#+e#fC$=G2|K4-C+nh-4X!2^VO8#+2>c$M#?Zm0u9UmN$z3Mj+YYk8zi zMi-Cz805bFmNqsomU`R1sR0IE2>NmLI%4TLa$q2b%*A6j7jNpa!R;~Orl)KV1h0m8 z|F}9cR%$8<>3GY$T6V10;!ooi0J4TkJ<8t1MBt_UIwB-2F~|`h!D+SXrjkY)_V*qi z2NU_$cELKkwelONoW2iD8jpR?Dd!3zJ85l5AKdh%*6)XVvww_!JY0nW9dK1cClxj{;5zq2Wp7g4a`~&jAFy~-$Z3TU{TovlNEFS}{Y4F^ zz*rKGk$sw6Qd3jB^u$aa|HMqKd4tsoS{`R~$4yIhLO)gccV-!IQCRWXYLx9Uo3Gd;e|3Fu)tZ;ZcwHaeVOsU^lTJSo}q ze+W;gkdo$8lpJKLsTg;iRIhjDjlBO%Z85AVm!^qRajL^{)`ofFSQbN z_FkBp-B|#2Qo-*cM60_oq>2kzoa)mD&yh&SMQe`O=tT~Wx8h^0na_#@`HGo$uke*J zfZpxIf}!-|2grD}3L(X1L`?p{^r3|9fveilM`qgWR@l9Y0{1N-G}y|}mlXA_l0)Uo zM(>GxxRV`q7nHXu#s)8+`($ir_>%DQMMjg>72Q;Mv(N3s>Agd&4IQ3rOZT4s-uQNi zzm5WhTGlW9^2RWL*IAi9`W1%EOwXk{(Jd}9UCe#0c^!u^AEuSaEt4~z4?GM@w$r5& zt7~8Cp%p;NcT9#nk1p`9O49EV~BAt#8adY`K zGUJ3<3GQ1qLm~dw-FUrkM_Y+2^~-{#r<=)9BiY;frZ%CbkC^%&+y>SUS*FUwi%nsN z$$5_V+~x6xCXlHz42*O`Nc9Q55mHF$o7e2fnq`9vT|vqEK5Hw;tl0@K9TaGjo5t*! z2+p4xS#-UPw=p(b{sBbhZf7tTDl;HHave@8URrUp0s!M@?(4Iwg|&N0P9Y}+9D8hi zoVo!f%O3t67lPay=0F}ld&N(>AXlQbd4V1Q9O3)J4Uf(vufwC2fNNRRUXa;FDMB~@ zfs2ynkB@2I@aCCKXXGgdWI84H6!k|WZC+!pwSyLPnL&KFQX#2;$fKc3Gvv-Xv-1n& zdB%CSm#zTuDpn7>Mu!aEJ)kEP(cQ4pV)WL6KRpa%feH6j46Xtaeoz1ReFG7ZJb&8? zUHna6x3ZE%)%*>hTle(`h89D{=zSx%SkcY5w$EI3ux71$#`yaPXM+oYIvne>K#|K= zD86Ao1FN!1r>v>{C=XC9nlIF_L@ou`8Jh+}1pMe=7MiGFl_rA+5|Fn`Nlx$-#oVEN=V`@Re)p9^8(^T3TqVPoViVPSk^ zAFPA1_$pPrR!ywm-c#%A_2Ue3iXAC20RReO#Srewehlz`<> z^~?0p;HM?Goa{UL+rnQ2?_|5;cny|BaoFm|eh1_4F8CL;Df#^%A-4|GZwij&hHO6o zzwZM)Pr;tm)9X%2_D$1j=?liax}au3 zgK*^2G8^SAN|VY59h>rM#9rZ<*KKZL%kf&op*gR%#)MzYwd}G?@PxI3d`NcV!~JlDHWE){= zvO3C8(}YHbfA@F%z{dP`i?a!NCe8PJv?tXSqtERJbHh~qTJLSAw@PWzRae2aQX`Gg z31DVn3fa_&-Q*3QKHoUu{8+*LsfYy!X(~Tskz+ZW9&8M`RO=>Py{~_{4o0aCop4YW z_Z?H6H~bthv_13L$z!!u1nOD;^G6gYcVa`h8>c8!*_J9Dy0s$OFB8y96qGZmWnDAD<4<;Ysp93a#`aJRf9x~MpD9v8lDr<_HV27k6rb73dF1=0K z`1$B;4ELPGgt=zSMM-nG0-gw5X_cYW?`5C|(qny+@3YyM2KC=It$$lPV-B0p7^=dY z^8o)|R^p?K0~7~DYbHAVvACFpBKOrY|D77G7jr7+`y?_~h%thn=LXR6uwQ}6#zq3V z9UXRtA{cRZ;~zb^E6G?pjb7jm^8E)gk&_HNY4wCsViW@)PgAgz%ERBA_w;@T@4v`+ z7XBP>Zcq$JQu5)!p=C(XT+2>2HZ^|0z3*E8?R5i9q z%VGh1>J!vhUXNqKUiAK4Tj;aV!VSH&y&b&Si5CN0a9$#av_3k2Zz5LF?EfI5Lx?}$ zKq7`-xm*OhrsgiCgs=YoDCSgx-Alsy@N-tsyx+IQPgNwlvBK+x@uGk$|C?O8K3rR@ zUl_kspCn0p?nR5WhKw>C;U?A&X^XE`>MJoFR;|MttjG4O8;{2K%R#=yTZ@NW$KcQL^GuR2W= zo*cD*{QYF62U^I?&{Q*=qUO|z$5>t*B7{d6huH`C4k<`elg%BTsBL&K`5vs!_wub(*@~5xD{s(lKp4$Kb literal 0 HcmV?d00001 diff --git a/e2e/integration/tests/__screenshots__/visual.spec.ts/rhs-unreads.png b/e2e/integration/tests/__screenshots__/visual.spec.ts/rhs-unreads.png new file mode 100644 index 0000000000000000000000000000000000000000..1428d95ad175e7111a2faca89ecf37c30c027bbf GIT binary patch literal 18813 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 literal 0 HcmV?d00001 diff --git a/e2e/integration/tests/__screenshots__/visual.spec.ts/tooltip-issue.png b/e2e/integration/tests/__screenshots__/visual.spec.ts/tooltip-issue.png new file mode 100644 index 0000000000000000000000000000000000000000..c1ff775c0a16fb0ec59f6e7bfc1f597d037127c3 GIT binary patch literal 7756 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 MeUTTh02uoH56DFX1poj5 literal 0 HcmV?d00001 diff --git a/e2e/integration/tests/__screenshots__/visual.spec.ts/webhook-merged-pull.png b/e2e/integration/tests/__screenshots__/visual.spec.ts/webhook-merged-pull.png new file mode 100644 index 0000000000000000000000000000000000000000..0f1a6c8cd6794998ccbcc2b0e93f45d40fcc9f2e GIT binary patch literal 11640 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
i3UYeqU7{%}2OiO=;^&N!c%|9;pWcK}I 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 literal 0 HcmV?d00001 diff --git a/e2e/integration/tests/__screenshots__/visual.spec.ts/webhook-new-pull.png b/e2e/integration/tests/__screenshots__/visual.spec.ts/webhook-new-pull.png new file mode 100644 index 0000000000000000000000000000000000000000..e7bb9eb25f9855ab2fd3883bef5a417122eb5c76 GIT binary patch literal 24609 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 literal 0 HcmV?d00001 diff --git a/e2e/integration/tests/__screenshots__/visual.spec.ts/webhook-push.png b/e2e/integration/tests/__screenshots__/visual.spec.ts/webhook-push.png new file mode 100644 index 0000000000000000000000000000000000000000..af0192386a2286018fe3b0f8527f1c7a43de9a17 GIT binary patch literal 7971 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 WT-~`Jee0iF5ENuoq{}2f2K_%F1crD3 literal 0 HcmV?d00001 diff --git a/e2e/integration/tests/connect.spec.ts b/e2e/integration/tests/connect.spec.ts new file mode 100644 index 000000000..1322783a3 --- /dev/null +++ b/e2e/integration/tests/connect.spec.ts @@ -0,0 +1,140 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import type {Page} from '@playwright/test'; + +import {defaultScopes} from '../fake-github/server'; +import {expect, test} from '../support/fixture'; +import {fakeGitHub} from '../support/fake_github'; +import {hasKey, readUserInfo} from '../support/kv'; +import {botDMPosts, openChannel, postWithText, runSlashCommand, waitForPost} from '../support/ui'; + +const completePath = /\/plugins\/github\/oauth\/complete/; + +// `/github connect` answers with a link that opens the OAuth flow in a new tab, which closes itself on success. +async function startConnect(page: Page, channelsPage: Parameters[0]) { + await runSlashCommand(channelsPage, '/github connect'); + const complete = page.context().waitForEvent('response', {predicate: (r) => completePath.test(r.url()), timeout: 30_000}); + const [popup] = await Promise.all([ + page.waitForEvent('popup'), + postWithText(page, 'Click here to link your GitHub account').getByRole('link', {name: 'Click here to link your GitHub account'}).click(), + ]); + return {popup, complete}; +} + +test.describe('OAuth connect', () => { + test('connects through GitHub\'s consent page and stores the classic token', async ({pw, github}) => { + await fakeGitHub.signIn(github.login); + const {channelsPage, page} = await openChannel(pw, github.user, github.team.name); + + const {popup, complete} = await startConnect(page, channelsPage); + await expect(popup.getByRole('heading', {name: 'Authorize Mattermost E2E'})).toBeVisible(); + await expect(popup.getByRole('listitem')).toHaveText(defaultScopes.slice().sort()); + + await popup.getByRole('button', {name: 'Authorize Mattermost E2E'}).click(); + expect((await complete).status()).toBe(200); + + const stored = readUserInfo(github.user.id); + expect(stored).toMatchObject({GitHubUsername: github.login, AllowedPrivateRepos: false, MM34646ResetTokenDone: true, Token: {token_type: 'bearer'}}); + expect(stored!.Token.access_token).toMatch(/^gho_/); + + await waitForPost(() => botDMPosts(github.adminClient, github.user.id), (p) => (p.type as string) === 'custom_git_welcome', 'welcome DM'); + expect(await fakeGitHub.calls()).toEqual(expect.arrayContaining(['GET /login/oauth/authorize', 'POST /login/oauth/access_token', 'GET /api/user'])); + }); + + test('skips the consent page when the app was already authorized for these scopes', async ({pw, github}) => { + await fakeGitHub.signIn(github.login); + await fakeGitHub.grant(github.login, defaultScopes); + const {channelsPage, page} = await openChannel(pw, github.user, github.team.name); + + const {complete} = await startConnect(page, channelsPage); + + expect((await complete).status()).toBe(200); + expect(readUserInfo(github.user.id)?.GitHubUsername).toBe(github.login); + expect(await fakeGitHub.calls()).not.toContain('POST /login/oauth/authorize'); + }); + + // GitHub grants the union of every scope the user ever approved for the app, so an old `repo` grant comes back. + test('rejects a token that carries private repo scope from an earlier authorization', async ({pw, github}) => { + await fakeGitHub.signIn(github.login); + await fakeGitHub.grant(github.login, ['repo']); + const {channelsPage, page} = await openChannel(pw, github.user, github.team.name); + + const {popup, complete} = await startConnect(page, channelsPage); + await popup.getByRole('button', {name: 'Authorize Mattermost E2E'}).click(); + const response = await complete; + + expect(response.status()).toBe(403); + expect(await response.text()).toContain('OAuth token scope does not match the requested permissions'); + expect(readUserInfo(github.user.id)).toBeUndefined(); + await waitForPost(() => botDMPosts(github.adminClient, github.user.id), (p) => JSON.stringify(p.props).includes('OAuth token scope does not match'), 'scope mismatch DM'); + }); + + test('rejects a callback whose state was never issued', async ({pw, github}) => { + const {page} = await openChannel(pw, github.user, github.team.name); + + const response = await page.goto('/plugins/github/oauth/complete?code=0123456789abcdef0123&state=forgedstate0000'); + + expect(response?.status()).toBe(400); + expect(await response?.text()).toContain('invalid state token'); + expect(await fakeGitHub.calls()).not.toContain('POST /login/oauth/access_token'); + }); + + // GitHub reports token-exchange errors with HTTP 200 and an error field. + test('fails cleanly when GitHub rejects an expired authorization code', async ({pw, github}) => { + await fakeGitHub.signIn(github.login); + await fakeGitHub.setOAuthApp({codeTTLms: 0}); + const {channelsPage, page} = await openChannel(pw, github.user, github.team.name); + + const {popup, complete} = await startConnect(page, channelsPage); + await popup.getByRole('button', {name: 'Authorize Mattermost E2E'}).click(); + const response = await complete; + + expect(response.status()).toBe(500); + expect(await response.text()).toContain('failed to exchange oauth code into token'); + expect(readUserInfo(github.user.id)).toBeUndefined(); + const exchange = (await fakeGitHub.requests()).filter((r) => r.path === '/login/oauth/access_token'); + expect(exchange.length).toBeGreaterThan(0); + expect(exchange.every((r) => r.status === 200)).toBe(true); + }); + + test('/github disconnect removes the stored token', async ({pw, github}) => { + await github.connect(); + const {channelsPage, page} = await openChannel(pw, github.user, github.team.name); + + await runSlashCommand(channelsPage, '/github disconnect'); + + await expect(postWithText(page, 'Disconnected your GitHub account.')).toBeVisible(); + expect(hasKey(`${github.user.id}_githubtoken`)).toBe(false); + expect(hasKey(`${github.login}_githubusername`)).toBe(false); + }); +}); + +test.describe('auth failures', () => { + test('a revoked token disconnects the user and DMs them to reconnect', async ({pw, github}) => { + const {token} = await github.connect(); + await fakeGitHub.revoke(token.value); + const {channelsPage} = await openChannel(pw, github.user, github.team.name); + + await runSlashCommand(channelsPage, '/github me'); + + await waitForPost(() => botDMPosts(github.adminClient, github.user.id), (p) => (p.type as string) === 'custom_git_revoked_token', 'revoked-token DM'); + expect(hasKey(`${github.user.id}_githubtoken`)).toBe(false); + expect((await fakeGitHub.requests()).some((r) => r.path.startsWith('/api/') && r.status === 401)).toBe(true); + }); + + test('SAML enforcement keeps the connection and DMs a re-authorize link once', async ({pw, github}) => { + await fakeGitHub.createOrg('acme-sso', {members: [github.login], ssoEnforced: true}); + await fakeGitHub.createRepo('acme-sso/widgets'); + await github.connect(); + const {channelsPage} = await openChannel(pw, github.user, github.team.name); + + await runSlashCommand(channelsPage, '/github subscriptions add acme-sso/widgets'); + await runSlashCommand(channelsPage, '/github subscriptions add acme-sso/widgets'); + + const dm = await waitForPost(() => botDMPosts(github.adminClient, github.user.id), (p) => p.message.includes('SAML SSO session expired'), 'SAML DM'); + expect(dm.message).toContain('https://github.com/orgs/acme-sso/sso?authorization_request='); + await expect.poll(async () => (await botDMPosts(github.adminClient, github.user.id)).filter((p) => p.message.includes('SAML SSO session expired')).length).toBe(1); + expect(readUserInfo(github.user.id)?.GitHubUsername).toBe(github.login); + }); +}); diff --git a/e2e/integration/tests/contract.spec.ts b/e2e/integration/tests/contract.spec.ts new file mode 100644 index 000000000..631cd95ca --- /dev/null +++ b/e2e/integration/tests/contract.spec.ts @@ -0,0 +1,206 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {expect, test} from '@playwright/test'; + +import {skeleton, validateGraphQLQuery, validateResponse, validateWebhook, webhookSkeleton} from '../fake-github/contract/validator'; +import {type FakeGitHub, startFakeGitHub} from '../fake-github/server'; +import {State, normalizeScopes, parseSearch} from '../fake-github/state'; +import {buildPayload, signatures} from '../fake-github/webhooks'; + +function seededState() { + const state = new State(); + state.addUser('alice'); + state.addOrg('acme', [], ['alice']); + state.addItem(state.addRepo('acme/widgets'), {kind: 'issue', title: 'Broken widget', author: 'alice'}); + return state; +} + +// These check the fake against GitHub's published schemas without a Mattermost server. +const port = 8193; +const base = `http://localhost:${port}`; +const app = {clientId: 'Iv1.contract', clientSecret: 'contract-secret', callbackURL: 'http://localhost:1/plugins/github/oauth/complete', name: 'Contract'}; + +test.describe('contract', () => { + let fake: FakeGitHub; + let token: string; + + const control = async (path: string, body: unknown) => (await fetch(`${base}/__control${path}`, {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body)})).json(); + const api = (path: string, init: RequestInit = {}) => fetch(`${base}/api${path}`, {...init, headers: {Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...init.headers}}); + + test.beforeAll(async () => { + fake = await startFakeGitHub({port, publicURL: base, app}); + }); + test.afterAll(() => fake.close()); + + test.beforeEach(async () => { + await control('/reset', {}); + await control('/users', {login: 'alice'}); + await control('/users', {login: 'bob'}); + await control('/orgs', {login: 'acme', members: ['alice', 'bob'], admins: ['alice']}); + await control('/teams', {org: 'acme', slug: 'core', members: ['bob']}); + await control('/repos', {fullName: 'acme/widgets', milestones: ['v1']}); + await control('/items', {repo: 'acme/widgets', kind: 'pull', title: 'Add widget', author: 'alice', reviewers: ['bob'], teamReviewers: ['core'], labels: ['bug']}); + await control('/items', {repo: 'acme/widgets', kind: 'issue', title: 'Broken widget', author: 'bob', assignees: ['alice'], milestone: 1}); + await control('/threads', {login: 'alice', repo: 'acme/widgets', number: 1, reason: 'review_requested'}); + token = (await control('/tokens', {login: 'alice'})).value; + }); + + test.afterEach(async () => { + await fetch(`${base}/__control/settle`, {method: 'POST'}); + expect(await (await fetch(`${base}/__control/contract-violations`)).json()).toEqual([]); + }); + + test('every REST route the plugin uses answers with a schema-valid body', async () => { + for (const path of ['/user', '/users/bob', '/user/orgs', '/user/repos', '/users/alice/repos', '/orgs/acme', '/orgs/acme/repos', '/orgs/acme/hooks', + '/repos/acme/widgets', '/repos/acme/widgets/issues/2', '/repos/acme/widgets/labels', '/repos/acme/widgets/assignees', '/repos/acme/widgets/milestones', + '/repos/acme/widgets/pulls/1', '/repos/acme/widgets/pulls/1/reviews', '/search/issues?q=is%3Apr+review-requested%3Abob', '/notifications']) { + expect((await api(path)).status, path).toBe(200); + } + expect((await api('/orgs/acme/members/bob')).status).toBe(204); + expect((await api('/repos/acme/widgets/issues', {method: 'POST', body: JSON.stringify({title: 'New', labels: ['bug']})})).status).toBe(201); + expect((await api('/repos/acme/widgets/issues/1/comments', {method: 'POST', body: JSON.stringify({body: 'hi'})})).status).toBe(201); + }); + + test('answers like GitHub for bad credentials, missing scopes and SAML', async () => { + const bad = await fetch(`${base}/api/user`, {headers: {Authorization: 'Bearer gho_nope'}}); + expect(bad.status).toBe(401); + expect(await bad.json()).toMatchObject({message: 'Bad credentials'}); + await control('/revoke', {token}); + expect((await api('/graphql', {method: 'POST', body: JSON.stringify({query: '{ search(type: ISSUE, query: "x", first: 1) { issueCount } }'})})).status).toBe(401); + token = (await control('/tokens', {login: 'alice'})).value; + + expect((await api('/repos/acme/widgets/hooks')).status).toBe(404); + await control('/users', {login: 'dave'}); + const narrow = (await control('/tokens', {login: 'dave', scopes: ['public_repo']})).value; + expect((await fetch(`${base}/api/user/orgs`, {headers: {Authorization: `Bearer ${narrow}`}})).status).toBe(403); + expect((await api('/user')).headers.get('x-oauth-scopes')).toBe('admin:org_hook, notifications, public_repo, read:org'); + + await control('/sso', {org: 'acme', enforced: true}); + const saml = await api('/repos/acme/widgets'); + expect(saml.status).toBe(403); + expect(saml.headers.get('x-github-sso')).toMatch(/^required; url=https:\/\/github\.com\/orgs\/acme\/sso\?authorization_request=/); + const search = await api('/search/issues?q=author%3Aalice'); + expect((await search.json()).total_count).toBe(0); + expect(search.headers.get('x-github-sso')).toMatch(/^partial-results; organizations=\d+$/); + + await control('/users', {login: 'carol'}); + const outsider = (await control('/tokens', {login: 'carol'})).value; + expect((await fetch(`${base}/api/repos/acme/widgets`, {headers: {Authorization: `Bearer ${outsider}`}})).status).toBe(200); + }); + + test('commenting through the API delivers issue_comment to the repository hook', async () => { + await control('/hooks', {owner: 'acme', repo: 'widgets', url: `${base}/`, secret: 's'}); + expect((await api('/repos/acme/widgets/issues/2/comments', {method: 'POST', body: JSON.stringify({body: 'on it'})})).status).toBe(201); + await fetch(`${base}/__control/settle`, {method: 'POST'}); + const delivered = (await (await fetch(`${base}/__control/requests`)).json()).filter((r: {method: string; path: string; body?: {comment?: {body: string}}}) => r.method === 'POST' && r.path === '/'); + expect(delivered.map((r: {body?: {comment?: {body: string}}}) => r.body?.comment?.body)).toEqual(['on it']); + }); + + test('paginates with Link headers', async () => { + for (let i = 0; i < 4; i++) { + await control('/items', {repo: 'acme/widgets', kind: 'issue', title: `Bulk ${i}`, author: 'alice'}); + } + const first = await api('/search/issues?q=repo%3Aacme%2Fwidgets&per_page=2'); + expect(first.headers.get('link')).toContain('rel="next"'); + const last = await api('/search/issues?q=repo%3Aacme%2Fwidgets&per_page=2&page=3'); + expect(last.headers.get('link')).not.toContain('rel="next"'); + expect((await last.json()).items).toHaveLength(2); + }); + + test('the plugin\'s sidebar and digest queries validate and execute against GitHub\'s schema', async () => { + const query = `query($q: String!, $c: String) { pullRequest: search(first: 100, after: $c, query: $q, type: ISSUE) { issueCount pageInfo { hasNextPage endCursor } + nodes { ... on PullRequest { body number authorAssociation createdAt updatedAt repository { name nameWithOwner url owner { login } } state title author { login } url + labels(first: 100) { nodes { name color } } milestone { title } additions deletions changedFiles + reviewRequests(first: 100) { nodes { requestedReviewer { __typename ... on User { login } ... on Team { slug organization { login } } } } } } + ... on Issue { number title state url milestone { title } } } } }`; + expect(validateGraphQLQuery(query).errors).toEqual([]); + + const res = await api('/graphql', {method: 'POST', body: JSON.stringify({query, variables: {q: 'review-requested:bob is:pr is:OPEN archived:false org:acme'}})}); + const {data} = await res.json(); + expect(data.pullRequest.issueCount).toBe(1); + expect(data.pullRequest.nodes[0].reviewRequests.nodes.map((n: any) => n.requestedReviewer)).toEqual([ + {__typename: 'User', login: 'bob'}, + {__typename: 'Team', slug: 'core', organization: {login: 'acme'}}, + ]); + }); + + test('GraphQL rejects unknown fields and missing pagination bounds like GitHub', async () => { + expect(validateGraphQLQuery('{ search(first: 1, query: "x", type: ISSUE) { nodes { ... on Issue { nope } } } }').errors).toHaveLength(1); + const res = await api('/graphql', {method: 'POST', body: JSON.stringify({query: '{ search(query: "x", type: ISSUE) { issueCount } }'})}); + expect(res.status).toBe(200); + expect((await res.json()).errors[0].message).toContain('You must provide a `first` or `last` value'); + }); + + test('OAuth token exchange errors come back as HTTP 200, as form or JSON by Accept header', async () => { + const form = await fetch(`${base}/login/oauth/access_token`, {method: 'POST', body: new URLSearchParams({client_id: app.clientId, client_secret: app.clientSecret, code: 'nope'})}); + expect(form.status).toBe(200); + expect(Object.fromEntries(new URLSearchParams(await form.text()))).toMatchObject({error: 'bad_verification_code'}); + + const json = await fetch(`${base}/login/oauth/access_token`, {method: 'POST', headers: {Accept: 'application/json'}, body: new URLSearchParams({client_id: app.clientId, client_secret: 'wrong', code: 'nope'})}); + expect(await json.json()).toMatchObject({error: 'incorrect_client_credentials'}); + }); + + test('webhook payloads match GitHub\'s event schemas and carry both signatures', async () => { + const state = fake.state; + for (const request of [ + {event: 'pull_request', action: 'opened', number: 1}, + {event: 'pull_request', action: 'review_requested', number: 1, requestedReviewer: 'bob'}, + {event: 'issues', action: 'opened', number: 2}, + {event: 'issue_comment', action: 'created', number: 2}, + {event: 'push'}, + ]) { + const payload = buildPayload(state, {repo: 'acme/widgets', sender: 'alice', ...request}); + expect(validateWebhook(request.event, payload), `${request.event}.${request.action}`).toEqual([]); + } + expect(signatures('secret', '{}')).toEqual({ + 'X-Hub-Signature': expect.stringMatching(/^sha1=[0-9a-f]{40}$/), + 'X-Hub-Signature-256': expect.stringMatching(/^sha256=[0-9a-f]{64}$/), + }); + }); + + test('creating a hook sends GitHub\'s ping delivery', async () => { + const received: string[] = []; + const {createServer} = await import('node:http'); + const sink = createServer((req, res) => { + received.push(String(req.headers['x-github-event'])); + res.end(); + }); + await new Promise((resolve) => sink.listen(8194, resolve)); + try { + const res = await api('/orgs/acme/hooks', {method: 'POST', body: JSON.stringify({name: 'web', config: {url: 'http://localhost:8194/hook', content_type: 'json', secret: 's'}, events: ['*']})}); + expect(res.status).toBe(201); + await expect.poll(() => received).toEqual(['ping']); + } finally { + sink.close(); + } + }); +}); + +test.describe('fake semantics', () => { + test('search qualifiers are case-insensitive, repeated org: is OR, and @me resolves to the viewer', () => { + expect(parseSearch('is:pr is:OPEN org:a org:b author:@me "two words"', 'alice')).toEqual([ + {qualifier: 'is', values: ['pr'], negated: false}, + {qualifier: 'is', values: ['OPEN'], negated: false}, + {qualifier: 'org', values: ['a', 'b'], negated: false}, + {qualifier: 'author', values: ['alice'], negated: false}, + {qualifier: 'text', values: ['two words'], negated: false}, + ]); + }); + + test('scopes covered by a broader grant are folded away', () => { + expect(normalizeScopes(['public_repo', 'repo', 'read:org', 'notifications'])).toEqual(['notifications', 'read:org', 'repo']); + expect(normalizeScopes(['read:org', 'admin:org'])).toEqual(['admin:org']); + }); + + test('skeletons satisfy their own schemas', () => { + expect(validateResponse('GET', '/user', 200, skeleton('private-user'))).toEqual([]); + expect(validateWebhook('ping', webhookSkeleton('ping'))).toEqual([]); + }); + + test('the fake rejects payloads GitHub would never send', () => { + expect(validateResponse('GET', '/user', 200, {login: 'x'})).not.toEqual([]); + const payload = buildPayload(seededState(), {event: 'issues', action: 'opened', repo: 'acme/widgets', number: 1, sender: 'alice'}); + expect(validateWebhook('issues', {...payload, action: 'teleported'})).not.toEqual([]); + }); +}); diff --git a/e2e/integration/tests/issues.spec.ts b/e2e/integration/tests/issues.spec.ts new file mode 100644 index 000000000..674806d3e --- /dev/null +++ b/e2e/integration/tests/issues.spec.ts @@ -0,0 +1,108 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {expect, test, type GitHubFixture} from '../support/fixture'; +import {fakeGitHub} from '../support/fake_github'; +import {channelPosts, chooseOption, openChannel, openCreateIssueModal, postWithText, selectField, waitForPost} from '../support/ui'; + +const repo = 'acme/widgets'; + +async function seedRepo(github: GitHubFixture, options: {canPush?: boolean} = {}) { + await fakeGitHub.createUser('octo-author'); + await fakeGitHub.createOrg('acme', {members: [github.login, 'octo-author']}); + await fakeGitHub.createRepo(repo, {writers: options.canPush === false ? ['octo-author'] : [github.login, 'octo-author'], labels: ['bug', 'enhancement'], milestones: ['v1.0']}); +} + +test.describe('link tooltip', () => { + test('shows pull request details for a github.com link', async ({pw, github}) => { + await seedRepo(github); + const pr = await fakeGitHub.createItem(repo, {kind: 'pull', title: 'Add widget polishing', author: 'octo-author', labels: ['enhancement'], head: 'polish', base: 'main'}); + await github.connect(); + const url = `https://github.com/${repo}/pull/${pr.number}`; + await github.userClient.createPost({channel_id: github.channel.id, message: `Have a look: ${url}`} as any); + const {page} = await openChannel(pw, github.user, github.team.name); + + await postWithText(page, 'Have a look').getByRole('link', {name: url}).hover(); + + const tooltip = page.locator('.github-tooltip').first(); + await expect(tooltip).toContainText('Add widget polishing'); + await expect(tooltip).toContainText(`#${pr.number}`); + await expect(tooltip.locator('.opened-by')).toContainText('octo-author'); + await expect(tooltip.locator('.commit-ref')).toHaveText(['main', 'polish']); + await expect(tooltip.locator('.labels')).toContainText('enhancement'); + expect(await fakeGitHub.calls()).toContain(`GET /api/repos/${repo}/pulls/${pr.number}`); + }); + + test('shows issue details for a github.com link', async ({pw, github}) => { + await seedRepo(github); + const issue = await fakeGitHub.createItem(repo, {kind: 'issue', title: 'Widgets squeak on Mondays', author: 'octo-author', body: 'Every Monday, without fail.'}); + await github.connect(); + const url = `https://github.com/${repo}/issues/${issue.number}`; + await github.userClient.createPost({channel_id: github.channel.id, message: url} as any); + const {page} = await openChannel(pw, github.user, github.team.name); + + await page.getByRole('link', {name: url}).last().hover(); + + const tooltip = page.locator('.github-tooltip').first(); + await expect(tooltip).toContainText('Widgets squeak on Mondays'); + await expect(tooltip).toContainText('Every Monday, without fail.'); + }); +}); + +test.describe('create issue from a post', () => { + async function openCreateIssue(pw: Parameters[0], github: GitHubFixture, message: string) { + await github.userClient.createPost({channel_id: github.channel.id, message} as any); + const {channelsPage, page} = await openChannel(pw, github.user, github.team.name); + return {page, modal: await openCreateIssueModal(channelsPage, page)}; + } + + test('creates the issue with labels, assignee and milestone and links back to the post', async ({pw, github}) => { + await seedRepo(github); + await github.connect(); + const {page, modal} = await openCreateIssue(pw, github, 'The widget squeaks when it rains'); + + await expect(selectField(modal, 'Organization')).toContainText(github.login); + await chooseOption(page, selectField(modal, 'Repository'), 'widgets'); + await modal.getByLabel('Title for the GitHub Issue').fill('Widget squeaks in the rain'); + await chooseOption(page, selectField(modal, 'Labels'), 'bug'); + await chooseOption(page, selectField(modal, 'Assignees'), github.login); + await chooseOption(page, selectField(modal, 'Milestone'), 'v1.0'); + await modal.getByRole('button', {name: 'Submit'}).click(); + await expect(modal).toBeHidden(); + + const created = (await fakeGitHub.items()).find((i) => i.title === 'Widget squeaks in the rain'); + expect(created).toMatchObject({kind: 'issue', author: github.login, labels: ['bug'], assignees: [github.login], milestone: 1}); + expect(created!.body).toContain('The widget squeaks when it rains'); + const reply = await waitForPost(() => channelPosts(github.adminClient, github.channel.id), (p) => p.message.startsWith('Created GitHub issue'), 'issue created reply'); + expect(reply.message).toContain(`https://github.com/${repo}/issues/${created!.number}`); + }); + + // GitHub silently drops labels, assignees and milestones from users without push access; the modal hides them. + test('hides issue attributes for a repository the user cannot push to', async ({pw, github}) => { + await seedRepo(github, {canPush: false}); + await github.connect(); + const {page, modal} = await openCreateIssue(pw, github, 'Read-only repo message'); + + await chooseOption(page, selectField(modal, 'Repository'), 'widgets'); + + await expect(modal.getByText('Labels', {exact: true})).toHaveCount(0); + await expect(modal.getByText('Assignees', {exact: true})).toHaveCount(0); + await modal.getByLabel('Title for the GitHub Issue').fill('Filed without push access'); + await modal.getByRole('button', {name: 'Submit'}).click(); + await expect(modal).toBeHidden(); + expect((await fakeGitHub.items()).find((i) => i.title === 'Filed without push access')).toMatchObject({labels: [], assignees: []}); + }); + + test('switching organization lists that organization\'s repositories', async ({pw, github}) => { + await seedRepo(github); + await fakeGitHub.createOrg('gizmo-co', {members: [github.login]}); + await fakeGitHub.createRepo('gizmo-co/gizmos', {writers: [github.login]}); + await github.connect(); + const {page, modal} = await openCreateIssue(pw, github, 'Which org?'); + + await chooseOption(page, selectField(modal, 'Organization'), 'gizmo-co'); + + await expect.poll(() => fakeGitHub.calls(), {timeout: 5_000}).toContain('GET /api/orgs/gizmo-co/repos'); + await chooseOption(page, selectField(modal, 'Repository'), 'gizmos'); + }); +}); diff --git a/e2e/integration/tests/sidebar.spec.ts b/e2e/integration/tests/sidebar.spec.ts new file mode 100644 index 000000000..5d053aedc --- /dev/null +++ b/e2e/integration/tests/sidebar.spec.ts @@ -0,0 +1,78 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {expect, test, type GitHubFixture} from '../support/fixture'; +import {fakeGitHub} from '../support/fake_github'; +import {openChannel, sidebarButton} from '../support/ui'; + +const repo = 'acme/widgets'; + +async function seedWork(github: GitHubFixture) { + const me = github.login; + await fakeGitHub.createUser('octo-author'); + await fakeGitHub.createOrg('acme', {members: [me, 'octo-author']}); + await fakeGitHub.createRepo(repo, {writers: [me, 'octo-author'], milestones: ['v1.0']}); + await fakeGitHub.createItem(repo, {kind: 'pull', title: 'Add widget polishing', author: me, labels: ['enhancement'], additions: 42, deletions: 7, changedFiles: 3}); + await fakeGitHub.createItem(repo, {kind: 'pull', title: 'Fix widget wobble', author: me, ciState: 'failure'}); + const review = await fakeGitHub.createItem(repo, {kind: 'pull', title: 'Please review the gizmo', author: 'octo-author', reviewers: [me]}); + await fakeGitHub.createItem(repo, {kind: 'pull', title: 'Closed widget work', author: me, state: 'closed'}); + await fakeGitHub.createItem(repo, {kind: 'issue', title: 'Widgets squeak on Mondays', author: 'octo-author', assignees: [me], milestone: 1}); + await fakeGitHub.createThread(me, repo, {number: review.number, reason: 'review_requested'}); +} + +test('sidebar counts and RHS lists come from GitHub search and notifications', async ({pw, github}) => { + await seedWork(github); + await github.connect(); + const {page} = await openChannel(pw, github.user, github.team.name); + + await expect(sidebarButton(page, 'fa-compress')).toHaveText('2'); + await expect(sidebarButton(page, 'fa-code-fork')).toHaveText('1'); + await expect(sidebarButton(page, 'fa-list-ol')).toHaveText('1'); + await expect(sidebarButton(page, 'fa-envelope')).toHaveText('1'); + + const rhs = page.locator('#rhsContainer'); + await sidebarButton(page, 'fa-compress').click(); + await expect(rhs.getByRole('link', {name: 'Your Open Pull Requests'})).toBeVisible(); + await expect(rhs.getByRole('link', {name: 'Add widget polishing'})).toBeVisible(); + await expect(rhs.getByRole('link', {name: 'Fix widget wobble'})).toBeVisible(); + await expect(rhs.getByText('Closed widget work')).toHaveCount(0); + await expect(rhs.getByText('3 Files Changed')).toBeVisible(); + await expect(rhs.getByLabel('Success')).toHaveCount(1); + + await sidebarButton(page, 'fa-code-fork').click(); + await expect(rhs.getByRole('link', {name: 'Pull Requests Needing Review'})).toBeVisible(); + await expect(rhs.getByRole('link', {name: 'Please review the gizmo'})).toBeVisible(); + + await sidebarButton(page, 'fa-list-ol').click(); + await expect(rhs.getByRole('link', {name: 'Your Assignments'})).toBeVisible(); + await expect(rhs.getByRole('link', {name: 'Widgets squeak on Mondays'})).toBeVisible(); + await expect(rhs.getByRole('note', {name: 'v1.0'})).toBeVisible(); + + await sidebarButton(page, 'fa-envelope').click(); + await expect(rhs.getByRole('link', {name: 'Unread Messages'})).toBeVisible(); + await expect(rhs.getByText('Please review the gizmo')).toBeVisible(); + + const calls = await fakeGitHub.calls(); + expect(calls).toEqual(expect.arrayContaining(['POST /api/graphql', 'GET /api/notifications', `GET /api/repos/${repo}/pulls/1/reviews`, `GET /api/repos/${repo}/pulls/1`])); +}); + +test('an org lock limits the sidebar to that organization', async ({pw, github}) => { + await seedWork(github); + await fakeGitHub.createOrg('elsewhere', {members: [github.login]}); + await fakeGitHub.createRepo('elsewhere/gadgets', {writers: [github.login]}); + await fakeGitHub.createItem('elsewhere/gadgets', {kind: 'pull', title: 'Gadget PR outside the org', author: github.login}); + await github.connect(); + const {page} = await openChannel(pw, github.user, github.team.name); + await expect(sidebarButton(page, 'fa-compress')).toHaveText('3'); + + const {patchPluginSettings} = await import('../support/plugin'); + await patchPluginSettings(github.adminClient, {githuborg: 'acme'}); + try { + await page.reload(); + await expect(sidebarButton(page, 'fa-compress')).toHaveText('2'); + const graphql = (await fakeGitHub.requests()).filter((r) => r.path === '/api/graphql').at(-1); + expect(JSON.stringify(graphql?.body)).toContain('org:acme'); + } finally { + await patchPluginSettings(github.adminClient, {githuborg: ''}); + } +}); diff --git a/e2e/integration/tests/smoke.spec.ts b/e2e/integration/tests/smoke.spec.ts new file mode 100644 index 000000000..12e99b4f7 --- /dev/null +++ b/e2e/integration/tests/smoke.spec.ts @@ -0,0 +1,16 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {expect, test} from '../support/fixture'; +import {fakeGitHub} from '../support/fake_github'; +import {openChannel, postWithText, runSlashCommand} from '../support/ui'; + +test('/github me reports the connected GitHub account', async ({pw, github}) => { + await github.connect(); + const {channelsPage, page} = await openChannel(pw, github.user, github.team.name); + + await runSlashCommand(channelsPage, '/github me'); + + await expect(postWithText(page, 'You are connected to GitHub as')).toContainText(github.login); + expect(await fakeGitHub.calls()).toContain('GET /api/user'); +}); diff --git a/e2e/integration/tests/todo.spec.ts b/e2e/integration/tests/todo.spec.ts new file mode 100644 index 000000000..852a7497f --- /dev/null +++ b/e2e/integration/tests/todo.spec.ts @@ -0,0 +1,35 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {expect, test} from '../support/fixture'; +import {fakeGitHub} from '../support/fake_github'; +import {openChannel, postWithText, runSlashCommand} from '../support/ui'; + +test('/github todo lists unread notifications, review requests, open PRs and assignments', async ({pw, github}) => { + const me = github.login; + await fakeGitHub.createUser('octo-author'); + await fakeGitHub.createRepo(`${me}/widgets`, {writers: ['octo-author']}); + const review = await fakeGitHub.createItem(`${me}/widgets`, {kind: 'pull', title: 'Please review the gizmo', author: 'octo-author', reviewers: [me]}); + await fakeGitHub.createItem(`${me}/widgets`, {kind: 'pull', title: 'My own widget PR', author: me}); + await fakeGitHub.createItem(`${me}/widgets`, {kind: 'issue', title: 'Assigned widget bug', author: 'octo-author', assignees: [me]}); + await fakeGitHub.createThread(me, `${me}/widgets`, {number: review.number, reason: 'review_requested'}); + await github.connect(); + const {channelsPage, page} = await openChannel(pw, github.user, github.team.name); + + await runSlashCommand(channelsPage, '/github todo'); + + const todo = postWithText(page, 'Unread Messages'); + await expect(todo).toContainText('Review Requests'); + await expect(todo).toContainText('Please review the gizmo'); + await expect(todo).toContainText('Your Open Pull Requests'); + await expect(todo).toContainText('My own widget PR'); + await expect(todo).toContainText('Your Assignments'); + await expect(todo).toContainText('Assigned widget bug'); + + const searches = (await fakeGitHub.requests()).filter((r) => r.path === '/api/search/issues').map((r) => r.query.q); + expect(searches).toEqual(expect.arrayContaining([ + expect.stringContaining(`review-requested:${me}`), + expect.stringContaining(`author:${me}`), + expect.stringContaining(`assignee:${me}`), + ])); +}); diff --git a/e2e/integration/tests/ui_layout.spec.ts b/e2e/integration/tests/ui_layout.spec.ts new file mode 100644 index 000000000..00d549c5a --- /dev/null +++ b/e2e/integration/tests/ui_layout.spec.ts @@ -0,0 +1,105 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import type {Page} from '@playwright/test'; + +import {webhookSecret, webhookURL} from '../support/constants'; +import {expect, test} from '../support/fixture'; +import {fakeGitHub} from '../support/fake_github'; +import {expectInside, expectNoOverlap, expectNothingClipped} from '../support/layout'; +import {labels, longTitle, repo, seedWork as seed} from '../support/seed'; +import {openChannel, openCreateIssueModal, postWithText, sidebarButton, sidebarButtonBar} from '../support/ui'; + +function sidebarButtons(page: Page) { + return (['fa-compress', 'fa-code-fork', 'fa-list-ol', 'fa-envelope'] as const).map((icon) => sidebarButton(page, icon)); +} + +test('the sidebar buttons fit the channel sidebar header without overlapping', async ({pw, github}) => { + await seed(github); + const {page} = await openChannel(pw, github.user, github.team.name); + const buttons = sidebarButtons(page); + await expect(buttons[0]).toHaveText('1'); + const sidebar = page.locator('#SidebarContainer'); + + for (const button of buttons) { + await expectInside(button, sidebar); + } + for (let i = 1; i < buttons.length; i++) { + await expectNoOverlap(buttons[i - 1], buttons[i]); + } +}); + +test('a long pull request title and many labels wrap inside the right-hand panel', async ({pw, github}) => { + await seed(github); + const {page} = await openChannel(pw, github.user, github.team.name); + await sidebarButton(page, 'fa-compress').click(); + const rhs = page.locator('#rhsContainer'); + const title = rhs.getByRole('link', {name: longTitle}); + + await expect(title).toBeVisible(); + await expectInside(title, rhs); + for (const label of labels) { + await expectInside(rhs.getByLabel(label), rhs); + } + await expectNothingClipped(rhs.locator('.scrollbar--view')); +}); + +test('the link tooltip stays inside the window and keeps its content', async ({pw, github}) => { + const pr = await seed(github); + const url = `https://github.com/${repo}/pull/${pr.number}`; + await github.userClient.createPost({channel_id: github.channel.id, message: url} as any); + const {page} = await openChannel(pw, github.user, github.team.name); + + await page.getByRole('link', {name: url}).last().hover(); + const tooltip = page.locator('.github-tooltip .box').first(); + await expect(tooltip).toContainText(longTitle.slice(0, 40)); + + const viewport = page.viewportSize()!; + const box = (await tooltip.boundingBox())!; + expect(box.x).toBeGreaterThanOrEqual(0); + expect(box.x + box.width).toBeLessThanOrEqual(viewport.width); + await expectInside(tooltip.locator('.labels'), tooltip); +}); + +test('a webhook post with labels fits the post', async ({github, pw}) => { + const pr = await seed(github); + await fakeGitHub.createHook('acme', 'widgets', webhookURL, webhookSecret); + await github.userClient.executeCommand(`/github subscriptions add ${repo}`, {channel_id: github.channel.id, team_id: github.team.id}); + await fakeGitHub.webhook({event: 'pull_request', action: 'opened', repo, number: pr.number, sender: github.login}); + const {page} = await openChannel(pw, github.user, github.team.name); + + const post = postWithText(page, '#new-pull-request'); + await expect(post).toBeVisible(); + await expectInside(post.getByRole('heading', {name: longTitle}), post); + await expectNothingClipped(post.locator('.post-message__text')); +}); + +test.describe('accessibility', () => { + test('the sidebar buttons and right-hand panel have no violations', async ({pw, github, axe}) => { + await seed(github); + const {page} = await openChannel(pw, github.user, github.team.name); + await sidebarButton(page, 'fa-compress').click(); + await expect(page.locator('#rhsContainer').getByRole('link', {name: longTitle})).toBeVisible(); + await sidebarButtonBar(page).evaluate((el) => el.setAttribute('data-e2e-axe', 'github-sidebar-buttons')); + + const results = await axe.builder(page).include('[data-e2e-axe="github-sidebar-buttons"]').include('#rhsContainer .scrollbar--view').analyze(); + + // Known plugin bug: the icon-only GitHub profile and refresh links have no accessible name. + // Drop this entry once they are labelled; any other violation still fails. + expect(results.violations.flatMap((v) => v.nodes.map((n) => `${v.id}: ${n.html.match(/fa-[\w-]+/)?.[0] ?? n.target}`))).toEqual(['link-name: fa-github', 'link-name: fa-refresh']); + }); + + test('the create issue modal has no violations', async ({pw, github, axe}) => { + await seed(github); + await github.userClient.createPost({channel_id: github.channel.id, message: 'Make this an issue'} as any); + const {channelsPage, page} = await openChannel(pw, github.user, github.team.name); + await openCreateIssueModal(channelsPage, page); + + const results = await axe.builder(page).include('[role="dialog"]').analyze(); + + // Known plugin bug: the five react-selects and the description textarea have no associated label. + // Drop this entry once they are labelled; any other violation still fails. + const knownUnlabelled = 'label: Form elements must have labels (6)'; + expect(results.violations.map((v) => `${v.id}: ${v.help} (${v.nodes.length})`)).toEqual([knownUnlabelled]); + }); +}); \ No newline at end of file diff --git a/e2e/integration/tests/visual.spec.ts b/e2e/integration/tests/visual.spec.ts new file mode 100644 index 000000000..46f53b81c --- /dev/null +++ b/e2e/integration/tests/visual.spec.ts @@ -0,0 +1,191 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import type {PlaywrightExtended} from '@mattermost/playwright-lib'; +import type {Locator} from '@playwright/test'; + +import type {WebhookRequest} from '../fake-github/webhooks'; +import {webhookSecret, webhookURL} from '../support/constants'; +import {expect, test, type GitHubFixture} from '../support/fixture'; +import {fakeGitHub} from '../support/fake_github'; +import {longTitle, repo, seedWork} from '../support/seed'; +import {channelPosts, openChannel, openCreateIssueModal, postWithText, replaceText, runSlashCommand, sidebarButton, sidebarButtonBar, waitForPost} from '../support/ui'; + +test.describe('@visual', () => { + // eslint-disable-next-line no-process-env + test.skip(!process.env.PW_VISUAL_BROWSER_WS, 'baselines only match the dockerized browser (npm run test:visual)'); + + // Core UI overlapping the target (avatars, status, timestamps) changes between runs. + async function settle(pw: PlaywrightExtended, target: Locator, {keepHover = false} = {}) { + const page = target.page(); + await expect(page.locator('.LoadingScreen')).toBeHidden(); + await pw.hideDynamicChannelsContent(page); + if (!keepHover) { + await page.mouse.move(0, 0); + } + await page.evaluate(() => document.fonts.ready); + await page.evaluate(() => Promise.all(document.getAnimations().filter((a) => a.effect?.getComputedTiming().endTime !== Infinity).map((a) => a.finished))); + await target.evaluate((el: HTMLElement) => { + const {x, y} = el.getBoundingClientRect(); + el.style.transform = `translate(${Math.round(x) - x}px, ${Math.round(y) - y}px)`; + }); + } + + // Users and commit SHAs are random per run, and "Opened 2 years ago" grows with the calendar. + async function freeze(github: GitHubFixture, target: Locator) { + await replaceText(target, [ + [github.login, 'gh-octocat'], + [`@${github.user.username}`, '@octocat'], + [github.user.username, 'octocat'], + [/\b[0-9a-f]{6,40}\b/g, 'a1b2c3'], + [/\b(a|an|a few|\d+) (seconds?|minutes?|hours?|days?|months?|years?)\b/g, '2 years'], + ]); + } + + async function shoot(pw: PlaywrightExtended, github: GitHubFixture, target: Locator, name: string, options?: {keepHover?: boolean}) { + await expect(target).toBeVisible(); + await freeze(github, target); + await settle(pw, target, options); + await expect(target).toHaveScreenshot(name); + } + + async function rhsTab(pw: PlaywrightExtended, github: GitHubFixture, icon: Parameters[1], title: string) { + await seedWork(github); + const {page} = await openChannel(pw, github.user, github.team.name); + await sidebarButton(page, icon).click(); + const list = page.locator('#rhsContainer .scrollbar--view'); + await expect(list.getByRole('link', {name: title})).toBeVisible(); + return list; + } + + async function webhookPost(pw: PlaywrightExtended, github: GitHubFixture, request: Omit, marker: string) { + await fakeGitHub.createHook('acme', 'widgets', webhookURL, webhookSecret); + await github.userClient.executeCommand(`/github subscriptions add ${repo} --features pulls,issues,issue_comments,pushes`, {channel_id: github.channel.id, team_id: github.team.id}); + await fakeGitHub.webhook({repo, sender: github.login, ...request}); + const post = await waitForPost(() => channelPosts(github.adminClient, github.channel.id), (p) => p.message.includes(marker), `webhook post containing "${marker}"`); + const opened = await openChannel(pw, github.user, github.team.name); + return {...opened, post, message: opened.page.locator(`#post_${post.id} .post-message__text`)}; + } + + test('sidebar buttons', async ({pw, github}) => { + await seedWork(github); + const {page} = await openChannel(pw, github.user, github.team.name); + await expect(sidebarButton(page, 'fa-compress')).toHaveText('1'); + await shoot(pw, github, sidebarButtonBar(page), 'sidebar-buttons.png'); + }); + + test('open pull requests in the right-hand panel', async ({pw, github}) => { + await shoot(pw, github, await rhsTab(pw, github, 'fa-compress', longTitle), 'rhs-open-prs.png'); + }); + + test('review requests in the right-hand panel', async ({pw, github}) => { + await shoot(pw, github, await rhsTab(pw, github, 'fa-code-fork', 'Please review the gizmo'), 'rhs-review-requests.png'); + }); + + test('assignments with a milestone in the right-hand panel', async ({pw, github}) => { + await shoot(pw, github, await rhsTab(pw, github, 'fa-list-ol', 'Widgets squeak on Mondays'), 'rhs-assignments.png'); + }); + + test('unread messages in the right-hand panel', async ({pw, github}) => { + await shoot(pw, github, await rhsTab(pw, github, 'fa-envelope', 'Unread Messages'), 'rhs-unreads.png'); + }); + + test('pull request link tooltip', async ({pw, github}) => { + const pr = await seedWork(github); + const url = `https://github.com/${repo}/pull/${pr.number}`; + await github.userClient.createPost({channel_id: github.channel.id, message: url} as any); + const {page} = await openChannel(pw, github.user, github.team.name); + await page.getByRole('link', {name: url}).last().hover(); + const tooltip = page.locator('.github-tooltip .box').first(); + await expect(tooltip).toContainText('Splits polishing into stages.'); + await shoot(pw, github, tooltip, 'tooltip-pull.png', {keepHover: true}); + }); + + test('issue link tooltip', async ({pw, github}) => { + await seedWork(github); + const url = `https://github.com/${repo}/issues/3`; + await github.userClient.createPost({channel_id: github.channel.id, message: url} as any); + const {page} = await openChannel(pw, github.user, github.team.name); + await page.getByRole('link', {name: url}).last().hover(); + const tooltip = page.locator('.github-tooltip .box').first(); + await expect(tooltip).toContainText('Widgets squeak on Mondays'); + await shoot(pw, github, tooltip, 'tooltip-issue.png', {keepHover: true}); + }); + + test('new pull request webhook post', async ({pw, github}) => { + const pr = await seedWork(github); + const {message} = await webhookPost(pw, github, {event: 'pull_request', action: 'opened', number: pr.number}, '#new-pull-request'); + await shoot(pw, github, message, 'webhook-new-pull.png'); + }); + + test('merged pull request webhook post', async ({pw, github}) => { + const pr = await seedWork(github); + await fakeGitHub.updateItem(repo, pr.number, {state: 'closed', merged: true}); + const {message} = await webhookPost(pw, github, {event: 'pull_request', action: 'closed', number: pr.number}, 'was merged'); + await shoot(pw, github, message, 'webhook-merged-pull.png'); + }); + + test('new issue webhook post', async ({pw, github}) => { + await seedWork(github); + const {message} = await webhookPost(pw, github, {event: 'issues', action: 'opened', number: 3}, '#new-issue'); + await shoot(pw, github, message, 'webhook-new-issue.png'); + }); + + test('issue comment webhook post', async ({pw, github}) => { + await seedWork(github); + const comment = 'Oiled them, still squeaky. Next step is replacing the **bearing**:\n\n- order part\n- swap it'; + const {message} = await webhookPost(pw, github, {event: 'issue_comment', action: 'created', number: 3, comment}, 'New comment by'); + await shoot(pw, github, message, 'webhook-issue-comment.png'); + }); + + test('push webhook post', async ({pw, github}) => { + await seedWork(github); + const {message} = await webhookPost(pw, github, {event: 'push'}, 'pushed [1 new commit]'); + await shoot(pw, github, message, 'webhook-push.png'); + }); + + test('webhook post in the reply thread', async ({pw, github}) => { + const pr = await seedWork(github); + const {channelsPage, post} = await webhookPost(pw, github, {event: 'pull_request', action: 'opened', number: pr.number}, '#new-pull-request'); + await (await channelsPage.centerView.getPostById(post.id)).reply(); + const rhsPost = await channelsPage.sidebarRight.getPostById(post.id); + await shoot(pw, github, rhsPost.container.locator('.post-message__text'), 'reply-thread.png'); + }); + + test('GitHub account in the profile popover', async ({pw, github}) => { + await github.connect(); + await github.userClient.createPost({channel_id: github.channel.id, message: 'Who am I on GitHub?'} as any); + const {channelsPage} = await openChannel(pw, github.user, github.team.name); + const popover = await channelsPage.openProfilePopover(await channelsPage.getLastPost()); + const attribute = popover.container.locator('a[href^="https://github.com/"]').locator('xpath=..'); + await shoot(pw, github, attribute, 'profile-popover.png'); + }); + + test('create issue modal', async ({pw, github}) => { + await seedWork(github); + await github.userClient.createPost({channel_id: github.channel.id, message: 'Make this an issue'} as any); + const {channelsPage, page} = await openChannel(pw, github.user, github.team.name); + const modal = await openCreateIssueModal(channelsPage, page); + await shoot(pw, github, modal.locator('.modal-content'), 'create-issue-modal.png'); + }); + + test('attach to issue modal', async ({pw, github}) => { + await seedWork(github); + await github.userClient.createPost({channel_id: github.channel.id, message: 'Attach this to an issue'} as any); + const {channelsPage, page} = await openChannel(pw, github.user, github.team.name); + const post = await channelsPage.getLastPost(); + await post.hover(); + await post.postMenu.actionsButton.click(); + await page.getByRole('menuitem', {name: 'Attach to GitHub Issue'}).click(); + const modal = page.getByRole('dialog').filter({hasText: 'Attach Message to GitHub Issue'}); + await expect(modal.getByText('Attach this to an issue')).toBeVisible(); + await shoot(pw, github, modal.locator('.modal-content'), 'attach-issue-modal.png'); + }); + + test('/github todo reply', async ({pw, github}) => { + await seedWork(github); + const {channelsPage, page} = await openChannel(pw, github.user, github.team.name); + await runSlashCommand(channelsPage, '/github todo'); + await shoot(pw, github, postWithText(page, 'Unread Messages').locator('.post-message__text'), 'todo.png'); + }); +}); diff --git a/e2e/integration/tests/webhooks.spec.ts b/e2e/integration/tests/webhooks.spec.ts new file mode 100644 index 000000000..e03bef3d9 --- /dev/null +++ b/e2e/integration/tests/webhooks.spec.ts @@ -0,0 +1,146 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {defaultScopes} from '../fake-github/server'; +import {signatures} from '../fake-github/webhooks'; +import {webhookSecret, webhookURL} from '../support/constants'; +import {expect, test, type GitHubFixture} from '../support/fixture'; +import {fakeGitHub} from '../support/fake_github'; +import {botDMPosts, channelPosts, openChannel, postWithText, runSlashCommand, waitForPost} from '../support/ui'; + +const noWebhookNote = 'No webhook was found for this repository or organization'; +const allFeatures = 'pulls,issues,issue_comments,pushes'; + +async function subscribedRepo(github: GitHubFixture) { + const repo = `${github.login}/widgets`; + await fakeGitHub.createRepo(repo); + await fakeGitHub.createHook(github.login, 'widgets', webhookURL, webhookSecret); + await github.connect(); + await github.userClient.executeCommand(`/github subscriptions add ${repo} --features ${allFeatures}`, {channel_id: github.channel.id, team_id: github.team.id}); + return repo; +} + +function waitForChannelPost(github: GitHubFixture, text: string) { + return waitForPost(() => channelPosts(github.adminClient, github.channel.id), (p) => p.message.includes(text), `channel post containing "${text}"`); +} + +test.describe('subscriptions', () => { + test('warns that the repository has no webhook pointing at this server', async ({pw, github}) => { + await fakeGitHub.createRepo(`${github.login}/widgets`); + await github.connect(undefined, {scopes: [...defaultScopes, 'repo']}); + const {channelsPage, page} = await openChannel(pw, github.user, github.team.name); + + await runSlashCommand(channelsPage, `/github subscriptions add ${github.login}/widgets`); + + await expect(postWithText(page, 'subscribed this channel to')).toContainText(`${github.login}/widgets`); + await expect(postWithText(page, noWebhookNote)).toContainText('/github setup webhook'); + }); + + test('drops the warning once a webhook points at this server', async ({pw, github}) => { + await fakeGitHub.createRepo(`${github.login}/widgets`); + await fakeGitHub.createHook(github.login, 'widgets', webhookURL, webhookSecret); + await github.connect(undefined, {scopes: [...defaultScopes, 'repo']}); + const {channelsPage, page} = await openChannel(pw, github.user, github.team.name); + + await runSlashCommand(channelsPage, `/github subscriptions add ${github.login}/widgets`); + + await expect(postWithText(page, 'subscribed this channel to')).toContainText(`${github.login}/widgets`); + await expect.poll(() => fakeGitHub.calls()).toContain(`GET /api/repos/${github.login}/widgets/hooks`); + await expect(postWithText(page, noWebhookNote)).toHaveCount(0); + }); + + // Listing repository hooks needs `repo` or `read:repo_hook`; the plugin's public-repo scopes have neither, + // GitHub answers 404, and the plugin treats that as "can't tell" instead of warning. + test('cannot check for a webhook with the default public-repo scopes', async ({pw, github}) => { + await fakeGitHub.createRepo(`${github.login}/widgets`); + await github.connect(); + const {channelsPage, page} = await openChannel(pw, github.user, github.team.name); + + await runSlashCommand(channelsPage, `/github subscriptions add ${github.login}/widgets`); + + await expect(postWithText(page, 'subscribed this channel to')).toBeVisible(); + await expect.poll(async () => (await fakeGitHub.requests()).find((r) => r.path === `/api/repos/${github.login}/widgets/hooks`)?.status).toBe(404); + await expect(postWithText(page, noWebhookNote)).toHaveCount(0); + }); +}); + +test.describe('webhook events', () => { + test('a new pull request is posted to the subscribed channel', async ({github}) => { + const repo = await subscribedRepo(github); + const pr = await fakeGitHub.createItem(repo, {kind: 'pull', title: 'Add widget polishing', author: github.login, body: 'Polishes widgets.', labels: ['enhancement']}); + + const {deliveries} = await fakeGitHub.webhook({event: 'pull_request', action: 'opened', repo, number: pr.number, sender: github.login}); + + expect(deliveries.map((d) => d.status)).toEqual([200]); + const post = await waitForChannelPost(github, 'Add widget polishing'); + expect(post.message).toContain('#new-pull-request'); + expect(post.message).toContain(`https://github.com/${repo}/pull/${pr.number}`); + }); + + test('a new issue is posted to the subscribed channel', async ({github}) => { + const repo = await subscribedRepo(github); + const issue = await fakeGitHub.createItem(repo, {kind: 'issue', title: 'Widgets wobble', author: github.login, body: 'They wobble.'}); + + await fakeGitHub.webhook({event: 'issues', action: 'opened', repo, number: issue.number, sender: github.login}); + + const post = await waitForChannelPost(github, 'Widgets wobble'); + expect(post.message).toContain('#new-issue'); + }); + + test('an issue comment is posted to the subscribed channel', async ({github}) => { + const repo = await subscribedRepo(github); + const issue = await fakeGitHub.createItem(repo, {kind: 'issue', title: 'Widgets squeak', author: github.login}); + + await fakeGitHub.webhook({event: 'issue_comment', action: 'created', repo, number: issue.number, sender: github.login, comment: 'Oiled them, still squeaky.'}); + + const post = await waitForChannelPost(github, 'Oiled them, still squeaky.'); + expect(post.message).toContain('New comment by'); + }); + + test('a push is posted to the subscribed channel', async ({github}) => { + const repo = await subscribedRepo(github); + + await fakeGitHub.webhook({event: 'push', repo, sender: github.login}); + + const post = await waitForChannelPost(github, 'Update README'); + expect(post.message).toContain('pushed [1 new commit]'); + expect(post.message).toContain(`${repo}:main`); + }); + + test('a review request DMs the connected reviewer', async ({pw, github}) => { + const repo = await subscribedRepo(github); + const reviewer = await pw.createNewUserProfile(github.adminClient, {prefix: 'reviewer', disableTutorial: true, disableOnboarding: true}); + await github.adminClient.addToTeam(github.team.id, reviewer.id); + const {login: reviewerLogin} = await github.connect(reviewer); + const pr = await fakeGitHub.createItem(repo, {kind: 'pull', title: 'Review my widget', author: github.login, reviewers: [reviewerLogin]}); + + await fakeGitHub.webhook({event: 'pull_request', action: 'review_requested', repo, number: pr.number, sender: github.login, requestedReviewer: reviewerLogin}); + + const dm = await waitForPost(() => botDMPosts(github.adminClient, reviewer.id), (p) => p.message.includes('requested your review'), 'review request DM'); + expect(dm.message).toContain('Review my widget'); + }); +}); + +test.describe('webhook signatures', () => { + async function deliverRaw(github: GitHubFixture, sign: (body: string) => Record) { + const repo = await subscribedRepo(github); + const issue = await fakeGitHub.createItem(repo, {kind: 'issue', title: 'Tampered issue', author: github.login}); + const {payload} = await fakeGitHub.webhook({event: 'issues', action: 'opened', repo, number: issue.number, sender: github.login, deliver: false}); + const body = JSON.stringify(payload); + const res = await fetch(webhookURL, {method: 'POST', body, headers: {'Content-Type': 'application/json', 'X-GitHub-Event': 'issues', ...sign(body)}}); + const posts = await channelPosts(github.adminClient, github.channel.id); + return {status: res.status, posted: posts.some((p) => p.message.includes('Tampered issue'))}; + } + + test('a delivery without X-Hub-Signature is rejected', async ({github}) => { + expect(await deliverRaw(github, () => ({}))).toEqual({status: 401, posted: false}); + }); + + test('a delivery signed with another secret is rejected', async ({github}) => { + expect(await deliverRaw(github, (body) => signatures('not-the-webhook-secret', body))).toEqual({status: 401, posted: false}); + }); + + test('a delivery signed with the right secret is accepted', async ({github}) => { + expect(await deliverRaw(github, (body) => signatures(webhookSecret, body))).toEqual({status: 200, posted: true}); + }); +}); diff --git a/e2e/integration/tsconfig.json b/e2e/integration/tsconfig.json new file mode 100644 index 000000000..980db1313 --- /dev/null +++ b/e2e/integration/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "commonjs", + "moduleResolution": "node", + "lib": ["es2022", "dom"], + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/e2e/playwright/mock_oauth_server/mock_oauth_server.ts b/e2e/playwright/mock_oauth_server/mock_oauth_server.ts index 1d9456cd0..b5d7c5f3e 100644 --- a/e2e/playwright/mock_oauth_server/mock_oauth_server.ts +++ b/e2e/playwright/mock_oauth_server/mock_oauth_server.ts @@ -49,5 +49,34 @@ export const makeOAuthServer = ({ app.use(authorizeURLPrefix, oauthRouter); + // E2E builds also send REST, GraphQL and upload calls here; this suite expects them to reach GitHub. + app.use('/api', forwardTo('https://api.github.com')); + app.use('/uploads', forwardTo('https://uploads.github.com')); + return app; }; + +const hopByHopHeaders = ['host', 'connection', 'content-length', 'content-encoding', 'transfer-encoding']; + +function forwardTo(target: string): express.RequestHandler[] { + return [express.raw({type: () => true, limit: '25mb'}), async (req, res) => { + const headers = Object.fromEntries(Object.entries(req.headers).filter(([name]) => !hopByHopHeaders.includes(name))) as Record; + const hasBody = Buffer.isBuffer(req.body) && req.body.length > 0; + + // Express 4 doesn't catch rejected handlers; an unreachable GitHub would otherwise crash the mock. + let upstream: Awaited>; + try { + upstream = await fetch(target + req.url, {method: req.method, headers, body: hasBody ? req.body : undefined}); + } catch (err) { + res.status(502).send(`mock OAuth server could not reach ${target}: ${err}`); + return; + } + + upstream.headers.forEach((value, name) => { + if (!hopByHopHeaders.includes(name)) { + res.setHeader(name, value); + } + }); + res.status(upstream.status).send(Buffer.from(await upstream.arrayBuffer())); + }]; +} diff --git a/server/plugin/graphql/client.go b/server/plugin/graphql/client.go index a606dcb8b..e56d4917d 100644 --- a/server/plugin/graphql/client.go +++ b/server/plugin/graphql/client.go @@ -25,35 +25,34 @@ type Client struct { // NewClient creates and returns Client. The third party package that queries GraphQL is initialized here. func NewClient(logger pluginapi.LogService, getOrganizations func() []string, token oauth2.Token, username, orgName, enterpriseBaseURL string) *Client { - ts := oauth2.StaticTokenSource(&token) - httpClient := oauth2.NewClient(context.Background(), ts) - var client Client - if enterpriseBaseURL == "" { - client = Client{ + return &Client{ username: username, - client: githubv4.NewClient(httpClient), + client: githubv4.NewClient(oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(&token))), logger: logger, org: orgName, getOrganizations: getOrganizations, } - } else { - baseURL, err := url.JoinPath(enterpriseBaseURL, "api", "graphql") - if err != nil { - logger.Debug("Not able to parse the enterprise URL", "error", err.Error()) - return nil - } + } - client = Client{ - client: githubv4.NewEnterpriseClient(baseURL, httpClient), - username: username, - org: orgName, - logger: logger, - getOrganizations: getOrganizations, - } + baseURL, err := url.JoinPath(enterpriseBaseURL, "api", "graphql") + if err != nil { + logger.Debug("Not able to parse the enterprise URL", "error", err.Error()) + return nil } - return &client + return NewClientWithURL(logger, getOrganizations, token, username, orgName, baseURL) +} + +// NewClientWithURL creates a Client for the GraphQL endpoint at graphQLURL. +func NewClientWithURL(logger pluginapi.LogService, getOrganizations func() []string, token oauth2.Token, username, orgName, graphQLURL string) *Client { + return &Client{ + client: githubv4.NewEnterpriseClient(graphQLURL, oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(&token))), + username: username, + org: orgName, + logger: logger, + getOrganizations: getOrganizations, + } } // executeQuery takes a query struct and sends it to Github GraphQL API via helper package. diff --git a/server/plugin/plugin.go b/server/plugin/plugin.go index 079bfd172..a306ef48f 100644 --- a/server/plugin/plugin.go +++ b/server/plugin/plugin.go @@ -63,7 +63,8 @@ const ( invalidTokenError = "401 Bad credentials" //#nosec G101 -- False positive ) -// testOAuthServerURL is the URL for the oauthServer used for testing purposes +// testOAuthServerURL is the URL of a fake GitHub used for testing purposes. When set, it replaces +// github.com for OAuth, api.github.com (under /api) and uploads.github.com (under /uploads). // It should be set through ldflags when compiling for E2E, and keep it blank otherwise var testOAuthServerURL = "" @@ -181,6 +182,9 @@ func (p *Plugin) githubConnectUser(_ context.Context, info *GitHubUserInfo) *git func (p *Plugin) graphQLConnect(info *GitHubUserInfo) *graphql.Client { conf := p.getConfiguration() + if testOAuthServerURL != "" && conf.EnterpriseBaseURL == "" { + return graphql.NewClientWithURL(p.client.Log, p.configuration.getOrganizations, *info.Token, info.GitHubUsername, conf.GitHubOrg, testOAuthServerURL+"/api/graphql") + } return graphql.NewClient(p.client.Log, p.configuration.getOrganizations, *info.Token, info.GitHubUsername, conf.GitHubOrg, conf.EnterpriseBaseURL) } @@ -205,7 +209,17 @@ func GetGitHubClient(token oauth2.Token, config *Configuration) (*github.Client, func getGitHubClient(authenticatedClient *http.Client, config *Configuration) (*github.Client, error) { if config.EnterpriseBaseURL == "" || config.EnterpriseUploadURL == "" { - return github.NewClient(authenticatedClient), nil + client := github.NewClient(authenticatedClient) + if testOAuthServerURL != "" { + var err error + if client.BaseURL, err = url.Parse(testOAuthServerURL + "/api/"); err != nil { + return nil, err + } + if client.UploadURL, err = url.Parse(testOAuthServerURL + "/uploads/"); err != nil { + return nil, err + } + } + return client, nil } baseURL, err := url.JoinPath(config.EnterpriseBaseURL, "api", "v3") if err != nil { @@ -251,6 +265,10 @@ func (p *Plugin) setDefaultConfiguration() error { func (p *Plugin) OnActivate() error { p.ensurePluginAPIClient() + if testOAuthServerURL != "" { + p.client.Log.Warn("GitHub OAuth, REST and GraphQL endpoints are overridden; this build is for E2E testing only", "url", testOAuthServerURL) + } + _, err := getSiteURL(p.client) if err != nil { return err diff --git a/server/plugin/plugin_test.go b/server/plugin/plugin_test.go index 8b565d07d..2c1afbb66 100644 --- a/server/plugin/plugin_test.go +++ b/server/plugin/plugin_test.go @@ -625,3 +625,18 @@ func TestUseGitHubClient_AuthFailureNotifiesUser(t *testing.T) { }) } } + +func TestGetGitHubClientWithTestServerURL(t *testing.T) { + original := testOAuthServerURL + testOAuthServerURL = "http://fake-github:8093" + t.Cleanup(func() { testOAuthServerURL = original }) + + client, err := getGitHubClient(http.DefaultClient, &Configuration{}) + require.NoError(t, err) + require.Equal(t, "http://fake-github:8093/api/", client.BaseURL.String()) + require.Equal(t, "http://fake-github:8093/uploads/", client.UploadURL.String()) + + enterprise, err := getGitHubClient(http.DefaultClient, &Configuration{EnterpriseBaseURL: "https://ghe.example.com", EnterpriseUploadURL: "https://ghe.example.com"}) + require.NoError(t, err) + require.Equal(t, "https://ghe.example.com/api/v3/", enterprise.BaseURL.String()) +} diff --git a/webapp/src/components/github_repo_selector/github_repo_selector.jsx b/webapp/src/components/github_repo_selector/github_repo_selector.jsx index 7ad3fd393..87ecc4022 100644 --- a/webapp/src/components/github_repo_selector/github_repo_selector.jsx +++ b/webapp/src/components/github_repo_selector/github_repo_selector.jsx @@ -63,12 +63,14 @@ export default class GithubRepoSelector extends PureComponent { if (prevProps.yourOrgs !== this.props.yourOrgs && this.props.yourOrgs.length > 0) { const newOrg = this.props.yourOrgs[0].login; if (this.state.org !== newOrg) { - this.onChangeForOrg(newOrg); + this.selectOrg(newOrg); } } } - onChangeForOrg = (org) => { + onChangeForOrg = (_, org) => this.selectOrg(org); + + selectOrg = (org) => { if (this.state.org !== org) { this.setState({org}); this.props.actions.getReposByOrg(org, this.props.currentChannelId); From 269f6f8527d75bb101ba3e0c6540e6c6c786c22e Mon Sep 17 00:00:00 2001 From: Nevyana Angelova Date: Mon, 2 Nov 2026 15:37:34 +0700 Subject: [PATCH 2/2] MM-71467 Address review: scope real-lane secrets, install deps for drift check, tolerate non-JSON GitHub errors Co-authored-by: Cursor --- .github/workflows/e2e.yml | 21 ++++++++++++------- .../fake-github/contract/validator.ts | 5 +++-- e2e/integration/real-github/api.ts | 15 +++++++++++-- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index f09994798..54c611613 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -142,6 +142,12 @@ jobs: - uses: actions/setup-node@v4 with: node-version-file: .nvmrc + cache: npm + cache-dependency-path: e2e/integration/package-lock.json + + - name: Install e2e dependencies + working-directory: e2e/integration + run: npm ci - name: Compare committed specs with GitHub's published REST description and GraphQL schema working-directory: e2e/integration @@ -158,13 +164,6 @@ jobs: cancel-in-progress: false env: CI: true - E2E_GITHUB_OAUTH_CLIENT_ID: ${{ secrets.E2E_GITHUB_OAUTH_CLIENT_ID }} - E2E_GITHUB_OAUTH_CLIENT_SECRET: ${{ secrets.E2E_GITHUB_OAUTH_CLIENT_SECRET }} - E2E_GITHUB_REPO: ${{ secrets.E2E_GITHUB_REPO }} - E2E_GITHUB_MACHINE_TOKEN: ${{ secrets.E2E_GITHUB_MACHINE_TOKEN }} - E2E_GITHUB_LOGIN_USERNAME: ${{ secrets.E2E_GITHUB_LOGIN_USERNAME }} - E2E_GITHUB_LOGIN_PASSWORD: ${{ secrets.E2E_GITHUB_LOGIN_PASSWORD }} - E2E_GITHUB_LOGIN_TOTP_SECRET: ${{ secrets.E2E_GITHUB_LOGIN_TOTP_SECRET }} steps: - uses: actions/checkout@v4 with: @@ -202,3 +201,11 @@ jobs: - name: Run journey working-directory: e2e/integration run: npm run test:real + env: + E2E_GITHUB_OAUTH_CLIENT_ID: ${{ secrets.E2E_GITHUB_OAUTH_CLIENT_ID }} + E2E_GITHUB_OAUTH_CLIENT_SECRET: ${{ secrets.E2E_GITHUB_OAUTH_CLIENT_SECRET }} + E2E_GITHUB_REPO: ${{ secrets.E2E_GITHUB_REPO }} + E2E_GITHUB_MACHINE_TOKEN: ${{ secrets.E2E_GITHUB_MACHINE_TOKEN }} + E2E_GITHUB_LOGIN_USERNAME: ${{ secrets.E2E_GITHUB_LOGIN_USERNAME }} + E2E_GITHUB_LOGIN_PASSWORD: ${{ secrets.E2E_GITHUB_LOGIN_PASSWORD }} + E2E_GITHUB_LOGIN_TOTP_SECRET: ${{ secrets.E2E_GITHUB_LOGIN_TOTP_SECRET }} diff --git a/e2e/integration/fake-github/contract/validator.ts b/e2e/integration/fake-github/contract/validator.ts index b5f0b3ca1..e21e3961b 100644 --- a/e2e/integration/fake-github/contract/validator.ts +++ b/e2e/integration/fake-github/contract/validator.ts @@ -30,7 +30,7 @@ for (const format of ['uri-template', 'repo.nwo', 'ssh-key', 'ssh-key fingerprin } ajv.addSchema({components: fromOpenAPI(rest.components)}, 'rest'); -type Operation = {method: string; template: string; pattern: RegExp; request?: ValidateFunction; responses: Map}; +type Operation = {method: string; template: string; pattern: RegExp; request?: ValidateFunction; responses: Map; documented: Set}; const operations: Operation[] = Object.entries(rest.paths as JsonObject).flatMap(([template, methods]) => Object.entries(methods as JsonObject).map(([method, op]) => ({ method: method.toUpperCase(), @@ -41,6 +41,7 @@ const operations: Operation[] = Object.entries(rest.paths as JsonObject).flatMap const schema = jsonSchema(resolve(response)); return schema ? [[status, ajv.compile(fromOpenAPI(schema))]] : []; })), + documented: new Set(Object.keys(op.responses as JsonObject)), }))); const webhooks = new Map(Object.entries(rest['x-webhooks'] as JsonObject).map(([name, hook]) => [name, ajv.compile(fromOpenAPI(jsonSchema(hook.post.requestBody)))])); @@ -65,7 +66,7 @@ export function validateResponse(method: string, pathname: string, status: numbe } const validator = op.responses.get(String(status)) ?? apiWide.get(String(status)); if (!validator) { - if (op.responses.has(String(status)) || status === 204 || status === 304) { + if (op.documented.has(String(status)) || status === 204 || status === 304) { return []; } return [`${method} ${op.template}: GitHub does not document a ${status} response`]; diff --git a/e2e/integration/real-github/api.ts b/e2e/integration/real-github/api.ts index 6c8de42c9..f06642a09 100644 --- a/e2e/integration/real-github/api.ts +++ b/e2e/integration/real-github/api.ts @@ -35,8 +35,19 @@ export async function gh(method: string, path: string, {body, token = r continue; } } - const text = await res.text(); - return {status: res.status, body: (text ? JSON.parse(text) : undefined) as T, headers: res.headers}; + return {status: res.status, body: parseBody(await res.text()) as T, headers: res.headers}; + } +} + +// GitHub's 5xx pages are HTML or plain text; keep them so callers can still report the status. +function parseBody(text: string): unknown { + if (!text) { + return undefined; + } + try { + return JSON.parse(text); + } catch { + return text; } }