diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 191e6a6ac..1a22ec711 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,6 +1,6 @@ # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.195.0/containers/javascript-node/.devcontainer/base.Dockerfile -# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 16, 14, 12, 16-bullseye, 14-bullseye, 12-bullseye, 16-buster, 14-buster, 12-buster -ARG VARIANT=20-bookworm +# [Choice] Node.js version/variant (use -bookworm variants on local arm64/Apple Silicon): e.g. 22-bookworm, 20-bookworm, 18-bookworm +ARG VARIANT=22-bookworm FROM mcr.microsoft.com/devcontainers/javascript-node:1-${VARIANT} # [Optional] Uncomment this section to install additional OS packages. @@ -14,16 +14,17 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ # [Optional] Uncomment if you want to install more global node modules # RUN su node -c "npm install -g " -# Update npm -RUN npm install -g npm -# Intall aws cli +# Install aws cli RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m).zip" -o "awscliv2.zip" && \ unzip awscliv2.zip && \ sudo ./aws/install && \ rm -rf ./aws && \ rm awscliv2.zip # Install sam cli -RUN curl -L "https://github.com/aws/aws-sam-cli/releases/latest/download/aws-sam-cli-linux-$(dpkg --print-architecture).zip" -o "aws-sam-cli.zip" && \ +RUN ARCH_RAW=$(uname -m) && \ + ARCH=$ARCH_RAW && \ + if [ "$ARCH_RAW" = "aarch64" ]; then ARCH="arm64"; fi && \ + curl -L "https://github.com/aws/aws-sam-cli/releases/latest/download/aws-sam-cli-linux-${ARCH}.zip" -o "aws-sam-cli.zip" && \ unzip aws-sam-cli.zip -d sam-installation && \ sudo ./sam-installation/install && \ rm -rf ./sam-installation && \ diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 62232b1da..38626686f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,10 +4,10 @@ "name": "Node.js", "build": { "dockerfile": "Dockerfile", - // Update 'VARIANT' to pick a Node version: 16, 14, 12. - // Append -bullseye or -buster to pin to an OS version. - // Use -bullseye variants on local arm64/Apple Silicon. - "args": { "VARIANT": "20-bookworm" } + // Update 'VARIANT' to pick a Node version, e.g. 22, 20, 18. + // Append -bookworm or -bullseye to pin to an OS version. + // Use -bookworm variants on local arm64/Apple Silicon. + "args": { "VARIANT": "22-bookworm" } }, "settings": {}, diff --git a/.github/workflows/advanced-codeql.yml b/.github/workflows/advanced-codeql.yml new file mode 100644 index 000000000..8dcb9517c --- /dev/null +++ b/.github/workflows/advanced-codeql.yml @@ -0,0 +1,72 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + workflow_dispatch: + push: + branches: [ "main-enterprise" ] + pull_request: + branches: [ "main-enterprise" ] + + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: javascript-typescript + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: 'Checkout repository' + uses: actions/checkout@v4 + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/create-pre-release.yml b/.github/workflows/create-pre-release.yml index 48cfc9f6c..ef5f7d22f 100644 --- a/.github/workflows/create-pre-release.yml +++ b/.github/workflows/create-pre-release.yml @@ -42,23 +42,23 @@ jobs: outputs: release: ${{ steps.prerelease.outputs.release }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: 16.x + node-version: 22.x cache: 'npm' - - run: npm install + - run: npm ci - name: Set up Docker Buildx - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f - name: Log in to the Container registry - uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build Docker Image Locally - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 with: context: . file: ./Dockerfile @@ -71,9 +71,10 @@ jobs: - name: Run Functional Tests id: functionaltest run: | - docker run --env APP_ID=${{ secrets.APP_ID }} --env PRIVATE_KEY=${{ secrets.PRIVATE_KEY }} --env WEBHOOK_SECRET=${{ secrets.WEBHOOK_SECRET }} -d -p 3000:3000 ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main-enterprise + CONTAINER_ID=$(docker run --env APP_ID=${{ secrets.APP_ID }} --env PRIVATE_KEY=${{ secrets.PRIVATE_KEY }} --env WEBHOOK_SECRET=${{ secrets.WEBHOOK_SECRET }} --env NODE_ENV=development -d -p 3000:3000 ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main-enterprise) sleep 10 - curl http://localhost:3000 + docker logs $CONTAINER_ID || true + curl --fail --retry 5 --retry-delay 3 --retry-connrefused http://localhost:3000 - run: echo "${{ github.ref }}" - name: Tag a final release id: prerelease @@ -85,7 +86,7 @@ jobs: commitish: ${{ github.ref }} - name: Push Docker Image if: ${{ success() }} - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 with: context: . file: ./Dockerfile diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index eff9f838a..47bf2d931 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -20,23 +20,23 @@ jobs: outputs: release: ${{ steps.finalrelease.outputs.release }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: 16.x + node-version: 22.x cache: "npm" - - run: npm install + - run: npm ci - name: Set up Docker Buildx - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f - name: Log in to the Container registry - uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build Docker Image Locally - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 with: context: . file: ./Dockerfile @@ -59,7 +59,7 @@ jobs: bump: final - name: Push Docker Image if: ${{ success() }} - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 with: context: . file: ./Dockerfile @@ -75,7 +75,7 @@ jobs: needs: build steps: - name: Clone repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Prepare run: | diff --git a/.github/workflows/deploy-k8s.yml b/.github/workflows/deploy-k8s.yml index a9e0fd1d6..7bfb4332b 100644 --- a/.github/workflows/deploy-k8s.yml +++ b/.github/workflows/deploy-k8s.yml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index 5db222f63..d44ec8039 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -11,19 +11,19 @@ concurrency: jobs: test: - if: ${{ github.actor != 'dependabot'}} + if: ${{ github.actor != 'dependabot[bot]'}} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} cache: npm - - run: npm install + - run: npm ci - run: npm run test:unit:ci strategy: matrix: node-version: - - 18 - - 20 + - 22 + - 24 diff --git a/.github/workflows/rc-release.yml b/.github/workflows/rc-release.yml index dcc67c28a..08997e161 100644 --- a/.github/workflows/rc-release.yml +++ b/.github/workflows/rc-release.yml @@ -23,11 +23,11 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Use Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: 16.x + node-version: 22.x cache: npm - run: npm ci - run: npm run build --if-present @@ -55,12 +55,12 @@ jobs: steps: - name: Check out the repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 - run: echo ${{ github.actor }} - name: Log in to the Container registry - uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -68,15 +68,15 @@ jobs: - name: Extract metadata id: meta - uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 with: images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} - name: Set up QEMU - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f - name: Build and push Docker image - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 with: context: . push: true @@ -92,7 +92,7 @@ jobs: # runs-on: ubuntu-latest # environment: staging # steps: -# - uses: actions/checkout@v4 +# - uses: actions/checkout@v6 # - name: package to docker # uses: ./.github/actions/publish-docker # with: diff --git a/.gitignore b/.gitignore index 9cd65700b..3eee961c2 100644 --- a/.gitignore +++ b/.gitignore @@ -128,7 +128,7 @@ npm-debug.log .DS_Store node_modules/ private-key.pem -.env +*.env *.pem .vscode yarn.lock diff --git a/.nvmrc b/.nvmrc index 9a2a0e219..53d1c14db 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20 +v22 diff --git a/Dockerfile b/Dockerfile index 5c2fcdf54..1da906ff3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,7 @@ -FROM node:20-alpine +FROM node:22-alpine@sha256:4d64b49e6c891c8fc821007cb1cdc6c0db7773110ac2c34bf2e6960adef62ed3 WORKDIR /opt/safe-settings ENV NODE_ENV production +ENV HOST=0.0.0.0 ## Set the Labels LABEL version="1.0" \ description="Probot app which is a modified version of Settings Probot GitHub App" \ diff --git a/README.md b/README.md index 07a626748..4419722c8 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ > It is possible specify a custom repo instead of the `admin` repo with `ADMIN_REPO`. See [Environment variables](#environment-variables) for more details. -1. The **settings** in the **default** branch are applied. If the settings are changed on a non-default branch and a PR is created to merge the changes, the app runs in a `dry-run` mode to evaluate and validate the changes. Checks pass or fail based on the `dry-run` results. +1. The **settings** in the **default** branch are applied. If the settings are changed on a non-default branch and a PR is created to merge the changes, the app runs in a `dry-run` mode to evaluate and validate the changes. Checks pass or fail based on the `dry-run` results. The dry-run compares the PR's config against the **base branch** config, so the check run and PR comment report only the changes the PR itself introduces (see [Dry-run PR comment](#dry-run-pr-comment)). 1. In `safe-settings` the settings can have 2 types of targets: 1. `org` - These settings are applied to the organization. `Org`-targeted settings are defined in `.github/settings.yml`. Currently, only `rulesets` are supported as `org`-targeted settings. @@ -168,7 +168,7 @@ The App listens to the following webhook events: - **repository.renamed**: If a repository is renamed, the default behavior is safe-settings will ignore this (for backward-compatibility). If `BLOCK_REPO_RENAME_BY_HUMAN` env variable is set to true, `safe-settings` will revert the repo to the previous name unless it is renamed using a `bot`. If it is renamed using a `bot`, it will try to copy the existing `.yml` to `.yml` so that the repo config yml stays consistent. If a file already exists, it doesn't create a new one. -- **pull_request.opened**, **pull_request.reopened**, **check_suite.requested**: If the settings are changed, but it is not in the `default` branch, and there is an existing PR, the code will validate the settings changes by running safe-settings in `nop` mode and update the PR with the `dry-run` status. +- **pull_request.opened**, **pull_request.reopened**, **check_suite.requested**: If the settings are changed, but it is not in the `default` branch, and there is an existing PR, the code will validate the settings changes by running safe-settings in `nop` mode and update the PR with the `dry-run` status. The run loads the base-branch config and filters the results so only the changes the PR introduces are reported (see [Dry-run PR comment](#dry-run-pr-comment)). - **repository_ruleset**: If the `ruleset` settings are modified in the UI manually, `safe-settings` will `sync` the settings to prevent any unauthorized changes. @@ -178,6 +178,42 @@ The App listens to the following webhook events: - __custom_property_values__: If new repository properties are set for a repository, `safe-settings` will run to so that if a sub-org config is defined by that property, it will be applied for the repo +- **repository_dispatch** (`event_type: safe-settings-generate`): Triggers the **settings generator**, which reads the current configuration of a repo/org/suborg and opens a PR against the `admin` repo with the generated YAML. See [Generating settings from existing configuration](#generating-settings-from-existing-configuration). + +### Dry-run PR comment + +When a config change is proposed in a PR (a non-default branch), `safe-settings` runs in `nop` (no-operation) `dry-run` mode and posts a comment summarizing what *would* change if the PR were merged. The results are filtered against the **base branch** config, so the comment reports only the changes the PR introduces — not the full diff against live GitHub settings. + +The comment contains: + +- A header with the run timestamp, the **number of repos considered**, and the **number of repos affected**. +- **Breakdown of changes** — a collapsible section, grouped by plugin/repo, showing field-level diffs. Each entry is marked as an addition, modification, or deletion, with the before/after values for modified fields. When there are no changes, it shows `No changes to apply.` +- **Breakdown of errors** — a collapsible section listing any errors by repo, or `None` when there are none. The check run is marked as failed when errors are present. +- **Informational messages** — a collapsible section listing non-error notices such as plugins skipped via [`disable_plugins`](#disabling-plugins-disable_plugins) or deletions suppressed by `additive_plugins`, so reviewers can see which settings were intentionally not applied. + +For very large diffs the comment is split across multiple comments, and the check-run summary is truncated with a notice when it exceeds the size limit. + +### Suborg re-evaluation after repo-level changes + +A repo's suborg membership can depend on state that is itself written by `safe-settings`: + +- `suborgteams` — repos belong to a suborg because a given team is granted access +- `suborgproperties` — repos belong to a suborg because a custom property has a given value +- `suborgrepos` — repos belong to a suborg because their name matches a glob + +When a repo-level change (a push to `.github/repos/.yml`, or a `repository.created` event for a brand-new repo) adds, removes, or changes a team or custom property, the repo may start or stop matching a suborg config. A new repo may also start matching a suborg because its name matches a `suborgrepos` glob. + +To handle this, after applying a repo-yml change `safe-settings` re-evaluates the repo's suborg membership. If the matched suborg source set changed, it runs the repo through the apply pipeline a second time so newly matched suborg settings are applied and settings from a no-longer-matching suborg can be removed in the same sync. + +**Scope:** Re-evaluation runs only on the repo-yml change paths (`Settings.sync` and the per-repo loop of `Settings.syncSelectedRepos`). Global settings changes (`syncAll`) and suborg-yml changes (`syncSubOrgs`) already iterate all relevant repos and do not need it. + +**Loop prevention.** Two guards prevent infinite re-evaluation: + +1. **Stability check (primary):** Before applying changes, `safe-settings` snapshots the set of suborg source paths that match the repo. After applying, it refreshes the suborg cache and recomputes the set. If the set did not change, re-evaluation stops. If a source appeared or disappeared, the repo is processed once more. +2. **Hard depth cap (safety net):** Each repo is re-evaluated at most `MAX_REEVALUATION_DEPTH = 1` time per sync. This resolves the dominant single-hop case (repo change → suborg membership changed → apply the corrected suborg overlay once) while preventing pathological chains (suborg A applies a team that activates suborg B that activates suborg C…). Chains beyond one hop are resolved on the next sync event, and a warning is logged when the cap is hit. + +**Trigger optimization.** Re-evaluation is skipped entirely when the applied repo change did not affect `teams`, `custom_properties`, repository creation, or repository rename state — these are the repo-level changes that can affect suborg matching. + ### Use `safe-settings` to rename repos If you rename a `` that corresponds to a repo, safe-settings will rename the repo to the new name. This behavior will take effect whether the env variable `BLOCK_REPO_RENAME_BY_HUMAN` is set or not. @@ -301,6 +337,60 @@ Notes: > ⚠️ **Warning:** When `{{EXTERNALLY_DEFINED}}` is removed from an existing branch protection rule or ruleset configuration, the status checks in the existing rules in GitHub will revert to the checks that are defined in safe-settings. From this point onwards, all status checks configured through the GitHub UI will be reverted back to the safe-settings configuration. +#### Referencing ruleset bypass actors and reviewers by name + +Rulesets normally require numeric ids for `bypass_actors[].actor_id` and for the +team in `required_reviewers[].reviewer.id`. To avoid looking these ids up, you +can reference them by name and safe-settings resolves them to the correct id +before applying the ruleset: + +- `bypass_actors[].name` — an alternative to `actor_id`. The value is resolved + based on `actor_type`: + - `Team` → team slug + - `User` → username + - `Integration` → GitHub App slug + - `RepositoryRole` → role name. Built-in roles (`read`, `triage`, `write`, + `maintain`, `admin`) are mapped automatically; any other name is looked up + among the organization's custom repository roles. +- `required_reviewers[].reviewer.slug` — an alternative to `reviewer.id`, the + slug of the reviewing team. + +```yaml +rulesets: + - name: Main protection + target: branch + enforcement: active + bypass_actors: + - name: my-team # resolved to actor_id + actor_type: Team + bypass_mode: always + - name: admin # built-in repository role + actor_type: RepositoryRole + bypass_mode: always + rules: + - type: pull_request + parameters: + required_approving_review_count: 1 + dismiss_stale_reviews_on_push: false + require_code_owner_review: false + require_last_push_approval: false + required_review_thread_resolution: false + required_reviewers: + - minimum_approvals: 1 + file_patterns: ["*.js"] + reviewer: + slug: my-reviewers-team # resolved to reviewer.id + type: Team +``` + +Notes: + - This is fully backward compatible. Existing policies that use `actor_id` / + `reviewer.id` continue to work unchanged, and numeric ids are never looked up. + - Provide either the name (`name` / `slug`) or the id (`actor_id` / + `reviewer.id`) for a given entry, not both. Specifying both is an error. + - If a name cannot be resolved to an id, the ruleset sync fails with a clear + error so the misconfiguration is surfaced rather than silently ignored. + #### Status checks inheritance across scopes Refer to [Status checks](docs/status-checks.md). @@ -451,6 +541,225 @@ And the `checkrun` page will look like this: image

+### Disabling plugins (`disable_plugins`) + +Any settings file (deployment-settings, org `settings.yml`, suborg, or repo) can +contain a top-level `disable_plugins` list to turn off one or more safe-settings +plugins for a given scope. Each entry is either: + +- A plugin name string (shorthand for `{ plugin: , target: all }`), or +- An object `{ plugin: , target: self | children | all }` (default `target: all`). + +Valid plugin names: `repository`, `labels`, `collaborators`, `teams`, +`milestones`, `branches`, `autolinks`, `validator`, `rulesets`, `environments`, +`custom_properties`, `custom_repository_roles`, `variables`, `archive`, +`app_installations`. + +#### Strip matrix (which source layers are removed before merge) + +| Declared at | `target: self` | `target: children` | `target: all` | +| -------------------------- | ------------------ | ------------------------- | ----------------------------- | +| deployment-settings | deployment | org + suborg + repo | deployment + org + suborg + repo | +| org `settings.yml` | org | suborg + repo | org + suborg + repo | +| suborgs/`*.yml` (matched) | suborg | repo | suborg + repo | +| repos/`*.yml` | repo | (no-op) | repo | + +When safe-settings builds the merged configuration for a repo, it strips the +disabled plugin's keys from the indicated source layers before merging. For +repo-level execution points (the `repository` and `archive` plugins) and +org-level execution points (`rulesets`, `custom_repository_roles`), a disable +that targets the corresponding layer also short-circuits the plugin run, and +the skip is recorded as an INFO `NopCommand` in NOP mode (PR check run). + +#### Cascade rules + +- **Union-only.** Strips accumulate across layers; a lower-level config can add + more strips but can never undo a strip declared above it. +- **No re-enable.** If `disable_plugins: [labels]` is set at the org layer, a + repo cannot re-enable `labels` for itself. + +#### Important limitation + +Because strips operate on **source layers**, a lower-level disable cannot +remove configuration contributed by a higher layer. For example, if `branches` +is defined at the org layer and a suborg adds +`disable_plugins: [{plugin: branches, target: all}]`, the suborg's strip +removes the `branches` key only from the suborg and repo layers — the org's +`branches` config still merges in, and the branches plugin still runs. + +To fully suppress a plugin for matched repos, declare the disable at (or above) +the layer that contributes the configuration — typically the org layer with +`target: all`, or at the deployment layer. + +#### Examples + +Org `settings.yml` — disable `custom_repository_roles` only at the org execution +point (rulesets still run): + +```yaml +disable_plugins: + - plugin: custom_repository_roles + target: self +``` + +Org `settings.yml` — disable `branches` everywhere (shorthand): + +```yaml +disable_plugins: + - branches +``` + +Suborg `suborgs/team-x.yml` — strip `labels` for matched repos (effective only +if `labels` is not also defined at the org layer): + +```yaml +disable_plugins: + - plugin: labels + target: all +``` + +### Additive plugins (`additive_plugins`) + +`additive_plugins` is the complementary "soft mode" to `disable_plugins`. When a +Diffable plugin is listed here, safe-settings will only **add** and **update** +entries — it will **never call `remove()`**. Items that exist on GitHub but are +absent from the YAML are preserved, effectively merging external changes with +your policy rather than overwriting them. + +Declare `additive_plugins` only in `settings.yml` (org level) to keep behaviour +consistent across all repositories. + +**Supported plugins** (all extend `Diffable`): + +| Plugin | Effect in additive mode | +|--------|------------------------| +| `labels` | Extra labels not in YAML are kept | +| `collaborators` | Extra collaborators not in YAML are kept | +| `teams` | Extra team permissions not in YAML are kept | +| `milestones` | Extra milestones not in YAML are kept | +| `autolinks` | Extra autolinks not in YAML are kept | +| `environments` | Extra environments not in YAML are kept | +| `custom_properties` | Extra property values not in YAML are kept | +| `variables` | Extra variables not in YAML are kept | +| `rulesets` | Extra rulesets not in YAML are kept | +| `custom_repository_roles` | Extra custom roles not in YAML are kept | + +> [!important] +> `repository`, `archive`, `branches`, and `validator` are **not** supported. +> Listing them in `additive_plugins` will produce a validation error. + +**NOP mode**: when `additive_plugins` is active and the diff would produce +deletions, an informational message — *"Additive mode active: N deletion(s) +suppressed by additive_plugins"* — is included in the PR check-run comment so +reviewers can see what is being preserved. + +**Example** — never delete labels or collaborators that were added outside of +safe-settings: + +```yaml +additive_plugins: + - labels + - collaborators +``` + +### App installation management (`app_installations`) + +Most safe-settings plugins target a **repository**. The `app_installations` +plugin is different: its target is a **GitHub App installation**. It lets you +declaratively manage *which repositories a GitHub App can access* (the app's +`repository_selection`), using the same `org` → `suborg` → `repo` config +hierarchy you already use for repository settings. + +This is useful for controlling, as code, which repos apps such as Copilot, +Dependabot, or your own internal apps are installed on across the org. + +#### Prerequisites + +- Safe-settings must be installed on the **enterprise** with the **Enterprise + organization installations** permission (see the + [Enterprise organization installations API](https://docs.github.com/en/enterprise-cloud@latest/rest/enterprise-admin/organization-installations)). + Managing app installations requires an enterprise-level token; the regular + org installation token is not sufficient. If safe-settings is not installed + on the enterprise with this permission, app installation sync is reported as + an error and skipped. +- The enterprise slug is read from the webhook event payload + (`payload.enterprise.slug`); no extra environment variable is required. + +#### How repository selection is resolved + +The config layer where `app_installations` is declared determines which repos +are selected for the app: + +| Layer | File | Repos selected for the app | +| --- | --- | --- | +| Org | `settings.yml` | All repos in the org | +| Suborg | `suborgs/*.yml` | Repos matching the suborg's targeting (`suborgrepos`, `suborgteams`, `suborgproperties`) | +| Repo | `repos/.yml` | That specific repo | + +> [!important] +> An app listed at the **org** level (which implies all repos) takes +> precedence. Suborg/repo-level selections for that same app are ignored, and +> repos are never removed from it by incremental (suborg/repo) changes — it is +> reconciled only by the full (scheduled) sync. + +#### Examples + +Org-level `settings.yml` — give an app access to **all** repos in the org: + +```yaml +app_installations: + - app_slug: my-internal-app +``` + +Suborg-level `suborgs/backend.yml` — give an app access to the repos targeted +by this suborg (here, all repos with the `Team=backend` custom property): + +```yaml +suborgproperties: + - Team: backend +app_installations: + - app_slug: my-internal-app +``` + +Repo-level `repos/my-repo.yml` — add this specific repo to the app: + +```yaml +app_installations: + - app_slug: my-internal-app +``` + +Removing an app from a suborg/repo config (or changing the suborg's targeting) +removes the affected repos from that app on the next sync, unless another layer +still selects them. + +#### Sync behavior + +- **Incremental (delta) sync** runs when a `suborgs/*.yml` or `repos/*.yml` + file changes. Only the apps affected by the changed file are reconciled: the + previous version of the file is compared with the new one to compute repos to + add (`repository_selection`) and repos to remove (`repository_unselection`). + Additions are applied before removals (422-safe swap), so a repo removed by one config and + added by another ends up present. +- **Full sync** runs on the schedule (cron), on manual sync, and when + `settings.yml` changes. It recomputes the full desired state for every managed + app across all layers and reconciles it against the live installation state. + This is the mechanism that corrects any configuration drift. +- Add/remove operations are automatically batched in chunks of 50 repos (the + API limit). + +> [!note] +> Drift on managed apps is reconciled by the **full (cron) sync**, not by +> webhooks. A GitHub App only receives `installation` repository events for its +> *own* installation, so safe-settings cannot detect — via webhooks — when a +> human changes another app's repository access. Keep the scheduled sync enabled +> for timely drift correction. + +#### Disabling and additive mode + +`app_installations` honors both [`disable_plugins`](#disabling-plugins-disable_plugins) +and [`additive_plugins`](#additive-plugins-additive_plugins). In additive mode +the plugin only **adds** repos to installations and never removes them. + ### The Settings Files The settings files can be used to set the policies at the `org`, `suborg` or `repo` level. @@ -470,6 +779,7 @@ The following can be configured: - `Repository name validation` using regex pattern - `Rulesets` - `Environments` - wait timer, required reviewers, prevent self review, protected branches deployment branch policy, custom deployment branch policy, variables, deployment protection rules +- `App installations` - which repositories a GitHub App installation can access (see [App installation management](#app-installation-management-app_installations)) See [`docs/sample-settings/settings.yml`](docs/sample-settings/settings.yml) for a sample settings file. @@ -573,7 +883,215 @@ You can pass environment variables; the easiest way to do it is via a `.env` fil 3. __[Deploy and install the app](docs/deploy.md)__. Alternatively, the __[GitHub Actions Guide](docs/github-action.md)__ describes how to run `safe-settings` with GitHub Actions. +## Smoke Testing + +The repository includes an end-to-end smoke test script (`smoke-test.js`) that validates safe-settings against a live GitHub organization. It starts the app, creates repos/configs via the API, and verifies that safe-settings correctly applies and enforces settings. + +### Prerequisites + +- **Node.js** (same version used to run safe-settings) +- **`gh` CLI** — authenticated and available on PATH (used for drift-remediation tests only) +- A **GitHub App** installed on the target org with the required permissions +- A `.env` file in the project root (see below) + +### Authentication + +The smoke test uses **two authentication methods**: + +- **GitHub App token** (via `APP_ID` + `PRIVATE_KEY`) — used for the majority of tests: creating configs, merging PRs, validating repos, teams, rulesets, custom properties, etc. +- **Fine-grained PAT** (via `GH_TOKEN`) — used **only** in Phase 2 (team removal) and Phase 3 (rogue ruleset creation). These drift-remediation tests must appear as a human action because safe-settings ignores webhook events where `sender.type` is `Bot`. + +### Configuration + +Add the following to your `.env` file: + +| Variable | Description | Required | +|---|---|---| +| `GH_ORG` | Target GitHub organization (e.g. `my-org`) | Yes | +| `APP_ID` | GitHub App ID | Yes | +| `PRIVATE_KEY` | GitHub App private key (use `\n` for newlines) | Yes | +| `WEBHOOK_PROXY_URL` | Smee.io proxy URL for webhooks | Yes | +| `ADMIN_REPO` | Admin repo name (default: `admin`) | No | +| `CONFIG_PATH` | Config path within admin repo (default: `.github`) | No | +| `GH_TOKEN` | Fine-grained PAT with org admin + repo permissions | Yes | +| `SMOKE_VERBOSE` | Set to `1` to show live safe-settings logs | No | + +### Running + +```bash +# Run all phases +npm run smoke-test +# or +node smoke-test.js + +# Interactive mode — pause after each phase for manual validation +npm run smoke-test:interactive +# or +node smoke-test.js --interactive + +# Run a single phase (with setup + teardown) +npm run smoke-test:phase -- 3 +# or +node smoke-test.js --phase 3 + +# Run a range of phases +npm run smoke-test:phase -- 1-3 +node smoke-test.js --phase 1-3 + +# Run specific comma-separated phases +npm run smoke-test:phase -- 1,3,5 +node smoke-test.js --phase 1,3,5 + +# Mix range + interactive +npm run smoke-test:phase -- 1-3 interactive +node smoke-test.js --phase 1-3 --interactive +``` + +### What it tests + +The smoke test runs the following phases: + +| Phase | Description | +|---|---| +| **Setup** | Initializes the admin repo with an empty `settings.yml`, removes stale test repos, and starts safe-settings | +| **Phase 1** | Creates a repo config (`test`), validates NOP mode via check runs, merges, and verifies repo creation, teams, custom properties, and rulesets | +| **Phase 2** | Removes a team from the repo and verifies safe-settings re-adds it (drift remediation) | +| **Phase 3** | Creates a rogue ruleset and verifies safe-settings removes it (drift remediation) | +| **Phase 4** | Creates `demo-repo-service1` with teams, topics, and branch protection | +| **Phase 5** | Creates a property-targeted suborg config, verifies suborg rulesets apply to two matching repos, then changes one repo's custom property and verifies the ruleset is removed only from the repo that no longer matches | +| **Phase 6** | Archives `demo-repo-service1` and verifies the repo is archived | +| **Phase 7** | Creates `demo-repo-service2` and verifies suborg rulesets are inherited | +| **Phase 7b** | Tests external group team sync | +| **Phase 8** | Creates org-level settings (custom repository roles + org rulesets) and verifies they are applied | +| **Phase 10** | Validates `disable_plugins` — ensures disabled plugins are skipped | +| **Phase 11** | Validates `additive_plugins` — verifies additive-mode plugin behaviour | +| **Phase 12** | Tests `custom_properties` plugin | +| **Phase 13** | Tests the `variables` plugin (create, update, remove variables) | +| **Teardown** | Shuts down safe-settings, deletes test repos, teams, custom roles, and rulesets | + +### Output + +The script uses colored terminal output with pass (✅) / fail (❌) indicators and prints a summary at the end: + +``` +══════════════════════════════════════ + Results: 45 passed, 0 failed +══════════════════════════════════════ +``` + + +## Generating settings from existing configuration + +Safe-settings normally works "forward": you declare settings in YAML and it applies them to GitHub. The **settings generator** does the reverse — it reads the *current* state of a repo, an org, or a collection of repos (a suborg) and produces the corresponding safe-settings YAML (`repos/.yml`, `settings.yml`, or `suborgs/.yml`). This is useful for onboarding existing repositories/orgs onto safe-settings without hand-authoring config. + +It can be invoked two ways: + +- **Standalone CLI** (`generate-settings.js`) — writes the generated file to your local filesystem. +- **App trigger** via a `repository_dispatch` event — the running app generates the file and opens a **pull request** against the admin repo. + +### Source types + +| `source_type` | `source_value` | What is extracted | Output file | +|---|---|---|---| +| `repo` | repository name | All repo-level plugins (repository, labels, collaborators, teams, milestones, branches, autolinks, custom_properties, variables, environments, repo rulesets) | `repos/.yml` | +| `org` | org login | Org-level rulesets and custom repository roles only | `settings.yml` | +| `custom-property` | `name=value` (e.g. `Team=backend`) | Repo-level settings **common to all repos** carrying that custom property value (intersection) | `suborgs/_.yml` | + +> **Note on suborgs:** for `custom-property`, the generator discovers every repo with the given custom property value, extracts each repo's config, and keeps only the settings that are **identical across all of them**. A `suborgproperties` selector is prepended automatically. + +### Overwrite behavior + +By default (`overwrite=false`) the generator will **not** replace an existing file. If the target already exists it writes a `.sample.yml` file next to it instead. Set `overwrite=true` to replace the file. + +### Standalone invocation + +The CLI loads variables from a `.env` file in the project root (`APP_ID`, `PRIVATE_KEY`, and optionally `GH_ORG`/`OWNER`). Options can be passed as flags or environment variables. + +```bash +# Generate repos/my-repo.yml from a single repository +node generate-settings.js \ + --source-type repo \ + --source-value my-repo \ + --owner my-org \ + --output-dir ./out + +# Generate settings.yml from org-level rulesets + custom repository roles +node generate-settings.js --source-type org --source-value my-org --output-dir ./out + +# Generate suborgs/Team_backend.yml from all repos with the custom property Team=backend +node generate-settings.js \ + --source-type custom-property \ + --source-value "Team=backend" \ + --owner my-org \ + --output-dir ./out + +# Overwrite an existing file instead of writing a .sample.yml +node generate-settings.js --source-type repo --source-value my-repo --owner my-org --overwrite + +# Using environment variables instead of flags +SOURCE_TYPE=repo SOURCE_VALUE=my-repo OWNER=my-org OUTPUT_DIR=./out node generate-settings.js +``` + +| Flag | Env var | Description | Default | +|---|---|---|---| +| `--source-type` | `SOURCE_TYPE` | `repo`, `org`, or `custom-property` | (required) | +| `--source-value` | `SOURCE_VALUE` | repo name / org login / `name=value` | (required) | +| `--property-name` | `SOURCE_PROPERTY_NAME` | Custom property name (alternative to encoding it in `--source-value`) | — | +| `--owner` | `OWNER` / `GITHUB_ORG` / `GH_ORG` | Org login (selects the matching App installation) | first installation | +| `--output-dir` | `OUTPUT_DIR` | Directory to write generated files into | `.` | +| `--overwrite` | `OVERWRITE=true` | Replace existing files instead of writing `.sample.yml` | `false` | + +### App invocation (`repository_dispatch`) + +When the app is running, trigger generation by sending a `repository_dispatch` event (with `event_type: safe-settings-generate`) to the **admin repo**. The app generates the file and opens a PR against the admin repo's default branch. + +```bash +# Generate a repo config and open a PR +gh api --method POST \ + /repos/my-org/admin/dispatches \ + -f event_type=safe-settings-generate \ + -F 'client_payload[source_type]=repo' \ + -F 'client_payload[source_value]=my-repo' \ + -F 'client_payload[overwrite]=false' + +# Generate org-level settings.yml and open a PR +gh api --method POST \ + /repos/my-org/admin/dispatches \ + -f event_type=safe-settings-generate \ + -F 'client_payload[source_type]=org' \ + -F 'client_payload[source_value]=my-org' + +# Generate a suborg config from a custom property +gh api --method POST \ + /repos/my-org/admin/dispatches \ + -f event_type=safe-settings-generate \ + -F 'client_payload[source_type]=custom-property' \ + -F 'client_payload[source_value]=Team=backend' \ + -F 'client_payload[overwrite]=false' +``` + +The `client_payload` fields are: + +| Field | Description | Required | +|---|---|---| +| `source_type` | `repo`, `org`, or `custom-property` | Yes | +| `source_value` | repo name / org login / `name=value` | Yes | +| `property_name` | Custom property name (alternative to encoding it in `source_value`) | No | +| `overwrite` | `true` to replace an existing file; otherwise a `.sample.yml` is created | No (default `false`) | + +> **Tip:** Always review the generated PR before merging. Running safe-settings in NOP mode against the generated config should report no unexpected diffs. + +#### Generated changes always go through a pull request + +The app **never** commits generated configuration directly to the admin repo's default branch. Every `repository_dispatch` invocation produces a pull request that must be reviewed and merged before it takes effect. Concretely, for each request the app: + +1. Creates a **new branch** off the admin repo's default branch (`safe-settings-generate/--`). +2. Commits the generated YAML **to that branch only**. +3. Opens a **pull request** from that branch against the default branch. + +This means it is safe to give developers write access to the admin repo so they can trigger generation: a `repository_dispatch` event can only create a branch and open a PR — it cannot change the live configuration on its own. The generated config does not reach the path safe-settings acts on until the PR is merged, so all changes are subject to your normal review process and any branch protection / required-reviews rules configured on the admin repo's default branch. +To enforce review, protect the admin repo's default branch (for example, require pull request reviews and disallow direct pushes). Because the generator only ever writes to a feature branch and opens a PR, those rules apply to every generated change. ## License diff --git a/app.yml b/app.yml index 04b1f7015..d22ebc1e6 100644 --- a/app.yml +++ b/app.yml @@ -22,22 +22,23 @@ default_events: - pull_request - push - repository + - repository_dispatch - repository_ruleset - team - # The set of permissions needed by the GitHub App. The format of the object uses # the permission name for the key (for example, issues) and the access type for # the value (for example, write). # Valid values are `read`, `write`, and `none` default_permissions: - repository_custom_properties: write - organization_custom_properties: admin - # Workflows, workflow runs and artifacts. (needed to read environments when repo is private or internal) # https://developer.github.com/v3/apps/permissions/#repository-permissions-for-actions actions: read + # Manage Actions variables. + # https://docs.github.com/en/rest/actions/variables?apiVersion=2022-11-28 + actions_variables: write + # Repository creation, deletion, settings, teams, and collaborators. # https://developer.github.com/v3/apps/permissions/#permission-on-administration administration: write @@ -54,6 +55,10 @@ default_permissions: # https://developer.github.com/v3/apps/permissions/#permission-on-deployments # deployments: read + enterprise_organization_installations: write + + enterprise_organization_installation_repositories: write + # Manage repository environments. # https://developer.github.com/v3/apps/permissions/#repository-permissions-for-environments environments: write @@ -62,10 +67,44 @@ default_permissions: # https://developer.github.com/v3/apps/permissions/#permission-on-issues issues: write + # Organization members and teams. + # https://developer.github.com/v3/apps/permissions/#permission-on-members + members: write + # Search repositories, list collaborators, and access repository metadata. # https://developer.github.com/v3/apps/permissions/#metadata-permissions metadata: read + # View and manage users blocked by the organization. + # https://developer.github.com/v3/apps/permissions/#permission-on-organization-user-blocking + # organization_user_blocking: read + + # Manage organization projects, columns, and cards. + # https://developer.github.com/v3/apps/permissions/#permission-on-organization-projects + # organization_projects: read + + # Manage team discussions and related comments. + # https://developer.github.com/v3/apps/permissions/#permission-on-team-discussions + # team_discussions: read + + # Manage the post-receive hooks for an organization. + # https://developer.github.com/v3/apps/permissions/#permission-on-organization-hooks + # organization_hooks: read + + # Get notified of, and update, content references. + # https://developer.github.com/v3/apps/permissions/ + organization_administration: write + + # Manage custom organization roles. + # https://docs.github.com/en/enterprise-cloud@latest/rest/authentication/permissions-required-for-github-apps?apiVersion=2026-03-10#organization-permissions-for-custom-organization-roles + organization_custom_org_roles: write + + # Manage custom repository roles. + # https://docs.github.com/en/enterprise-cloud@latest/rest/authentication/permissions-required-for-github-apps?apiVersion=2026-03-10#organization-permissions-for-custom-repository-roles + organization_custom_roles: write + + organization_custom_properties: admin + # Retrieve Pages statuses, configuration, and builds, as well as create new builds. # https://developer.github.com/v3/apps/permissions/#permission-on-pages # pages: read @@ -74,6 +113,8 @@ default_permissions: # https://developer.github.com/v3/apps/permissions/#permission-on-pull-requests pull_requests: write + repository_custom_properties: write + # Manage the post-receive hooks for a repository. # https://developer.github.com/v3/apps/permissions/#permission-on-repository-hooks # repository_hooks: read @@ -90,32 +131,8 @@ default_permissions: # https://developer.github.com/v3/apps/permissions/#permission-on-statuses statuses: write - # Organization members and teams. - # https://developer.github.com/v3/apps/permissions/#permission-on-members - members: write - - # View and manage users blocked by the organization. - # https://developer.github.com/v3/apps/permissions/#permission-on-organization-user-blocking - # organization_user_blocking: read - - # Manage organization projects, columns, and cards. - # https://developer.github.com/v3/apps/permissions/#permission-on-organization-projects - # organization_projects: read - - # Manage team discussions and related comments. - # https://developer.github.com/v3/apps/permissions/#permission-on-team-discussions - # team_discussions: read - - # Manage the post-receive hooks for an organization. - # https://developer.github.com/v3/apps/permissions/#permission-on-organization-hooks - # organization_hooks: read - - # Get notified of, and update, content references. - # https://developer.github.com/v3/apps/permissions/ - organization_administration: write - # Manage Actions variables. - # https://docs.github.com/en/rest/actions/variables?apiVersion=2022-11-28 + # https://docs.github.com/en/rest/actions/variables?apiVersion=2026-03-10 actions_variables: write diff --git a/docs/README.md b/docs/README.md index 6d1f17436..76c9ee10f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,3 +10,5 @@ | Configure deployment environments | [Deployment Environments](github-settings/6.%20deployment-environments.md) | | Configure auto-link references | [AutoLinks](github-settings/7.%20autolinks.md) | | Configure pre-defined labels for issues and pull requests | [Labels](github-settings/8.%20labels.md) | + +For information on disabling plugins, see [Disabling plugins](../README.md#disabling-plugins-disable_plugins) in the root README. diff --git a/docs/adr/0001-app-installation-plugin.md b/docs/adr/0001-app-installation-plugin.md new file mode 100644 index 000000000..5a4ec05a7 --- /dev/null +++ b/docs/adr/0001-app-installation-plugin.md @@ -0,0 +1,313 @@ +# 1. App installation management plugin + +- Status: Accepted +- Date: 2026-07-06 +- Last updated: 2026-07-08 — documented the repo-centric → app-centric shift and + the delta/full-sync model, and made reconciliation 422-safe (additions before + removals in both paths, with an explicit error when a `selected` installation + would be reduced to zero repositories). Earlier (2026-07-07): reporting subject + model, per-repo pipeline exclusion, startup verification, non-managed-app + safety guarantee, smoke tests, and removal of the redundant + `repository_selection` config attribute (org level is implicitly "all"). +- Deciders: safe-settings maintainers +- Related PR: `decyjphr-app-installation-plugin` + +## Context + +Safe-settings manages configuration whose **target is a repository** (branch +protection, labels, collaborators, …), with a couple of exceptions +(`rulesets`, `custom_repository_roles`) that target the organization. All of +these are driven through the org → suborg → repo configuration hierarchy and +applied by `syncAll` / `syncSelectedRepos` / `sync`. + +We need a new capability where the **target of the operation is a GitHub App +installation** rather than a repository. Concretely, safe-settings should +declaratively control **which repositories each installed GitHub App can +access** (the installation's repository access), driven by the same config +hierarchy: + +- **Org-level `settings.yml`** → the app should have access to **all** repos in + the org. +- **Suborg-level `suborgs/*.yml`** → repos selected by the suborg's targeting + criteria (custom properties, teams, names). +- **Repo-level `repos/*.yml`** → the specific repo, by name. + +Two hard constraints shaped the design: + +1. **A different credential is required.** Reading org/suborg/repo config and + resolving repos can use the normal per-installation Octokit client. But + **mutating an app's installation repository access** requires an Octokit + client authenticated as the App at the **enterprise** level, using the + [Enterprise Organization Installations API][ent-api] (permission: + *Enterprise organization installations*). +2. **Drift.** Humans can change an app's repo access outside safe-settings, so + we want to detect and revert that drift. + +We also want the design to accommodate **future non-repo targets** (e.g., +Copilot policies) without another ground-up rewrite. + +## Decision + +Add an `app_installations` plugin plus supporting infrastructure, wired into +the existing sync pipeline as a **separate phase**. + +This capability shifts safe-settings from a purely **repo-centric** model toward +a more general one. Until now, a trigger — a change to `settings.yml`, a +`suborgs/*.yml`, or a `repos/*.yml` — caused `syncAll` or `syncSelectedRepos` to +resolve a **collection of repositories** and invoke each plugin against them. +App installation management inverts part of that flow: a change still resolves a +collection of repositories, but it **also** resolves a **collection of +applications**, which are then processed iteratively. For each application the +plugin computes the set of repositories that should constitute its +`repository_selection`. + +The subtlety is that a single `repos/*.yml` change surfaces only **one** +repository in the changed set, whereas an app's desired access is the **union of +every** repository that currently targets it — conceptually `existing + new`. +Removal is harder still: to know which repositories an app should *lose*, we must +compare against the repositories derived from the **previous commit** on the +default branch. We call the process of computing these per-app additions and +deletions from a config change **delta sync**; a complementary **full sync** +recomputes each app's complete desired state from scratch to reconcile +configuration drift. Both are detailed under *Sync model* below. + +### Configuration shape + +```yaml +# settings.yml (org level) — implies "all repos" +app_installations: + - app_slug: ghas-compliance-decyjphr-emu + - app_slug: migrator-destination-dotcom + +# suborgs/team-a.yml — repos selected by this suborg's criteria +app_installations: + - app_slug: migrator-destination-dotcom + +# repos/my-repo.yml — this specific repo +app_installations: + - app_slug: migrator-destination-dotcom +``` + +### Components + +| Component | Responsibility | +| --- | --- | +| `lib/plugins/appInstallations.js` | Reconcile desired vs. live repo access per app (`syncDelta` / `syncFull`). Not `Diffable` — app installations are an org-scoped target, not a per-repo list. | +| `lib/appOctokitClient.js` | Enterprise-level App client for the Enterprise Organization Installations API. | +| `lib/repoSelector.js` | Resolve a repo set from **fixed** criteria: name, team, custom properties, or "all". | +| `lib/settings.js` | `syncAppInstallations` phase; delta computation from changed configs; full desired-state computation; app-aware NOP reporting. | +| `lib/nopcommand.js` | Carries an optional `subject` / `subjectType` so reporting can render the **app** (not the org placeholder repo) as the subject of a change. | +| `index.js` | Enterprise-client enrichment on the context (`getEnterpriseAppClient`); `installation_target` webhook handler; startup `verifyAppInstallationsPlugin` diagnostic. | + +### Sync model: delta vs. full + +- **Delta (`syncSelectedRepos`, push events)**: only apps that appear in + **changed** config files are "marked for change". For each changed + suborg/repo file we compute, per app: + - `repository_selection` — repos to **add**, + - `repository_unselection` — repos to **remove** (by diffing the previous + `baseRef` version of *that one file* — one extra fetch, reusing the existing + "removed from suborg targeting" pattern). + + Apps configured with org-level "all" are **not** handled in delta mode; they + are managed only by full sync. + +- **Full (`syncAll`, cron/manual)**: recompute the complete desired state for + every managed app across all config layers and reconcile against live API + state. This is the only place the expensive full computation runs, and the + only path that reconciles drift. + +### Enterprise API usage + +All mutations go through the org-scoped Enterprise Organization Installations +API (version `2026-03-10`) and operate on repository **names**: + +- List installations: `GET /enterprises/{ent}/apps/organizations/{org}/installations` +- List repos: `GET …/installations/{id}/repositories` +- Toggle all/selected: `PATCH …/installations/{id}/repositories` +- Add: `PATCH …/installations/{id}/repositories/add` +- Remove: `PATCH …/installations/{id}/repositories/remove` + +Add/remove are capped at **50 repos per call** and are auto-batched. + +### Reporting (NOP / PR check-run) + +The rest of safe-settings reports changes **per repository**. App installation +changes have no meaningful repository subject — the NopCommands are emitted +against the ` (org)` placeholder repo, which rendered confusingly (e.g. a +`**admin**` heading with a nested `value: (all repositories)` row). + +To fix this without a disruptive rename of the repo-centric reporting pipeline, +`NopCommand` gained an **optional, additive `subject` / `subjectType`**: + +- `subject` defaults to the repo, so every existing plugin is unaffected. +- `app_installations` sets `subject = `, `subjectType = 'app'`. +- Reporting groups changes by `subject` (identical to the repo for all other + plugins), so each **app** becomes its own heading. +- For `app_installations`, the impact summary pluralizes **apps** (not repos), + rows render as a flat `+`/`-` list of repositories, and the section is + excluded from the "repos affected" count (app subjects must not inflate it). +- The `NopCommand` constructor accepts the `subject` object in the `type` + position (defaulting `type` to `INFO`) so callers can pass a subject without + restating the default type. + +## Decisions and rationale + +1. **Enterprise auth is a prerequisite, not a config knob.** + safe-settings must already be installed on the enterprise with the + *Enterprise organization installations* permission. If it is not, the plugin + surfaces a clear error rather than accepting a separate private-key env var. + The enterprise slug is read from the webhook payload + (`payload.enterprise.slug`); the enterprise installation id is discovered via + `apps.listInstallations` (matching `target_type === 'Enterprise'` && + `account.slug === enterprise.slug`) and cached for reuse. + +2. **App installation sync is a separate phase**, not folded into `updateOrg()`. + This keeps repo iteration and app reconciliation independent and easier to + reason about and disable. + +3. **Fixed repo-selection criteria only** (name, team, custom properties, plus + "all"). No arbitrary Search API queries, to keep behavior predictable and + reuse existing `getReposForTeam` / `getRepositoriesByProperty` patterns. + +4. **Org-level "all" takes precedence** over any suborg/repo-level selection or + exclusion. An org-level `app_installations` entry lists only the `app_slug` + — it always implies **all** repos (there is no `repository_selection` config + attribute; it was removed as redundant). When an app is named at the org + level, the installation is toggled to `all` and deltas for that app are + skipped. + +5. **Repository NAMES, not IDs.** The Enterprise Org Installations API accepts + names, so the plugin no longer resolves names → IDs or enumerates all repos + for the "all" case (it uses the native toggle instead). + +6. **Additions before removals (both delta and full sync).** The add/remove + sets are always disjoint before they are applied: in **delta** mode + `_buildAppChangesFromDelta` drops any repo appearing in both selection and + unselection (selection wins), and in **full sync** they are disjoint by + construction (`toAdd = desired − live`, `toRemove = live − desired`). Because + the sets are disjoint, ordering does not change the final repo set — so + additions are applied **first** in both paths. Adding first prevents a + "swap" (e.g. live `{A}` → desired `{B}`) from momentarily dropping a + `selected` installation to zero repositories, which the Enterprise API + rejects with `422`. + + A `selected` installation must retain at least one repository, so a change + that would drive one to **zero** is surfaced as an error rather than + attempted — but *only* when it would actually strip access while + safe-settings is managing it. Concretely, an empty desired/required repo set + is **not** universally an error: + - **Full sync, non-additive, installation would lose all repos** (currently + `all` and asked to narrow to none, or currently `selected` with live repos + and desired resolves to none) → **error**; the installation is left + unchanged. Full sync knows the complete desired set, so it detects this + up-front. + - **Additive mode** → never an error: additive never narrows or removes, so + an empty desired set simply adds nothing. + - **Nothing to reconcile** (installation already has no relevant repos) → + no-op, no error. + - **Org-level `all`** → not applicable; the app is toggled to `all` and the + empty case never arises. + - **Delta mode** → an empty incremental change is a no-op. Delta does not + fetch live state, so it cannot know up-front that a removal would empty the + installation; it instead catches the `422` on removal, emits a descriptive + error, and defers the correct end state to the next full sync. + + +7. **Churn skip.** In delta mode, if an app's targeting is unchanged between the + previous and current versions of a file, it is skipped entirely to avoid + redundant add/remove writes. + +8. **Full-sync `current_selection` awareness.** Full sync reads each + installation's live `repository_selection` and chooses the minimal action: + skip when already correct; toggle `all` ↔ `selected`; or diff names and + add-then-remove when already `selected` (see decision #6). In `additive` mode + it never narrows an `all` installation. + +9. **`disable_plugins` / `additive_plugins` support.** `app_installations` + participates in the same gating: it can be disabled at any layer, and in + additive mode it only adds, never removes. + +10. **Future target abstraction.** The plugin is structured around a target that + is *not* a repository, paving the way for future targets (e.g., Copilot + policies) to reuse the same phase/plumbing without being repo-bound. + +11. **`app_installations` is excluded from the per-repo plugin pipeline.** + Although it is registered in `Settings.PLUGINS` (so `disable_plugins` / + `additive_plugins` name-validation and suborg-cleanup detection recognize + it), `childPluginsList` explicitly skips it. The per-repo pipeline calls + `instance.sync()`, which `AppInstallations` does not implement (it exposes + `syncDelta` / `syncFull` and has a different constructor signature). It is + reconciled **only** through the dedicated `syncAppInstallations` phase. + +12. **App is the reporting subject.** See *Reporting* above — an additive + `NopCommand.subject` avoids a global rename of the repo-centric pipeline + while presenting app installation changes with the app as the subject and + keeping repo counts accurate. + +13. **Startup verification.** On boot, `index.js` runs + `verifyAppInstallationsPlugin`: when `GH_ENTERPRISE` is set it mints an + enterprise installation token and confirms it can list app installations in + the target org (`GH_ORG`), logging a clear success/failure. When + `GH_ENTERPRISE` is unset the check is skipped, so non-enterprise + deployments are unaffected. This surfaces a mis-scoped enterprise install + early rather than at first sync. + +14. **Only explicitly-named apps are ever touched.** Both full and delta sync + operate solely on apps that appear in an `app_installations` entry in some + config layer. `listOrgInstallations` is used only to resolve + `app_slug → installation_id`; it never seeds the desired state, and there is + no "remove apps not in config" sweep. Consequently the safe-settings app's + own installation (and every other unlisted app) is left untouched on every + sync unless a config explicitly names it. + +## Consequences + +### Positive + +- App access is now declarative and flows through the existing config hierarchy. +- Delta processing keeps incremental (push-triggered) runs cheap. +- Names-based API + native "all" toggle removes an entire class of ID-resolution + and enumeration work. +- Batching respects the 50-repo API limit transparently. +- App installation changes read clearly in PR comments (app as subject) without + reworking the repo-centric reporting pipeline or distorting repo counts. +- Unlisted apps — including safe-settings itself — are provably never modified, + so enabling the plugin cannot accidentally lock the app out of repositories. +- A startup self-check catches missing/mis-scoped enterprise permissions before + the first sync. +- Smoke coverage (`smoke-test.js` Phase 17) exercises org-level `all`, + repo-level selection, add/remove, drift remediation via full sync, and + sub-org (delta) targeting, restoring each app's original state on teardown. + +### Negative / limitations + +- **Managed-app drift relies on the scheduled full sync.** An app only receives + `installation` repository events for its *own* installation, so there is no + webhook that reports drift on *other* managed apps. The + `installation.repositories_added/removed` handler was intentionally **removed** + because it could not detect managed-app drift; only `installation_target` is + retained. Drift on managed apps is reconciled on the next cron full sync. +- **Multi-suborg overlap** in delta mode is handled by de-duplicating the + selection/unselection sets (selection wins) and applying additions before + removals, so the net end state is correct and a `selected` installation is + never momentarily emptied. A removal that would still drop the installation to + zero repos (which delta cannot detect without a live-state fetch) is caught as + a `422`, reported, and reconciled by the next full sync. +- Requires an enterprise-level installation with the specific permission; orgs + not on enterprise cannot use the plugin. + +## Alternatives considered + +- **Enumerate all repos and add them individually for the "all" case** — + rejected in favor of the API's native `repository_selection: all` toggle + (fewer calls, no drift from newly created repos). +- **Arbitrary Search API queries for repo selection** — rejected for now in + favor of a fixed, predictable criteria set. +- **Suborg exclusions overriding org "all"** — rejected; org "all" takes + precedence to keep the mental model simple. +- **A dedicated private-key env var for enterprise auth** — rejected in favor of + reusing the existing app credentials and treating enterprise installation as a + prerequisite. + +[ent-api]: https://docs.github.com/en/enterprise-cloud@latest/rest/enterprise-admin/organization-installations?apiVersion=2026-03-10 diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 000000000..03a952f8c --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,10 @@ +# Architecture Decision Records + +This directory captures significant architectural decisions for safe-settings +using lightweight [ADRs](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions). + +Each record is numbered and immutable once accepted; supersede rather than edit. + +| ADR | Title | Status | +| --- | --- | --- | +| [0001](0001-app-installation-plugin.md) | App installation management plugin | Accepted | diff --git a/docs/deploy.md b/docs/deploy.md index d61377bed..5962c9dd7 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -88,6 +88,9 @@ This will start the container in the background and detached. - `docker exec -it safe-settings /bin/sh` - You will now be inside the running **Docker** container and can perform any troubleshooting needed +### Troubleshooting Docker Build and Runtime Issues +For detailed guidance on debugging Docker image builds, runtime failures, and comparing local vs. GHCR images, see [docker-debugging.md](docker-debugging.md). + ## Deploy the app to AWS Lambda ### Production-Ready Template @@ -295,6 +298,7 @@ Every deployment will need an [App](https://developer.github.com/apps/). #### Organization Permissions - Administration: **Read & Write** +- Custom organization roles: **Read-only** - Custom properties: **Admin** - Members: **Read & Write** diff --git a/docs/docker-debugging.md b/docs/docker-debugging.md new file mode 100644 index 000000000..6a917a644 --- /dev/null +++ b/docs/docker-debugging.md @@ -0,0 +1,181 @@ + +# Docker Build and Test Debugging Runbook + +This document summarizes the Docker experiments done for this repo and converts them into a repeatable workflow for debugging local image builds and GHCR images. + +## Goals + +- Build and test the app image from local source. +- Compare behavior with the published image from GHCR. +- Quickly diagnose common failures (container startup, shell access, port exposure, env setup). + +## What We Learned From The Experiments + +- The image is Alpine-based, so use /bin/sh, not /bin/bash. +- Use docker rm (or docker container rm) to remove containers. Commands like docker delete, docker destroy, or docker remove do not exist. +- To pass host binding correctly, use HOST=0.0.0.0 (equal sign), not HOST:0.0.0.0. +- Port mapping is required for host access: -p 3000:3000. +- Supplying .env values is required for realistic startup testing. +- Testing both local and GHCR images with the same runtime flags makes behavior comparisons easier. + +## Prerequisites + +- Docker is installed and running. +- A valid .env file exists at repo root. +- You are in repo root. + +## 1) Build And Test Local Image + +Build the local image: + +```bash +docker build -t safe-settings:local . +``` + +Run container in foreground with explicit runtime env and port mapping: + +```bash +docker run --name safe-settings-local \ + --env-file ./.env \ + --env NODE_ENV=development \ + --env HOST=0.0.0.0 \ + -p 3000:3000 \ + -it safe-settings:local +``` + +If startup fails, inspect logs: + +```bash +docker logs safe-settings-local +``` + +Shell into running container for investigation: + +```bash +docker exec -it safe-settings-local /bin/sh +``` + +Clean up: + +```bash +docker rm -f safe-settings-local +``` + +## 2) Pull And Test GHCR Image + +Pull published image: + +```bash +docker pull ghcr.io/github/safe-settings:2.1.19 +``` + +Run with the same env and port flags used for local testing: + +```bash +docker run --name safe-settings-ghcr \ + --env-file ./.env \ + --env NODE_ENV=development \ + --env HOST=0.0.0.0 \ + -p 3000:3000 \ + -it ghcr.io/github/safe-settings:2.1.19 +``` + +Inspect logs: + +```bash +docker logs safe-settings-ghcr +``` + +Debug inside container: + +```bash +docker exec -it safe-settings-ghcr /bin/sh +``` + +Clean up: + +```bash +docker rm -f safe-settings-ghcr +``` + +## 3) Fast Differential Debug (Local vs GHCR) + +Use this when one image works and the other does not. + +1. Run both images with identical flags (env, HOST, port mapping). +2. Compare startup logs side-by-side. +3. Compare environment inside each container: + +```bash +docker exec -it safe-settings-local /bin/sh -c 'env | sort' +docker exec -it safe-settings-ghcr /bin/sh -c 'env | sort' +``` + +4. Confirm app process is listening on expected port inside container: + +```bash +docker exec -it safe-settings-local /bin/sh -c 'netstat -lntp 2>/dev/null || ss -lntp' +docker exec -it safe-settings-ghcr /bin/sh -c 'netstat -lntp 2>/dev/null || ss -lntp' +``` + +5. Validate host reachability: + +```bash +curl -i http://localhost:3000/ +``` + +## 4) Common Failure Patterns And Fixes + +Symptom: container exits immediately. +Likely causes: +- Missing required variables in .env. +- Invalid app credentials. +Checks: +- docker logs +- Confirm .env has required app settings. + +Symptom: cannot connect from host to localhost:3000. +Likely causes: +- Missing -p 3000:3000. +- App not binding to all interfaces. +Checks: +- Ensure HOST=0.0.0.0 is set. +- Ensure port mapping is present. + +Symptom: cannot shell into container with bash. +Likely cause: +- Alpine image does not include bash. +Fix: +- Use /bin/sh. + +Symptom: name conflict when re-running tests. +Likely cause: +- Old container still exists. +Fix: +- docker rm -f + +## 5) Minimal Known-Good Commands + +Local: + +```bash +docker build -t safe-settings:local . && \ +docker run --rm --name safe-settings-local \ + --env-file ./.env \ + --env NODE_ENV=development \ + --env HOST=0.0.0.0 \ + -p 3000:3000 \ + -it safe-settings:local +``` + +GHCR: + +```bash +docker pull ghcr.io/github/safe-settings:2.1.19 && \ +docker run --rm --name safe-settings-ghcr \ + --env-file ./.env \ + --env NODE_ENV=development \ + --env HOST=0.0.0.0 \ + -p 3000:3000 \ + -it ghcr.io/github/safe-settings:2.1.19 +``` diff --git a/docs/github-settings/1. repository-settings.md b/docs/github-settings/1. repository-settings.md index 4d7fc0785..46eaa64b9 100644 --- a/docs/github-settings/1. repository-settings.md +++ b/docs/github-settings/1. repository-settings.md @@ -50,8 +50,8 @@ repository: >[!TIP] >GitHub's API documentation defines these inputs and types: ->1. [Update an environment](https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#update-a-repository) ->2. [Replace all repository topics](https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#replace-all-repository-topics) +>1. [Update an environment](https://docs.github.com/en/rest/repos/repos?apiVersion=2026-03-10#update-a-repository) +>2. [Replace all repository topics](https://docs.github.com/en/rest/repos/repos?apiVersion=2026-03-10#replace-all-repository-topics)
diff --git a/docs/github-settings/2. repository-variables.md b/docs/github-settings/2. repository-variables.md index 5a9871b70..8d6d733f6 100644 --- a/docs/github-settings/2. repository-variables.md +++ b/docs/github-settings/2. repository-variables.md @@ -18,7 +18,7 @@ variables: >[!TIP] >GitHub's API documentation defines these inputs and types: ->1. [Update a repository variable](https://docs.github.com/en/rest/actions/variables?apiVersion=2022-11-28#update-a-repository-variable) +>1. [Update a repository variable](https://docs.github.com/en/rest/actions/variables?apiVersion=2026-03-10#update-a-repository-variable)
diff --git a/docs/github-settings/3. collaborators.md b/docs/github-settings/3. collaborators.md index bbc971c14..56a0ac595 100644 --- a/docs/github-settings/3. collaborators.md +++ b/docs/github-settings/3. collaborators.md @@ -20,8 +20,8 @@ collaborators: >[!TIP] >GitHub's API documentation defines these inputs and types: ->1. [Add a repository collaborator](https://docs.github.com/en/rest/collaborators/collaborators?apiVersion=2022-11-28#add-a-repository-collaborator) ->2. [Remove a repository collaborator](https://docs.github.com/en/rest/collaborators/collaborators?apiVersion=2022-11-28#remove-a-repository-collaborator) +>1. [Add a repository collaborator](https://docs.github.com/en/rest/collaborators/collaborators?apiVersion=2026-03-10#add-a-repository-collaborator) +>2. [Remove a repository collaborator](https://docs.github.com/en/rest/collaborators/collaborators?apiVersion=2026-03-10#remove-a-repository-collaborator)
diff --git a/docs/github-settings/4. teams.md b/docs/github-settings/4. teams.md index 496b30a32..d65bd1611 100644 --- a/docs/github-settings/4. teams.md +++ b/docs/github-settings/4. teams.md @@ -20,7 +20,7 @@ teams: >[!TIP] >GitHub's API documentation defines these inputs and types: ->1. [Add or update team repository permissions](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#add-or-update-team-repository-permissions) +>1. [Add or update team repository permissions](https://docs.github.com/en/rest/teams/teams?apiVersion=2026-03-10#add-or-update-team-repository-permissions) + + +
@@ -38,7 +38,7 @@ teams:

permissionstring

-

The permission to grant the team on this repository. We accept the following permissions to be set: pull, triage, push, maintain, admin and you can also specify a custom repository role name, if the owning organization has defined any. If no permission is specified, the team's permission attribute will be used to determine what permission to grant the team on this repository.

+

The permission to grant the team on this repository. We accept the following permissions to be set: pull, triage, push, maintain, admin and you can also specify a custom repository role name, if the owning organization has defined any.

Default: push

@@ -48,5 +48,46 @@ teams: permission: maintain ``` +
+

external_groupstring

+

Optional. The display name of an external IdP group (as listed under your organization's external groups) to link to the team. safe-settings looks up the group's id by display name via GET /orgs/{org}/external-groups and links the team via PATCH /orgs/{org}/teams/{team_slug}/external-groups. The link is reconciled on every sync and is idempotent (it skips the PATCH when the team is already linked to the same group). The external-groups list is fetched at most once per org per sync, only when at least one team entry uses this property. If the named group does not exist for the org, an error is logged and the team-repo association still applies.

+
+ +```yaml +teams: + - name: expert-services-developers + permission: push + external_group: "Engineering - Expert Services" +``` + +
+

excludearray

+

Exclude a list of repos for this team. The team is applied to every repo in scope except those whose names match one of these glob patterns.

+
+ +```yaml +teams: + - name: SuperFriends + permission: maintain + exclude: + - secret-repo +``` + +
+

includearray

+

Include a list of repos for this team. The team is applied only to repos whose names match one of these glob patterns.

+
+ +```yaml +teams: + - name: SuperFriends + permission: maintain + include: + - public-* +``` +
diff --git a/docs/github-settings/5. branch-protection.md b/docs/github-settings/5. branch-protection.md index 4874a1d79..7a235dffe 100644 --- a/docs/github-settings/5. branch-protection.md +++ b/docs/github-settings/5. branch-protection.md @@ -55,7 +55,7 @@ branches: >[!TIP] >GitHub's API documentation defines these inputs and types: ->1. [Update a repository variable](https://docs.github.com/en/rest/actions/variables?apiVersion=2022-11-28#update-a-repository-variable) +>1. [Update branch protection](https://docs.github.com/en/rest/branches/branch-protection?apiVersion=2026-03-10#update-branch-protection)
diff --git a/docs/github-settings/6. deployment-environments.md b/docs/github-settings/6. deployment-environments.md index c4705b33b..87c9e6b81 100644 --- a/docs/github-settings/6. deployment-environments.md +++ b/docs/github-settings/6. deployment-environments.md @@ -46,9 +46,9 @@ environments: >[!TIP] >GitHub's API documentation defines these inputs and types: ->1. [Create or update an environment](https://docs.github.com/en/rest/deployments/environments?apiVersion=2022-11-28#create-or-update-an-environment) ->2. [Create a deployment branch policy](https://docs.github.com/en/rest/deployments/branch-policies?apiVersion=2022-11-28#create-a-deployment-branch-policy) ->3. [Create an environment variable](https://docs.github.com/en/rest/actions/variables?apiVersion=2022-11-28#create-an-environment-variable) +>1. [Create or update an environment](https://docs.github.com/en/rest/deployments/environments?apiVersion=2026-03-10#create-or-update-an-environment) +>2. [Create a deployment branch policy](https://docs.github.com/en/rest/deployments/branch-policies?apiVersion=2026-03-10#create-a-deployment-branch-policy) +>3. [Create an environment variable](https://docs.github.com/en/rest/actions/variables?apiVersion=2026-03-10#create-an-environment-variable)
diff --git a/docs/github-settings/7. autolinks.md b/docs/github-settings/7. autolinks.md index 9ae3c19f7..26b5143ca 100644 --- a/docs/github-settings/7. autolinks.md +++ b/docs/github-settings/7. autolinks.md @@ -20,7 +20,7 @@ variables: >[!TIP] >GitHub's API documentation defines these inputs and types: ->1. [Create an autolink reference for a repository](https://docs.github.com/en/rest/repos/autolinks?apiVersion=2022-11-28#create-an-autolink-reference-for-a-repository) +>1. [Create an autolink reference for a repository](https://docs.github.com/en/rest/repos/autolinks?apiVersion=2026-03-10#create-an-autolink-reference-for-a-repository)
diff --git a/docs/github-settings/8. labels.md b/docs/github-settings/8. labels.md index ddf4c4b43..cee530f47 100644 --- a/docs/github-settings/8. labels.md +++ b/docs/github-settings/8. labels.md @@ -19,9 +19,9 @@ labels: >[!TIP] >GitHub's API documentation defines these inputs and types: ->1. [Create a label](https://docs.github.com/en/rest/issues/labels?apiVersion=2022-11-28#create-a-label) ->2. [Update a label](https://docs.github.com/en/rest/issues/labels?apiVersion=2022-11-28#update-a-label) ->3. [Delete a label](https://docs.github.com/en/rest/issues/labels?apiVersion=2022-11-28#delete-a-label) +>1. [Create a label](https://docs.github.com/en/rest/issues/labels?apiVersion=2026-03-10#create-a-label) +>2. [Update a label](https://docs.github.com/en/rest/issues/labels?apiVersion=2026-03-10#update-a-label) +>3. [Delete a label](https://docs.github.com/en/rest/issues/labels?apiVersion=2026-03-10#delete-a-label)
diff --git a/docs/sample-settings/sample-deployment-settings.yml b/docs/sample-settings/sample-deployment-settings.yml index 6164d4389..87beeafd3 100644 --- a/docs/sample-settings/sample-deployment-settings.yml +++ b/docs/sample-settings/sample-deployment-settings.yml @@ -27,10 +27,10 @@ overridevalidators: error: | `Branch protection required_approving_review_count cannot be overidden to a lower value` script: | - console.log(`baseConfig ${JSON.stringify(baseconfig)}`) - console.log(`overrideConfig ${JSON.stringify(overrideconfig)}`) - if (baseconfig.protection.required_pull_request_reviews.required_approving_review_count && overrideconfig.protection.required_pull_request_reviews.required_approving_review_count ) { - return overrideconfig.protection.required_pull_request_reviews.required_approving_review_count >= baseconfig.protection.required_pull_request_reviews.required_approving_review_count + const baseCount = baseconfig?.protection?.required_pull_request_reviews?.required_approving_review_count + const overrideCount = overrideconfig?.protection?.required_pull_request_reviews?.required_approving_review_count + if (baseCount && overrideCount) { + return overrideCount >= baseCount } return true - plugin: labels @@ -38,3 +38,14 @@ overridevalidators: Some error script: | return true + +# disable_plugins (optional) — disable safe-settings plugins at the deployment layer. +# Each entry is either a plugin name (shorthand for target: all) or { plugin, target }. +# target is one of: self | children | all (default: all). +# Declared here, target: all strips the plugin from every level below for every repo. +# See docs/README.md ("Disabling plugins") for the full strip matrix and limitations. +# +# disable_plugins: +# - plugin: rulesets # disables rulesets everywhere +# target: all +# - milestones # shorthand → { plugin: milestones, target: all } diff --git a/docs/sample-settings/settings.yml b/docs/sample-settings/settings.yml index 7e19d3354..edc2b2dcb 100644 --- a/docs/sample-settings/settings.yml +++ b/docs/sample-settings/settings.yml @@ -1,7 +1,7 @@ # This settings file can be used to create org-level settings # This is the settings that need to be applied to all repositories in the org -# See https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#create-an-organization-repository for all available settings for a repository +# See https://docs.github.com/en/rest/repos/repos?apiVersion=2026-03-10#create-an-organization-repository for all available settings for a repository repository: # A short description of the repository that will show up on GitHub description: description of the repo @@ -123,7 +123,7 @@ milestones: state: open # Collaborators: give specific users access to any repository. -# See https://docs.github.com/en/rest/collaborators/collaborators?apiVersion=2022-11-28#add-a-repository-collaborator for available options +# See https://docs.github.com/en/rest/collaborators/collaborators?apiVersion=2026-03-10#add-a-repository-collaborator for available options collaborators: - username: regpaco # The permission to grant the collaborator. Can be one of: @@ -144,7 +144,7 @@ collaborators: - another-repo # Teams -# See https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#create-a-team for available options +# See https://docs.github.com/en/rest/teams/teams?apiVersion=2026-03-10#create-a-team for available options teams: - name: core # The permission to grant the team. Can be one of: @@ -161,9 +161,19 @@ teams: - name: globalteam permission: push visibility: closed + - name: docs-team + permission: pull + # You can include a list of repos for this team and only those repos would have this team + include: + - actions-demo + - name: ops-team + permission: push + # You can exclude a list of repos for this team and all repos except these repos would have this team + exclude: + - actions-demo # Branch protection rules -# See https://docs.github.com/en/rest/branches/branch-protection?apiVersion=2022-11-28#update-branch-protection for available options +# See https://docs.github.com/en/rest/branches/branch-protection?apiVersion=2026-03-10#update-branch-protection for available options branches: # If the name of the branch value is specified as `default`, then the app will create a branch protection rule to apply against the default branch in the repo - name: default @@ -202,7 +212,7 @@ branches: teams: [] # Custom properties -# See https://docs.github.com/en/rest/repos/custom-properties?apiVersion=2022-11-28 +# See https://docs.github.com/en/rest/repos/custom-properties?apiVersion=2026-03-10 custom_properties: - name: test value: test @@ -221,7 +231,7 @@ validator: pattern: "[a-zA-Z0-9_-]+" # Rulesets -# See https://docs.github.com/en/rest/orgs/rules?apiVersion=2022-11-28#create-an-organization-repository-rulesetfor available options +# See https://docs.github.com/en/rest/orgs/rules?apiVersion=2026-03-10#create-an-organization-repository-rulesetfor available options rulesets: - name: Template # The target of the ruleset. Can be one of: @@ -262,6 +272,23 @@ rulesets: actor_type: Integration bypass_mode: always + # Instead of looking up numeric ids, you can use the `name` field to + # reference an actor by name. safe-settings resolves it to `actor_id` + # based on `actor_type` before applying the ruleset: + # - Team -> team slug + # - User -> username + # - Integration -> GitHub App slug + # - RepositoryRole -> role name (built-in: read, triage, write, + # maintain, admin; or a custom role name) + # Provide either `name` or `actor_id`, not both. + - name: my-team + actor_type: Team + bypass_mode: always + + - name: admin + actor_type: RepositoryRole + bypass_mode: always + conditions: # Parameters for a repository ruleset ref name condition ref_name: @@ -322,6 +349,17 @@ rulesets: # All conversations on code must be resolved before a pull # request can be merged. required_review_thread_resolution: true + # A collection of reviewers and the file patterns they must + # approve. Each reviewer is a team. Use `id` (team id) or, to + # avoid looking up the id, `slug` (team slug) which + # safe-settings resolves before applying the ruleset. Provide + # either `slug` or `id`, not both. + required_reviewers: + - minimum_approvals: 1 + file_patterns: ["*.js"] + reviewer: + slug: my-reviewers-team + type: Team # Choose which status checks must pass before branches can be merged # into a branch that matches this rule. When enabled, commits must @@ -397,3 +435,34 @@ rulesets: negate: false operator: regex pattern: ".*\/.*" + +# disable_plugins (optional) — disable safe-settings plugins at the org layer. +# Declared here: +# - target: self → strips from the org layer only (affects org-level runs: +# rulesets, custom_repository_roles). +# - target: children → strips from suborg + repo layers (per-repo runs). +# - target: all → strips from org + suborg + repo layers. +# Lower levels can never undo a strip declared at a higher level (union-only cascade). +# See docs/README.md ("Disabling plugins") for the full strip matrix. +# +# disable_plugins: +# - plugin: custom_repository_roles +# target: self +# - branches # shorthand → { plugin: branches, target: all } + +# additive_plugins (optional) — run selected Diffable plugins in additive mode. +# In additive mode a plugin will only add and update entries; it will never +# call remove(). Items that exist on GitHub but are absent from the YAML are +# preserved. This is useful when you want safe-settings to enforce a baseline +# of settings while still allowing teams to manage their own extra labels, +# teams, environments, etc. +# +# Supported plugins (must extend Diffable): +# labels, collaborators, teams, milestones, autolinks, environments, +# custom_properties, variables, rulesets, custom_repository_roles +# +# NOT supported (non-Diffable): repository, archive, branches, validator +# +# additive_plugins: +# - labels # never delete labels not in YAML +# - collaborators # never remove collaborators not in YAML diff --git a/docs/sample-settings/suborg.yml b/docs/sample-settings/suborg.yml index a509847cc..42e822993 100644 --- a/docs/sample-settings/suborg.yml +++ b/docs/sample-settings/suborg.yml @@ -14,3 +14,15 @@ suborgproperties: - EDP: true # Every other property is the same as the org level settings and can be overridden here + +# disable_plugins (optional) — disable safe-settings plugins for repos matched +# by this suborg. Declared here, target values mean: +# - self → strip from the suborg layer only. +# - children → strip from the repo layer for matched repos. +# - all → strip from suborg + repo layers for matched repos. +# Note: a suborg-level disable cannot strip config defined at the org layer. +# See docs/README.md ("Disabling plugins") for details. +# +# disable_plugins: +# - plugin: labels +# target: all diff --git a/docs/whitepaper-deploying-policies-at-scale.md b/docs/whitepaper-deploying-policies-at-scale.md new file mode 100644 index 000000000..406236b7a --- /dev/null +++ b/docs/whitepaper-deploying-policies-at-scale.md @@ -0,0 +1,936 @@ +# Deploying Policies at Scale Across Organizations Using GitHub Safe-Settings + +## A White Paper on Policy-as-Code for GitHub Enterprise Governance + +--- + +**Version:** 1.0 +**Date:** May 2026 +**Author:** GitHub Safe-Settings Team + +--- + +## Table of Contents + +1. [Executive Summary](#executive-summary) +2. [The Challenge: Governing Repositories at Scale](#the-challenge-governing-repositories-at-scale) +3. [Introducing Safe-Settings: Policy-as-Code for GitHub](#introducing-safe-settings-policy-as-code-for-github) +4. [Architecture Overview](#architecture-overview) +5. [The Configuration Hierarchy](#the-configuration-hierarchy) +6. [Designing Your Policy Framework](#designing-your-policy-framework) +7. [Deployment Models](#deployment-models) +8. [Scaling Strategies](#scaling-strategies) +9. [Governance Workflows](#governance-workflows) +10. [Advanced Policy Controls](#advanced-policy-controls) +11. [Drift Detection and Remediation](#drift-detection-and-remediation) +12. [Multi-Organization Deployments](#multi-organization-deployments) +13. [Security Considerations](#security-considerations) +14. [Case Study: Enterprise Rollout](#case-study-enterprise-rollout) +15. [Best Practices](#best-practices) +16. [Conclusion](#conclusion) + +--- + +## Executive Summary + +As organizations scale their software delivery practices on GitHub, managing repository configurations consistently across hundreds or thousands of repositories becomes a critical governance challenge. Manual configuration is error-prone, difficult to audit, and impossible to enforce at scale. + +**GitHub Safe-Settings** provides a policy-as-code solution that enables organizations to centrally define, enforce, and audit repository settings across an entire GitHub organization. By storing configuration as YAML in a centralized admin repository, Safe-Settings brings the principles of Infrastructure-as-Code to GitHub governance — enabling version control, peer review, automated validation, and continuous enforcement of organizational policies. + +This white paper provides a comprehensive guide to deploying Safe-Settings at enterprise scale, covering architecture decisions, policy design patterns, scaling strategies, and operational best practices. + +--- + +## The Challenge: Governing Repositories at Scale + +### The Problem + +Enterprise organizations on GitHub commonly face these governance challenges: + +- **Configuration sprawl**: Thousands of repositories with inconsistent settings — varying branch protections, team permissions, security configurations, and compliance controls. +- **Manual drift**: Repository administrators making ad-hoc changes that deviate from organizational standards, often without audit trails. +- **Onboarding delays**: New repositories require manual setup of branch protections, team access, labels, and compliance configurations. +- **Audit burden**: Demonstrating compliance with internal security policies or regulatory requirements (SOC 2, FedRAMP, HIPAA) demands evidence that every repository meets baseline standards. +- **Decentralized ownership**: Different teams need autonomy to manage their project-specific settings while still adhering to organization-wide baselines. + +### Why Existing Approaches Fall Short + +| Approach | Limitation | +|----------|-----------| +| **Manual configuration** | Does not scale; no audit trail; prone to drift | +| **GitHub repository templates** | Only applies at creation time; no ongoing enforcement | +| **Custom scripts/APIs** | High maintenance; fragile; no built-in review workflow | +| **Per-repo settings files** | Settings files live in individual repos, meaning any contributor can bypass policies | + +Safe-Settings addresses all of these limitations by centralizing policy definitions in a protected admin repository, enforcing them continuously, and providing a pull request-based review workflow for all changes. + +--- + +## Introducing Safe-Settings: Policy-as-Code for GitHub + +Safe-Settings is a GitHub App built on the [Probot](https://probot.github.io/) framework that implements policy-as-code for GitHub organizations. It operates on three foundational principles: + +### 1. Centralized Configuration + +All settings are stored in a single `admin` repository (configurable via the `ADMIN_REPO` environment variable). Unlike per-repo settings files, this prevents repository maintainers from overriding organizational policies. + +### 2. Hierarchical Policy Model + +Settings are defined at three levels with a clear precedence order: + +``` +Organization (baseline) → Sub-Organization (team/project overrides) → Repository (specific exceptions) +``` + +Higher-specificity levels override lower ones, enabling a flexible yet governed configuration model. + +### 3. Continuous Enforcement + +Safe-Settings responds to webhook events in real-time and can run on a configurable schedule (via cron) to detect and remediate configuration drift — ensuring that manual changes are automatically reverted to the declared policy state. + +### What Can Be Managed + +Safe-Settings supports a comprehensive set of GitHub configurations: + +| Category | Capabilities | +|----------|-------------| +| **Repository Settings** | Visibility, description, homepage, merge strategies, wiki, issues, projects, default branch, auto-init, security settings | +| **Branch Protections** | Required reviews, status checks, admin enforcement, push restrictions, dismiss stale reviews, code owner reviews | +| **Rulesets** | Organization and repository-level rulesets with branch/tag targeting, bypass actors, pattern rules, required workflows | +| **Teams & Collaborators** | Team permissions, collaborator access with include/exclude patterns | +| **Labels & Milestones** | Standardized issue labels and milestone definitions | +| **Custom Properties** | Organization-defined custom property values for repositories | +| **Environments** | Deployment environments with protection rules, wait timers, reviewers, and environment variables | +| **Autolinks** | External reference linking (e.g., Jira ticket prefixes) | +| **Repository Naming** | Regex-based validation of repository names | +| **Custom Repository Roles** | Organization-level custom roles | +| **Variables** | Repository and environment variables | + +--- + +## Architecture Overview + +### System Components + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ GitHub Platform │ +│ │ +│ ┌──────────┐ ┌──────────────┐ ┌──────────────────────┐ │ +│ │ Webhooks │ │ Admin Repo │ │ Target Repositories │ │ +│ │ (Events) │ │ (Policies) │ │ (1000s of repos) │ │ +│ └─────┬─────┘ └──────┬───────┘ └──────────┬───────────┘ │ +│ │ │ │ │ +└────────┼────────────────┼──────────────────────┼────────────────┘ + │ │ │ + ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Safe-Settings App │ +│ │ +│ ┌──────────────┐ ┌───────────────┐ ┌──────────────────┐ │ +│ │ Event Handler │ │ Config Merger │ │ Plugin Engine │ │ +│ │ (Webhooks) │ │ (Hierarchy) │ │ (API Calls) │ │ +│ └──────────────┘ └───────────────┘ └──────────────────┘ │ +│ │ +│ ┌──────────────┐ ┌───────────────┐ ┌──────────────────┐ │ +│ │ Drift Detect │ │ Validators │ │ Diff Engine │ │ +│ │ (Cron Sync) │ │ (Rules) │ │ (Smart Compare) │ │ +│ └──────────────┘ └───────────────┘ └──────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Event Processing Flow + +Safe-Settings listens to the following webhook events and responds accordingly: + +| Event | Action | +|-------|--------| +| `push` to admin repo (default branch) | Apply changed settings to affected repositories | +| `repository.created` | Apply full policy stack (org → suborg → repo) to the new repository | +| `repository.edited` | Sync settings to prevent unauthorized changes | +| `repository.renamed` | Optionally block human-initiated renames; sync config files for bot-initiated renames | +| `branch_protection_rule` | Revert unauthorized branch protection changes | +| `repository_ruleset` | Revert unauthorized ruleset modifications | +| `member` / `team` changes | Revert unauthorized collaborator or team permission changes | +| `custom_property_values` | Re-evaluate suborg membership and apply matching policies | +| `pull_request` (to admin repo) | Run in dry-run/NOP mode and report validation results as check runs | + +### Smart Diff Engine + +Safe-Settings does not blindly apply configuration on every event. It performs an intelligent comparison between the declared policy and the current GitHub state, generating a precise diff of `additions`, `modifications`, and `deletions`. API calls are made only when real changes exist, which is critical for performance at scale. + +--- + +## The Configuration Hierarchy + +### Directory Structure + +All policy files reside in the admin repository under the `.github` directory: + +``` +admin-repo/ +├── .github/ +│ ├── settings.yml # Organization-wide baseline policies +│ ├── suborgs/ # Sub-organization policies +│ │ ├── platform-team.yml # Policies for platform team repos +│ │ ├── frontend-team.yml # Policies for frontend team repos +│ │ ├── compliance-critical.yml # Policies for compliance-critical repos +│ │ └── open-source.yml # Policies for open-source repos +│ └── repos/ # Repository-specific overrides +│ ├── api-gateway.yml # Specific settings for api-gateway +│ ├── auth-service.yml # Specific settings for auth-service +│ └── docs-site.yml # Specific settings for docs-site +├── CODEOWNERS # Governs who can approve policy changes +└── deployment-settings.yml # Runtime configuration for the app +``` + +### Precedence Order + +``` +Repository-specific > Sub-Organization > Organization +``` + +When Safe-Settings computes the effective configuration for a given repository, it: + +1. Starts with the **organization-level** settings from `settings.yml` +2. Overlays any matching **sub-organization** settings +3. Overlays any **repository-specific** settings + +This layered approach means that organization-wide baselines are always applied, but teams can customize settings within the bounds defined by validators. + +### Sub-Organization Membership + +Sub-organizations ("suborgs") are a powerful abstraction for grouping repositories. A repository can belong to a suborg based on three criteria: + +| Criterion | Configuration Key | Example | +|-----------|------------------|---------| +| **Repository name pattern** | `suborgrepos` | `suborgrepos: ["api-*", "service-*"]` | +| **Team membership** | `suborgteams` | `suborgteams: ["platform-core"]` | +| **Custom property values** | `suborgproperties` | `suborgproperties: [{"compliance": "sox"}]` | + +This flexibility enables policies to be applied based on organizational structure, project taxonomy, or compliance classification — all without hard-coding repository lists. + +--- + +## Designing Your Policy Framework + +### Step 1: Define Your Organization Baseline + +> **⚠️ Scaling Best Practice: Keep `settings.yml` Minimal** +> +> Any change to the org-level `settings.yml` triggers Safe-Settings to process **every managed repository** in the organization. For orgs with thousands of repos, this can cascade into thousands of API calls and risk breaching GitHub's API rate limits within the 1-hour token lifetime. +> +> **Recommended approach:** Limit `settings.yml` to resources that are applied at the **org level** — specifically **org-level rulesets** and **custom repository roles**. These are managed via org-scoped API endpoints and do **not** require per-repo API calls. +> +> Move repo-scoped settings (repository configuration, teams, collaborators, labels, branch protections, etc.) to **suborg-level** files. This way, changes only affect the subset of repos matched by each suborg, keeping API call volume manageable and predictable. + +Define your organization-wide baseline using org-level rulesets and custom roles: + +```yaml +# .github/settings.yml — Organization baseline +# Keep this file minimal: only org-level rulesets and custom roles. +# Repo-scoped settings (teams, labels, repository config) belong in suborgs. + +rulesets: + - name: Branch Protection + target: branch + enforcement: active + bypass_actors: + - actor_id: 1 + actor_type: OrganizationAdmin + bypass_mode: always + conditions: + ref_name: + include: ["~DEFAULT_BRANCH"] + exclude: [] + repository_name: + include: ["~ALL"] + exclude: [] + rules: + - type: pull_request + parameters: + dismiss_stale_reviews_on_push: true + require_code_owner_review: true + require_last_push_approval: true + required_approving_review_count: 1 + required_review_thread_resolution: true + - type: required_status_checks + parameters: + strict_required_status_checks_policy: true + required_status_checks: [] + + - name: Branch Integrity + target: branch + enforcement: active + bypass_actors: + - actor_id: 1 + actor_type: OrganizationAdmin + bypass_mode: always + conditions: + ref_name: + include: ["~DEFAULT_BRANCH"] + exclude: [] + repository_name: + include: ["~ALL"] + exclude: [] + rules: + - type: deletion + - type: non_fast_forward + - type: required_linear_history + - type: required_signatures +``` + +Then define repo-scoped baseline settings at the **suborg level** to avoid cascading org-wide API calls. Use a broad suborg definition (e.g., `~ALL` repos or a wildcard pattern) to achieve org-wide coverage without the scaling risks: + +```yaml +# .github/suborgs/baseline.yml — Default repo-scoped settings for all repos +# Changes here only trigger API calls for matched repos, not the entire org. + +suborgrepos: + - "*" + +repository: + private: true + allow_auto_merge: false + delete_branch_on_merge: true + allow_update_branch: true + security: + enableVulnerabilityAlerts: true + enableAutomatedSecurityFixes: true + +teams: + - name: security-team + permission: admin + - name: all-engineers + permission: push + +labels: + - name: bug + color: "d73a4a" + description: "Something isn't working" + - name: security + color: "e11d48" + description: "Security-related issue" + - name: compliance + color: "7c3aed" + description: "Compliance-related" + +validator: + pattern: "[a-z0-9]+(-[a-z0-9]+)*" +``` + +### Step 2: Define Sub-Organization Policies + +Create suborg files for teams or projects that need additional or different policies: + +```yaml +# .github/suborgs/compliance-critical.yml + +# Repos with the "compliance" custom property set to "sox" +suborgproperties: + - compliance: sox + +# Stricter branch protections for SOX-compliant repositories +branches: + - name: default + protection: + required_pull_request_reviews: + required_approving_review_count: 2 + dismiss_stale_reviews: true + require_code_owner_reviews: true + require_last_push_approval: true + enforce_admins: true + +# Additional team access for compliance repos +teams: + - name: compliance-auditors + permission: pull +``` + +```yaml +# .github/suborgs/open-source.yml + +suborgrepos: + - "oss-*" + +repository: + private: false + visibility: public + has_wiki: true + +# Public repos need different branch protections +branches: + - name: default + protection: + required_pull_request_reviews: + required_approving_review_count: 2 + require_code_owner_reviews: true +``` + +### Step 3: Define Repository-Specific Overrides + +For repositories that need unique configurations: + +```yaml +# .github/repos/api-gateway.yml + +repository: + description: "Central API gateway for all microservices" + homepage: "https://api-docs.example.com" + topics: + - api + - gateway + - critical-infrastructure + +branches: + - name: default + protection: + required_status_checks: + strict: true + contexts: + - "ci/build" + - "ci/integration-tests" + - "security/codeql" + +environments: + - name: production + wait_timer: 30 + prevent_self_review: true + reviewers: + - type: Team + id: 12345 # platform-leads team + deployment_branch_policy: + protected_branches: true + custom_branch_policies: false +``` + +--- + +## Deployment Models + +Safe-Settings supports multiple deployment architectures to fit your infrastructure requirements. + +### Docker (Recommended for Most Organizations) + +Best for organizations with existing container infrastructure. + +```bash +# Build and run +docker build -t safe-settings . +docker run -d -p 3000:3000 --env-file .env safe-settings + +# Or with docker-compose +docker-compose --env-file .env up -d +``` + +**Advantages:** Simple, portable, works with any container orchestration platform. + +### Kubernetes with Helm + +Best for organizations running Kubernetes clusters. + +```bash +# Install using the official Helm chart +helm install safe-settings \ + oci://ghcr.io/github/helm-charts/safe-settings \ + --values myvalues.yaml +``` + +**Advantages:** Native Kubernetes integration, auto-scaling, rolling updates, health checks, secrets management via Kubernetes Secrets or external secret stores. + +### AWS Lambda (Serverless) + +Best for organizations wanting minimal infrastructure overhead. + +Use the [SafeSettings-Template](https://github.com/bheemreddy181/SafeSettings-Template) for a production-ready deployment featuring: + +- Docker-based Lambda functions +- Dual Lambda architecture (webhook handler + scheduled sync) +- GitHub Actions CI/CD pipeline +- Auto-scaling with pay-per-execution pricing + +### GitHub Actions + +Best for organizations that want to avoid deploying infrastructure entirely. + +Safe-Settings can be run as a GitHub Action, triggered by workflow dispatch or on a schedule. See the [GitHub Actions Guide](github-action.md) for configuration details. + +### Deployment Comparison + +| Model | Scalability | Operational Overhead | Real-Time Events | Scheduled Sync | Cost Model | +|-------|-------------|---------------------|-------------------|----------------|------------| +| **Docker** | Medium | Medium | ✅ Webhooks | ✅ CRON | Fixed | +| **Kubernetes** | High | Medium-High | ✅ Webhooks | ✅ CRON | Fixed | +| **AWS Lambda** | Very High | Low | ✅ Webhooks | ✅ EventBridge | Pay-per-use | +| **GitHub Actions** | Medium | Very Low | ❌ Polling only | ✅ Cron triggers | Actions minutes | + +--- + +## Scaling Strategies + +### Performance Considerations + +When managing thousands of repositories, Safe-Settings employs several strategies to operate within constraints: + +1. **Org-level settings are org-scoped**: Rulesets and custom repository roles defined in `settings.yml` are applied via org-level API endpoints — they do **not** generate per-repo API calls. This is why `settings.yml` should be reserved for these resources only. + +2. **Selective configuration loading**: Only repo-specific YAML files relevant to the changed settings are loaded — not the entire `.github/repos/` directory. Full loading occurs only for global settings changes. + +3. **Smart diff comparisons**: Before making any API call, Safe-Settings compares the desired state with the current GitHub state. API calls are only made when real changes are detected. + +4. **Rate limit handling**: Built on Probot, the app automatically handles GitHub API rate limits and abuse limits with exponential backoff. + +5. **Token lifetime awareness**: GitHub App installation tokens expire after 1 hour. Safe-Settings is designed to complete all work within this window. + +### Configuration for Large Organizations + +For organizations with 1,000+ repositories, consider these configurations: + +```env +# Run scheduled sync during off-peak hours +CRON=0 2 * * * + +# Set appropriate log level for production +LOG_LEVEL=info + +# Enable PR comments for audit trail +ENABLE_PR_COMMENT=true + +# Block manual repo renames to maintain config consistency +BLOCK_REPO_RENAME_BY_HUMAN=true +``` + +### Restricting Scope + +Use `deployment-settings.yml` to control which repositories Safe-Settings manages: + +```yaml +# deployment-settings.yml + +restrictedRepos: + include: + - "service-*" + - "lib-*" + - "infra-*" + exclude: + - admin + - .github + - safe-settings + - "test-*" + - "sandbox-*" +``` + +This is particularly useful during phased rollouts — start with a subset of repositories and expand as confidence grows. + +--- + +## Governance Workflows + +### Pull Request-Based Policy Changes + +All policy changes follow a pull request workflow, providing: + +1. **Version control**: Every change to organizational policies is tracked in Git history. +2. **Peer review**: Changes must be approved before taking effect. +3. **Dry-run validation**: Safe-Settings runs in NOP (no-operation) mode on PRs, producing a detailed report of what would change across all affected repositories. +4. **Check runs**: PR checks pass or fail based on dry-run results, including custom validator outcomes. + +### CODEOWNERS for Policy Governance + +Use GitHub's CODEOWNERS file in the admin repo to establish approval requirements: + +``` +# CODEOWNERS in admin repo + +# Security team must approve all policy changes +.github/settings.yml @org/security-team @org/platform-leads + +# Team leads approve their suborg policies +.github/suborgs/platform-team.yml @org/platform-leads +.github/suborgs/frontend-team.yml @org/frontend-leads +.github/suborgs/compliance-critical.yml @org/compliance-team @org/security-team + +# Repo owners can manage their repo-specific settings +.github/repos/api-gateway.yml @org/api-team +.github/repos/auth-service.yml @org/identity-team + +# Deployment settings require platform admin approval +deployment-settings.yml @org/platform-admins +``` + +This enables **delegated governance**: teams can manage their own settings within the guardrails established by the organization baseline and custom validators. + +### Change Review Workflow + +``` +Developer Admin Repo Safe-Settings GitHub + │ │ │ │ + ├─ Create branch ──►│ │ │ + ├─ Modify YAML ────►│ │ │ + ├─ Open PR ────────►│ │ │ + │ ├─ Webhook ───────────►│ │ + │ │ ├─ Dry-run ──────────┤ + │ │ ├─ Validate rules ───┤ + │ │ ├─ Report results ──►│ + │ │ │ │ + │◄──── Review PR with check results ──────────────────────────-│ + │ │ │ │ + ├─ Merge PR ───────►│ │ │ + │ ├─ Push webhook ──────►│ │ + │ │ ├─ Apply settings ──►│ + │ │ ├─ Create check ────►│ + │ │ │ │ +``` + +--- + +## Advanced Policy Controls + +### Custom Configuration Validators + +Validators allow you to define rules that settings must satisfy before they can be applied. They are defined in `deployment-settings.yml`. + +#### Config Validators + +Validate a setting in isolation: + +```yaml +configvalidators: + # Prevent granting admin access to collaborators + - plugin: collaborators + error: "Admin role cannot be assigned to individual collaborators" + script: | + return baseconfig.permission !== 'admin' + + # Ensure all repos have a description + - plugin: repository + error: "Repository must have a description" + script: | + return baseconfig.description && baseconfig.description.length > 10 + + # Validate repository naming conventions + - plugin: repository + error: "Repository names must follow the pattern: team-project-component" + script: | + const pattern = /^[a-z]+-[a-z]+-[a-z0-9-]+$/ + return pattern.test(baseconfig.name) +``` + +#### Override Validators + +Enforce constraints when lower-level settings override higher-level ones: + +```yaml +overridevalidators: + # Prevent reducing required approvers below the org baseline + - plugin: branches + error: "Cannot reduce required approving review count below organization minimum" + script: | + if (baseconfig.protection.required_pull_request_reviews.required_approving_review_count && + overrideconfig.protection.required_pull_request_reviews.required_approving_review_count) { + return overrideconfig.protection.required_pull_request_reviews.required_approving_review_count >= + baseconfig.protection.required_pull_request_reviews.required_approving_review_count + } + return true + + # Prevent disabling admin enforcement + - plugin: branches + error: "Cannot disable admin enforcement for branch protections" + script: | + if (baseconfig.protection.enforce_admins === true) { + return overrideconfig.protection.enforce_admins !== false + } + return true +``` + +### Disabling Plugins + +For scenarios where certain settings should not be managed by Safe-Settings at specific scopes: + +```yaml +# At the org level — disable milestones management entirely +disable_plugins: + - milestones + +# At the suborg level — disable labels for matched repos only +disable_plugins: + - plugin: labels + target: all +``` + +**Target options:** + +| Target | Effect | +|--------|--------| +| `self` | Strips the plugin from the declaring layer only | +| `children` | Strips from all layers below | +| `all` | Strips from the declaring layer and all layers below | + +**Important:** Strips are **union-only** — a lower-level config can add more strips but can never re-enable a plugin disabled at a higher level. + +### Additive Plugins + +For plugins where you want Safe-Settings to enforce a baseline but allow teams to add their own items without those additions being removed: + +```yaml +# In settings.yml — never remove labels or teams added outside safe-settings +additive_plugins: + - labels + - teams + - collaborators +``` + +In additive mode, Safe-Settings will **add** and **update** entries defined in YAML but will **never delete** items that exist on GitHub but are absent from the configuration. This is ideal for labels, teams, and collaborators where teams may need to add project-specific items. + +### Externally Defined Status Checks + +For status checks that are managed by CI/CD pipelines rather than Safe-Settings: + +```yaml +branches: + - name: default + protection: + required_status_checks: + contexts: + - "ci/build" # Managed by safe-settings + - "{{EXTERNALLY_DEFINED}}" # Preserve any additional checks set via UI +``` + +This allows Safe-Settings to enforce a minimum set of required status checks while preserving any additional checks configured by teams through the GitHub UI. + +--- + +## Drift Detection and Remediation + +### How Drift Is Detected + +Drift occurs when repository settings are changed outside of Safe-Settings — for example, a repository administrator modifying branch protections through the GitHub UI. + +Safe-Settings detects drift through two mechanisms: + +1. **Real-time webhook events**: When certain settings are changed (branch protections, rulesets, team memberships, collaborator changes), GitHub sends webhook events that trigger Safe-Settings to re-sync the affected repository. + +2. **Scheduled sync (CRON)**: A configurable cron job that periodically compares all managed repositories against the declared policy and remediates any drift. + +### Webhook-Based Remediation + +The following events trigger automatic remediation: + +- `branch_protection_rule` — Modified or deleted branch protections are restored +- `repository_ruleset` — Unauthorized ruleset changes are reverted +- `member` / `team` changes — Unauthorized permission changes are corrected +- `repository.edited` — Settings like default branch or topics are restored + +### Scheduled Sync Configuration + +```env +# Run drift detection every hour +CRON=0 * * * * + +# Or run at 2 AM daily for lower-priority environments +CRON=0 2 * * * +``` + +### Drift Remediation Strategy + +| Priority | Strategy | Use Case | +|----------|----------|----------| +| **Critical** | Real-time webhook + hourly CRON | Production security policies, branch protections | +| **Standard** | Real-time webhook + daily CRON | Team permissions, labels, general settings | +| **Advisory** | Daily CRON only | Low-risk settings where immediate enforcement isn't required | + +--- + +## Multi-Organization Deployments + +For enterprises with multiple GitHub organizations, Safe-Settings can be deployed in several patterns: + +### Pattern 1: One App per Organization + +Deploy a separate Safe-Settings instance for each organization. Each instance has its own admin repo and configuration. + +``` +┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ +│ Org: prod-eng │ │ Org: platform │ │ Org: open-src │ +│ │ │ │ │ │ +│ safe-settings │ │ safe-settings │ │ safe-settings │ +│ admin repo │ │ admin repo │ │ admin repo │ +│ policies A │ │ policies B │ │ policies C │ +└──────────────────┘ └──────────────────┘ └──────────────────┘ +``` + +**Advantages:** Complete isolation; different policies per org. +**Challenges:** Multiple deployments to manage; policy consistency must be maintained manually. + +### Pattern 2: Shared Policy Templates + +Maintain a shared repository of policy templates and use them as a source for each organization's admin repo: + +``` +┌─────────────────────────────────────────┐ +│ Policy Template Repo │ +│ (Gold-standard YAML templates) │ +└────────┬──────────┬──────────┬──────────┘ + │ │ │ + ┌────▼───┐ ┌────▼───┐ ┌───▼────┐ + │ Org A │ │ Org B │ │ Org C │ + │ admin │ │ admin │ │ admin │ + └────────┘ └────────┘ └────────┘ +``` + +Use CI/CD pipelines (e.g., GitHub Actions) to sync templates to each organization's admin repo, allowing per-org customization while maintaining a consistent baseline. + +### Pattern 3: GitHub Enterprise Server + Cloud + +For organizations using both GitHub Enterprise Server and GitHub.com: + +- Deploy Safe-Settings on-premises for GHES (set `GHE_HOST` environment variable) +- Deploy Safe-Settings in the cloud for GitHub.com organizations +- Use a shared policy template repo to maintain consistency + +--- + +## Security Considerations + +### Protecting the Admin Repository + +The admin repository is the source of truth for all organizational policies. Protect it with: + +1. **Branch protections on the default branch**: Require PR reviews, status checks, and code owner approval. +2. **CODEOWNERS**: Define who can approve changes to different policy files. +3. **Repository visibility**: Keep the admin repo private. +4. **Limited write access**: Only grant write access to authorized policy administrators. +5. **Audit logging**: GitHub's audit log captures all changes to the admin repo. + +### GitHub App Permissions + +Safe-Settings requires specific permissions to function. Follow the principle of least privilege: + +| Permission | Level | Purpose | +|-----------|-------|---------| +| Administration | Read & Write | Manage repository settings | +| Contents | Read & Write | Read config files from admin repo | +| Checks | Read & Write | Report validation results | +| Pull requests | Read & Write | Comment on policy change PRs | +| Custom properties | Read & Write | Manage custom property values | +| Members (org) | Read & Write | Manage team permissions | + +### Secrets Management + +- **Never** commit the GitHub App private key to the admin repo +- Use environment variables, Kubernetes Secrets, or cloud secret managers (AWS Secrets Manager, Azure Key Vault, HashiCorp Vault) +- Rotate the webhook secret periodically + +### Blocking Manual Overrides + +Enable `BLOCK_REPO_RENAME_BY_HUMAN=true` to prevent repository renames outside of Safe-Settings, maintaining configuration file consistency. + +--- + +## Case Study: Enterprise Rollout + +### Scenario + +A financial services company with 3,000+ repositories across 50 development teams needs to enforce SOX compliance controls, standardize branch protections, and reduce the time to provision new repositories from days to minutes. + +### Phase 1: Discovery and Planning (Week 1–2) + +1. **Audit current state**: Document existing repository configurations across the organization. +2. **Define policy tiers**: Establish three compliance tiers — Standard, Regulated, and Critical. +3. **Map teams to suborgs**: Define suborg membership using custom properties (`compliance_tier: standard|regulated|critical`). +4. **Design CODEOWNERS**: Map approval authority for each policy tier. + +### Phase 2: Pilot Deployment (Week 3–4) + +1. **Deploy Safe-Settings** to a Kubernetes cluster with the Helm chart. +2. **Restrict scope** to 10 pilot repositories using `deployment-settings.yml`. +3. **Define baseline policies** for organization-wide settings. +4. **Test dry-run mode** by creating PRs and validating check results. +5. **Validate drift remediation** by manually changing settings and confirming automatic reversion. + +### Phase 3: Gradual Rollout (Week 5–8) + +1. **Expand scope** to one team (50 repositories) per week. +2. **Create suborg policies** for each compliance tier. +3. **Enable override validators** to prevent weakening of security controls. +4. **Train team leads** on creating repo-specific overrides via PR workflow. + +### Phase 4: Full Deployment (Week 9–10) + +1. **Remove scope restrictions** — Safe-Settings manages all repositories. +2. **Enable scheduled sync** with `CRON=0 * * * *` for hourly drift checks. +3. **Enable `BLOCK_REPO_RENAME_BY_HUMAN`** for configuration consistency. +4. **Document runbooks** for common policy change scenarios. + +### Results + +| Metric | Before | After | +|--------|--------|-------| +| Time to provision new repo | 2–3 days | < 5 minutes | +| Repos with compliant branch protections | 62% | 100% | +| Manual drift incidents per month | 40+ | 0 (auto-remediated) | +| Policy change audit trail | Partial | Complete (Git history) | +| Time to demonstrate compliance | Days | Minutes (YAML-as-evidence) | + +--- + +## Best Practices + +### Policy Design + +1. **Keep `settings.yml` minimal — rulesets and custom roles only**: Any change to `settings.yml` triggers processing for every managed repository. To avoid cascading API calls across thousands of repos, limit org-level settings to resources that use org-scoped API endpoints (rulesets and custom repository roles). Move all repo-scoped settings (teams, labels, repository config, collaborators) to suborg files. + +2. **Use a broad suborg for repo-scoped baselines**: Create a `baseline.yml` suborg with `suborgrepos: ["*"]` to apply default repo-scoped settings (teams, labels, security config) to all repos. This achieves the same coverage as org-level settings but limits the blast radius of changes to the matched subset. + +3. **Use suborgs for team autonomy**: Rather than creating repo-level overrides for every repository, group repos into suborgs by team, project, or compliance tier. + +4. **Prefer custom properties for suborg membership**: Custom properties provide the most flexible and maintainable way to group repositories, as they can be updated without modifying the admin repo. + +5. **Use additive mode for shared resources**: For labels, teams, and collaborators, consider using `additive_plugins` to allow teams to add project-specific items without those additions being removed on the next sync. + +6. **Define validators early**: Establish override validators before teams start creating overrides. This prevents policy weakening from the start. + +### Operational Excellence + +6. **Protect the admin repo**: Apply the same (or stricter) branch protections to the admin repo as you require for production code. + +7. **Use CODEOWNERS strategically**: Grant approval authority at the appropriate level — security team for org settings, team leads for suborg settings, repo owners for repo-specific overrides. + +8. **Monitor check runs**: Set up notifications for failed Safe-Settings check runs to catch configuration issues early. + +9. **Schedule regular sync**: Even with webhook-based enforcement, configure a CRON schedule as a safety net for missed webhooks. + +10. **Version pin your deployment**: Use specific image tags (e.g., `ghcr.io/github/safe-settings:2.1.13`) rather than floating tags to ensure reproducible deployments. + +### Scaling + +11. **Phase your rollout**: Use `deployment-settings.yml` to gradually expand scope. Start with 10 repos, then 100, then 1,000. + +12. **Avoid repo-scoped settings in `settings.yml`**: Changes to `settings.yml` trigger processing for all managed repositories. Keep it to org-level rulesets and custom roles only. Use suborg files for repo-scoped settings to limit the blast radius of any single change. + +13. **Use include/exclude patterns**: For teams and collaborators, use `include` and `exclude` patterns rather than defining settings for every repository individually. + +14. **Monitor API rate limits**: At scale, watch for rate limit consumption. Probot handles this automatically, but awareness helps with capacity planning. + +--- + +## Conclusion + +GitHub Safe-Settings transforms repository governance from a manual, error-prone process into an automated, auditable, and scalable policy-as-code practice. By centralizing configuration in a protected admin repository, enforcing changes through pull request workflows, and continuously remediating drift, organizations can achieve consistent security baselines, streamlined compliance, and empowered development teams. + +Whether managing 50 or 5,000 repositories, Safe-Settings provides the flexibility to balance centralized governance with team autonomy — ensuring that every repository in your organization meets your standards, every time. + +--- + +## Additional Resources + +- **Repository**: [github/safe-settings](https://github.com/github/safe-settings) +- **Deployment Guide**: [docs/deploy.md](deploy.md) +- **GitHub Actions Guide**: [docs/github-action.md](github-action.md) +- **Sample Settings**: [docs/sample-settings/](sample-settings/) +- **AWS Lambda Template**: [SafeSettings-Template](https://github.com/bheemreddy181/SafeSettings-Template) + +--- + +*© 2026 GitHub, Inc. Safe-Settings is licensed under the [ISC License](https://opensource.org/licenses/ISC).* diff --git a/generate-settings.js b/generate-settings.js new file mode 100644 index 000000000..ab752fbaf --- /dev/null +++ b/generate-settings.js @@ -0,0 +1,156 @@ +/* eslint-disable camelcase */ +/** + * Standalone CLI to generate safe-settings YAML from the *current* state of a + * repo / org / collection-of-repos and write it to the local filesystem. + * + * Usage (env or flags): + * SOURCE_TYPE=repo SOURCE_VALUE=my-repo node generate-settings.js + * SOURCE_TYPE=org SOURCE_VALUE=my-org node generate-settings.js + * SOURCE_TYPE=custom-property SOURCE_VALUE=Team=backend node generate-settings.js + * + * node generate-settings.js --source-type repo --source-value my-repo \ + * --owner my-org --output-dir ./out --overwrite + * + * When overwrite is false (default) and the target file already exists, a + * `.sample.yml` file is written instead of replacing the existing file. + */ +const fs = require('fs') +const path = require('path') + +// Load .env into process.env before any module reads it (lib/env.js reads at +// require time). Mirrors the lightweight parser used by smoke-test.js so we +// avoid adding a dotenv dependency. +function loadEnv () { + const envPath = path.join(__dirname, '.env') + if (!fs.existsSync(envPath)) return + const lines = fs.readFileSync(envPath, 'utf8').split('\n') + let currentKey = null + let currentValue = '' + let inMultiline = false + + for (const line of lines) { + if (inMultiline) { + currentValue += '\n' + line + if (line.includes('"') || line.includes("'")) { + const val = currentValue.replace(/^["']|["']$/g, '') + // Like dotenv: .env values don't override existing env vars + if (!(currentKey in process.env)) process.env[currentKey] = val + inMultiline = false + } + continue + } + const trimmed = line.trim() + if (!trimmed || trimmed.startsWith('#')) continue + const eqIdx = trimmed.indexOf('=') + if (eqIdx === -1) continue + currentKey = trimmed.slice(0, eqIdx).trim() + currentValue = trimmed.slice(eqIdx + 1).trim() + if ((currentValue.startsWith('"') && !currentValue.endsWith('"')) || + (currentValue.startsWith("'") && !currentValue.endsWith("'"))) { + inMultiline = true + continue + } + const val = currentValue.replace(/^["']|["']$/g, '') + if (!(currentKey in process.env)) process.env[currentKey] = val + } +} + +loadEnv() +const { createProbot } = require('probot') +const SettingsGenerator = require('./lib/settingsGenerator') + +function parseArgs (argv) { + const args = {} + for (let i = 0; i < argv.length; i++) { + const arg = argv[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + if (key === 'overwrite') { + args.overwrite = true + } else { + args[key] = argv[++i] + } + } + } + return args +} + +function resolveOptions () { + const args = parseArgs(process.argv.slice(2)) + const sourceType = args['source-type'] || process.env.SOURCE_TYPE + const sourceValue = args['source-value'] || process.env.SOURCE_VALUE + const propertyName = args['property-name'] || process.env.SOURCE_PROPERTY_NAME + const owner = args.owner || process.env.OWNER || process.env.GITHUB_ORG || process.env.GH_ORG + const outputDir = args['output-dir'] || process.env.OUTPUT_DIR || '.' + const overwrite = args.overwrite || process.env.OVERWRITE === 'true' + + if (!sourceType || !sourceValue) { + throw new Error('SOURCE_TYPE and SOURCE_VALUE (or --source-type/--source-value) are required') + } + return { sourceType, sourceValue, propertyName, owner, outputDir, overwrite } +} + +/** + * Get an authenticated installation octokit + the org login. + * If OWNER is provided we match its installation, otherwise use the first. + */ +async function getInstallationClient (probot, owner) { + const app = await probot.auth() + const installations = await app.paginate( + app.apps.listInstallations.endpoint.merge({ per_page: 100 }) + ) + if (installations.length === 0) { + throw new Error('No installations found for this GitHub App') + } + const installation = owner + ? installations.find(i => i.account.login.toLowerCase() === owner.toLowerCase()) + : installations[0] + if (!installation) { + throw new Error(`No installation found for owner "${owner}"`) + } + const github = await probot.auth(installation.id) + return { github, owner: installation.account.login } +} + +/** + * Write content to disk honoring the overwrite/.sample rule. + * @returns {string} the path actually written + */ +function writeOutput (outputDir, filePath, content, overwrite) { + let target = path.join(outputDir, filePath) + if (!overwrite && fs.existsSync(target)) { + const parsed = path.parse(target) + target = path.join(parsed.dir, `${parsed.name}.sample${parsed.ext}`) + } + fs.mkdirSync(path.dirname(target), { recursive: true }) + fs.writeFileSync(target, content) + return target +} + +async function main () { + const opts = resolveOptions() + const probot = createProbot() + probot.log.info(`Generating settings: source-type=${opts.sourceType} source-value=${opts.sourceValue}`) + + const { github, owner } = await getInstallationClient(probot, opts.owner) + const generator = new SettingsGenerator(github, owner, { log: probot.log }) + + const { filePath, yaml } = await generator.generate({ + sourceType: opts.sourceType, + sourceValue: opts.sourceValue, + propertyName: opts.propertyName + }) + + const written = writeOutput(opts.outputDir, filePath, yaml, opts.overwrite) + probot.log.info(`Wrote ${written}`) + process.stdout.write(`${written}\n`) +} + +if (require.main === module) { + main().catch(error => { + process.stderr.write(`Error generating settings: ${error.stack || error}\n`) + process.exit(1) + }) +} + +module.exports = { parseArgs, resolveOptions, writeOutput, getInstallationClient } diff --git a/helm/safe-settings/README.md b/helm/safe-settings/README.md index a5112cfb2..97cd5b01b 100644 --- a/helm/safe-settings/README.md +++ b/helm/safe-settings/README.md @@ -9,22 +9,15 @@ A Helm chart for Kubernetes | affinity | object | `{}` | | | autoscaling.enabled | bool | `false` | | | autoscaling.maxReplicas | int | `10` | | -| autoscaling.minReplicas | int | `2` | | +| autoscaling.minReplicas | int | `1` | | | autoscaling.targetCPUUtilizationPercentage | int | `80` | | -| deploymentConfig.configvalidators[0].error | string | `"`Admin cannot be assigned to collaborators`\n"` | | -| deploymentConfig.configvalidators[0].plugin | string | `"collaborators"` | | -| deploymentConfig.configvalidators[0].script | string | `"console.log(`baseConfig ${JSON.stringify(baseconfig)}`)\nreturn baseconfig.permission != 'admin'\n"` | | -| deploymentConfig.overridevalidators[0].error | string | `"`Branch protection required_approving_review_count cannot be overidden to a lower value`\n"` | | -| deploymentConfig.overridevalidators[0].plugin | string | `"branches"` | | -| deploymentConfig.overridevalidators[0].script | string | `"console.log(`baseConfig ${JSON.stringify(baseconfig)}`)\nconsole.log(`overrideConfig ${JSON.stringify(overrideconfig)}`)\nif (baseconfig.protection.required_pull_request_reviews.required_approving_review_count && overrideconfig.protection.required_pull_request_reviews.required_approving_review_count ) {\n return overrideconfig.protection.required_pull_request_reviews.required_approving_review_count >= baseconfig.protection.required_pull_request_reviews.required_approving_review_count\n}\nreturn true\n"` | | -| deploymentConfig.overridevalidators[1].error | string | `"Some error\n"` | | -| deploymentConfig.overridevalidators[1].plugin | string | `"labels"` | | -| deploymentConfig.overridevalidators[1].script | string | `"return true\n"` | | +| deploymentConfig.configvalidators | list | [] | | +| deploymentConfig.overridevalidators | list | [] | | | deploymentConfig.restrictedRepos.exclude[0] | string | `"^admin$"` | | | deploymentConfig.restrictedRepos.exclude[1] | string | `"^\\.github$"` | | | deploymentConfig.restrictedRepos.exclude[2] | string | `"^safe-settings$"` | | | deploymentConfig.restrictedRepos.exclude[3] | string | `".*-test"` | | -| deploymentConfig.restrictedRepos.include[0] | string | `"^test$"` | | +| deploymentConfig.restrictedRepos.include | null | `null` | | | env | list | `[]` | | | envFrom | list | `[]` | | | extraObjects | list | `[]` | Add dynamic manifests via values. Example: extraObjects: - kind: ConfigMap apiVersion: v1 metadata: name: extra-cm-{{ .Release.Name }} data: | extra.yml: "does-my-install-need-extra-info: true" | @@ -44,7 +37,7 @@ A Helm chart for Kubernetes | nodeSelector | object | `{}` | | | podAnnotations | object | `{}` | | | podSecurityContext | object | `{}` | | -| replicaCount | int | `2` | | +| replicaCount | int | `1` | | | resources | object | `{}` | | | securityContext.allowPrivilegeEscalation | bool | `false` | | | securityContext.capabilities.drop[0] | string | `"ALL"` | | @@ -52,7 +45,7 @@ A Helm chart for Kubernetes | securityContext.readOnlyRootFilesystem | bool | `true` | | | securityContext.runAsNonRoot | bool | `true` | | | securityContext.runAsUser | int | `1000` | | -| service.port | int | `80` | | +| service.port | int | `3000` | | | service.type | string | `"ClusterIP"` | | | serviceAccount.annotations | object | `{}` | | | serviceAccount.automountServiceAccountToken | bool | `false` | | diff --git a/helm/safe-settings/templates/deployment.yaml b/helm/safe-settings/templates/deployment.yaml index 5d1d6bc50..cf75673ec 100644 --- a/helm/safe-settings/templates/deployment.yaml +++ b/helm/safe-settings/templates/deployment.yaml @@ -43,7 +43,7 @@ spec: {{- end }} ports: - name: http - containerPort: {{ .Values.service.port }} + containerPort: 3000 protocol: TCP resources: {{- toYaml .Values.resources | nindent 12 }} diff --git a/helm/safe-settings/values.yaml b/helm/safe-settings/values.yaml index 4a9253c61..99ffa497f 100644 --- a/helm/safe-settings/values.yaml +++ b/helm/safe-settings/values.yaml @@ -2,7 +2,7 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. -replicaCount: 2 +replicaCount: 1 image: repository: ghcr.io/github/safe-settings @@ -45,6 +45,26 @@ envFrom: [] # name: app-env env: [] +# - name: CRON +# value: '*/5 * * * *' +# - name: APP_ID +# valueFrom: +# secretKeyRef: +# name: github_app_secret +# key: app_id +# optional: false +# - name: PRIVATE_KEY +# valueFrom: +# secretKeyRef: +# name: github_app_secret +# key: private_key +# optional: false +# - name: WEBHOOK_SECRET +# valueFrom: +# secretKeyRef: +# name: github_app_secret +# key: webhook_secret +# optional: false service: type: ClusterIP @@ -83,7 +103,7 @@ resources: autoscaling: enabled: false - minReplicas: 2 + minReplicas: 1 maxReplicas: 10 targetCPUUtilizationPercentage: 80 # targetMemoryUtilizationPercentage: 80 @@ -104,30 +124,30 @@ deploymentConfig: # If no file is specified, then the following repositories - 'admin', '.github', 'safe-settings' are exempted by default exclude: ["^admin$", '^\.github$', "^safe-settings$", ".*-test"] # Alternatively you can only include certain repos - include: ["^test$"] - configvalidators: - - plugin: collaborators - error: | - `Admin cannot be assigned to collaborators` - script: | - console.log(`baseConfig ${JSON.stringify(baseconfig)}`) - return baseconfig.permission != 'admin' - overridevalidators: - - plugin: branches - error: | - `Branch protection required_approving_review_count cannot be overidden to a lower value` - script: | - console.log(`baseConfig ${JSON.stringify(baseconfig)}`) - console.log(`overrideConfig ${JSON.stringify(overrideconfig)}`) - if (baseconfig.protection.required_pull_request_reviews.required_approving_review_count && overrideconfig.protection.required_pull_request_reviews.required_approving_review_count ) { - return overrideconfig.protection.required_pull_request_reviews.required_approving_review_count >= baseconfig.protection.required_pull_request_reviews.required_approving_review_count - } - return true - - plugin: labels - error: | - Some error - script: | - return true + include: null # ["^test$"] + configvalidators: [] + # - plugin: collaborators + # error: | + # `Admin cannot be assigned to collaborators` + # script: | + # console.log(`baseConfig ${JSON.stringify(baseconfig)}`) + # return baseconfig.permission != 'admin' + overridevalidators: [] + # - plugin: branches + # error: | + # `Branch protection required_approving_review_count cannot be overridden to a lower value` + # script: | + # console.log(`baseConfig ${JSON.stringify(baseconfig)}`) + # console.log(`overrideConfig ${JSON.stringify(overrideconfig)}`) + # if (baseconfig.protection.required_pull_request_reviews.required_approving_review_count && overrideconfig.protection.required_pull_request_reviews.required_approving_review_count ) { + # return overrideconfig.protection.required_pull_request_reviews.required_approving_review_count >= baseconfig.protection.required_pull_request_reviews.required_approving_review_count + # } + # return true + # - plugin: labels + # error: | + # Some error + # script: | + # return true # -- Add dynamic manifests via values. Example: # extraObjects: diff --git a/index.js b/index.js index e6fd1c8d7..8b1305ebe 100644 --- a/index.js +++ b/index.js @@ -5,13 +5,19 @@ const cron = require('node-cron') const Glob = require('./lib/glob') const ConfigManager = require('./lib/configManager') const NopCommand = require('./lib/nopcommand') +const SettingsGenerator = require('./lib/settingsGenerator') +const AppOctokitClient = require('./lib/appOctokitClient') const env = require('./lib/env') let deploymentConfig module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => { let appSlug = 'safe-settings' - async function syncAllSettings (nop, context, repo = context.repo(), ref) { + // Cache of enterprise slug → enterprise installation id. Keyed by slug so a + // cached id is never reused for a different enterprise (e.g. when the app + // handles events from multiple enterprises). + const cachedEnterpriseInstallationIds = new Map() + async function syncAllSettings (nop, context, repo = context.repo(), ref, baseRef, changedFiles = {}) { try { deploymentConfig = await loadYamlFileSystem() robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) @@ -19,8 +25,24 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => const runtimeConfig = await configManager.loadGlobalSettingsYaml() const config = Object.assign({}, deploymentConfig, runtimeConfig) robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`) + + // Enrich context with enterprise info for app installation management + await enrichContextWithEnterprise(context) + + // Load base branch config for NOP filtering (only show PR-introduced changes) + let baseConfig = null + if (nop && baseRef) { + try { + const baseConfigManager = new ConfigManager(context, baseRef) + const baseRuntimeConfig = await baseConfigManager.loadGlobalSettingsYaml() + baseConfig = Object.assign({}, deploymentConfig, baseRuntimeConfig) + } catch (e) { + robot.log.debug(`Could not load base config for NOP filtering: ${e.message}`) + } + } + if (ref) { - return Settings.syncAll(nop, context, repo, config, ref) + return Settings.syncAll(nop, context, repo, config, ref, baseConfig, changedFiles) } else { return Settings.syncAll(nop, context, repo, config) } @@ -40,7 +62,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => } } - async function syncSubOrgSettings (nop, context, suborg, repo = context.repo(), ref) { + async function syncSettings (nop, context, repo = context.repo(), ref) { try { deploymentConfig = await loadYamlFileSystem() robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) @@ -48,7 +70,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => const runtimeConfig = await configManager.loadGlobalSettingsYaml() const config = Object.assign({}, deploymentConfig, runtimeConfig) robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`) - return Settings.syncSubOrgs(nop, context, suborg, repo, config, ref) + return Settings.sync(nop, context, repo, config, ref) } catch (e) { if (nop) { let filename = env.SETTINGS_FILE_PATH @@ -65,7 +87,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => } } - async function syncSettings (nop, context, repo = context.repo(), ref) { + async function syncSelectedSettings (nop, context, repos, subOrgs, ref, baseRef) { try { deploymentConfig = await loadYamlFileSystem() robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) @@ -73,7 +95,23 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => const runtimeConfig = await configManager.loadGlobalSettingsYaml() const config = Object.assign({}, deploymentConfig, runtimeConfig) robot.log.debug(`config for ref ${ref} is ${JSON.stringify(config)}`) - return Settings.sync(nop, context, repo, config, ref) + + // Enrich context with enterprise info for app installation management + await enrichContextWithEnterprise(context) + + // Load base branch config for NOP filtering (only show PR-introduced changes) + let baseConfig = null + if (nop && baseRef) { + try { + const baseConfigManager = new ConfigManager(context, baseRef) + const baseRuntimeConfig = await baseConfigManager.loadGlobalSettingsYaml() + baseConfig = Object.assign({}, deploymentConfig, baseRuntimeConfig) + } catch (e) { + robot.log.debug(`Could not load base config for NOP filtering: ${e.message}`) + } + } + + return Settings.syncSelectedRepos(nop, context, repos, subOrgs, config, ref, baseConfig, baseRef) } catch (e) { if (nop) { let filename = env.SETTINGS_FILE_PATH @@ -81,9 +119,9 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => filename = env.DEPLOYMENT_CONFIG_FILE_PATH deploymentConfig = {} } - const nopcommand = new NopCommand(filename, repo, null, e, 'ERROR') + const nopcommand = new NopCommand(filename, context.repo(), null, e, 'ERROR') robot.log.error(`NOPCOMMAND ${JSON.stringify(nopcommand)}`) - Settings.handleError(nop, context, repo, deploymentConfig, ref, nopcommand) + Settings.handleError(nop, context, context.repo(), deploymentConfig, ref, nopcommand) } else { throw e } @@ -115,6 +153,102 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => } } } + /** + * Lists all installations of the app using a JWT-authenticated client. + * + * @returns {Promise} All app installations + */ + async function listAllInstallations () { + const github = await robot.auth() + return github.paginate( + github.rest.apps.listInstallations.endpoint.merge({ per_page: 100 }) + ) + } + + /** + * Finds the enterprise installation matching the given slug from the app's + * installation list. Returns null if none matches. + * + * @param {string} enterpriseSlug - Enterprise slug + * @returns {Promise} The matching enterprise installation + */ + async function findEnterpriseInstallation (enterpriseSlug) { + const installations = await listAllInstallations() + return installations.find( + i => i.target_type === 'Enterprise' && i.account && `${i.account.slug}`.toLowerCase() === enterpriseSlug.toLowerCase() + ) || null + } + + /** + * Finds the enterprise installation for a given slug and returns an Octokit + * client authenticated with the enterprise installation token, along with + * the installation ID. + * + * Uses the cached enterprise installation ID for the given slug when + * available to avoid re-listing installations. The cache is keyed by + * enterprise slug so an id is never reused across enterprises. Returns null + * if no matching enterprise installation is found. + * + * @param {string} enterpriseSlug - Enterprise slug + * @returns {Promise<{ appGithub: object, installationId: number } | null>} + */ + async function getEnterpriseAppClient (enterpriseSlug) { + if (!enterpriseSlug) return null + // Normalize the slug to lowercase for consistent cache keying + enterpriseSlug = enterpriseSlug.toLowerCase() + + // Use the cached enterprise installation id for THIS slug if available. + // Keying by slug ensures a cached id is never reused for a different + // enterprise. + const cachedId = cachedEnterpriseInstallationIds.get(enterpriseSlug) + if (cachedId) { + try { + const appGithub = await robot.auth(cachedId) + return { appGithub, installationId: cachedId } + } catch (e) { + cachedEnterpriseInstallationIds.delete(enterpriseSlug) + } + } + + // Find the installation targeting this enterprise + const enterpriseInstallation = await findEnterpriseInstallation(enterpriseSlug) + if (!enterpriseInstallation) { + return null + } + cachedEnterpriseInstallationIds.set(enterpriseSlug, enterpriseInstallation.id) + const enterpriseGithub = await robot.auth(enterpriseInstallation.id) + return { appGithub: enterpriseGithub, installationId: enterpriseInstallation.id } + } + + /** + * Enriches the context with enterprise info for app installation management. + * Extracts enterprise slug from the webhook payload, finds the enterprise + * installation from the app's installation list, and creates an Octokit + * client authenticated with the enterprise installation token. + * + * @param {object} context - Probot context + */ + async function enrichContextWithEnterprise (context) { + const { payload } = context + const slugFromPayload = (payload.enterprise && payload.enterprise.slug) || + (payload.installation && payload.installation.enterprise && payload.installation.enterprise.slug) + const enterpriseSlug = slugFromPayload || process.env.GH_ENTERPRISE + + if (!enterpriseSlug) return + + context.enterpriseSlug = enterpriseSlug + try { + const result = await getEnterpriseAppClient(enterpriseSlug) + if (result) { + context.appGithub = result.appGithub + } else { + robot.log.debug(`No enterprise installation found for slug '${enterpriseSlug}'. App installation management will not be available.`) + } + } catch (e) { + robot.log.debug(`Could not create enterprise-authenticated client: ${e.message}`) + } + } + /** * Loads the deployment config file from file system * Do this once when the app starts and then return the cached value @@ -139,9 +273,15 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => const getMatchingFiles = (commits, type) => commits.flatMap((c) => c[type].filter((file) => pattern.test(file))) + // Include 'removed' so deleting a suborg config file is detected as a + // change. The downstream delta logic loads the current version from the + // head ref (which fails for a deleted file, yielding an empty config) and + // diffs it against the base ref, correctly detecting removed entries such + // as app_installations. const changes = [ ...getMatchingFiles(payload.commits, 'added'), - ...getMatchingFiles(payload.commits, 'modified') + ...getMatchingFiles(payload.commits, 'modified'), + ...getMatchingFiles(payload.commits, 'removed') ] return changes.map((file) => ({ @@ -156,9 +296,15 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => const getMatchingFiles = (commits, type) => commits.flatMap((c) => c[type].filter((file) => pattern.test(file))) + // Include 'removed' so deleting a repo config file is detected as a change. + // The downstream delta logic loads the current version from the head ref + // (which fails for a deleted file, yielding an empty config) and diffs it + // against the base ref, correctly detecting removed entries such as + // app_installations. const changes = [ ...getMatchingFiles(payload.commits, 'added'), - ...getMatchingFiles(payload.commits, 'modified') + ...getMatchingFiles(payload.commits, 'modified'), + ...getMatchingFiles(payload.commits, 'removed') ] return changes.map((file) => ({ @@ -191,7 +337,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => async function createCheckRun (context, pull_request, head_sha, head_branch) { const { payload } = context // robot.log.debug(`Check suite was requested! for ${context.repo()} ${pull_request.number} ${head_sha} ${head_branch}`) - const res = await context.octokit.checks.create({ + const res = await context.octokit.rest.checks.create({ owner: payload.repository.owner.login, repo: payload.repository.name, name: 'Safe-setting validator', @@ -201,27 +347,68 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => } async function info () { - const github = await robot.auth() - const installations = await github.paginate( - github.apps.listInstallations.endpoint.merge({ per_page: 100 }) - ) + const installations = await listAllInstallations() robot.log.debug(`installations: ${JSON.stringify(installations)}`) if (installations.length > 0) { const installation = installations[0] const github = await robot.auth(installation.id) - const app = await github.apps.getAuthenticated() + const app = await github.rest.apps.getAuthenticated() appSlug = app.data.slug - robot.log.debug(`Validated the app is configured properly = \n${JSON.stringify(app.data, null, 2)}`) + robot.log.info(`Validated the app is configured properly = \n${JSON.stringify(app.data, null, 2)}`) + } + + await verifyAppInstallationsPlugin() + } + + /** + * Verifies that the app-installations plugin can function properly. + * + * When the `GH_ENTERPRISE` env variable is set, this: + * 1. Finds the enterprise installation matching the slug. + * 2. Mints an installation token for that enterprise installation. + * 3. Confirms the token has permission to manage app installations in the + * target org (`GH_ORG`) by listing the org's app installations via the + * Enterprise organization installations API. + * + * If `GH_ENTERPRISE` is not set, this verification is skipped entirely. + */ + async function verifyAppInstallationsPlugin () { + const enterpriseSlug = process.env.GH_ENTERPRISE + if (!enterpriseSlug) { + robot.log.info('GH_ENTERPRISE is not set — skipping app-installations plugin verification') + return + } + + const org = process.env.GH_ORG + if (!org) { + robot.log.warn('GH_ENTERPRISE is set but GH_ORG is not — cannot verify app-installations plugin without a target org') + return + } + + try { + const result = await getEnterpriseAppClient(enterpriseSlug) + if (!result) { + robot.log.warn(`No enterprise installation found for slug '${enterpriseSlug}'. App-installations plugin will not be able to manage app access. Ensure safe-settings is installed on the enterprise.`) + return + } + + const client = new AppOctokitClient({ + github: result.appGithub, + enterpriseSlug, + log: robot.log + }) + + // Confirm the token can list org app installations (validates permission) + const orgInstallations = await client.listOrgInstallations(org) + robot.log.info(`App-installations plugin verified: enterprise '${enterpriseSlug}' installation (id: ${result.installationId}) can manage apps in org '${org}' (${orgInstallations.length} installation(s) visible)`) + } catch (e) { + robot.log.error(`App-installations plugin verification failed for enterprise '${enterpriseSlug}' / org '${org}': ${e.message}`) } } async function syncInstallation (nop = false) { robot.log.trace('Fetching installations') - const github = await robot.auth() - - const installations = await github.paginate( - github.apps.listInstallations.endpoint.merge({ per_page: 100 }) - ) + const installations = await listAllInstallations() if (installations.length > 0) { const installation = installations[0] @@ -254,27 +441,29 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => return } + let repoChanges = getAllChangedRepoConfigs(payload, context.repo().owner) + + let subOrgChanges = getAllChangedSubOrgConfigs(payload) + repoChanges = repoChanges.filter((r, i, arr) => arr.findIndex(item => item.repo === r.repo) === i) + + subOrgChanges = subOrgChanges.filter((s, i, arr) => arr.findIndex(item => item.repo === s.repo) === i) + robot.log.debug(`deduped repos ${JSON.stringify(repoChanges)}`) + robot.log.debug(`deduped subOrgs ${JSON.stringify(subOrgChanges)}`) + const settingsModified = payload.commits.find(commit => { return commit.added.includes(Settings.FILE_PATH) || commit.modified.includes(Settings.FILE_PATH) }) if (settingsModified) { robot.log.debug(`Changes in '${Settings.FILE_PATH}' detected, doing a full synch...`) - return syncAllSettings(false, context) - } - - const repoChanges = getAllChangedRepoConfigs(payload, context.repo().owner) - if (repoChanges.length > 0) { - return Promise.all(repoChanges.map(repo => { - return syncSettings(false, context, repo) - })) + return syncAllSettings(false, context, context.repo(), payload.after, null, { + repos: repoChanges, + subOrgs: subOrgChanges + }) } - const changes = getAllChangedSubOrgConfigs(payload) - if (changes.length) { - return Promise.all(changes.map(suborg => { - return syncSubOrgSettings(false, context, suborg) - })) + if (repoChanges.length > 0 || subOrgChanges.length > 0) { + return syncSelectedSettings(false, context, repoChanges, subOrgChanges, payload.after, payload.before) } robot.log.debug(`No changes in '${Settings.FILE_PATH}' detected, returning...`) @@ -453,6 +642,39 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => } }) + // ──────────────────────────────────────────────────────────────────────── + // App installation target handler + // + // Note: We intentionally do NOT handle `installation.repositories_added` / + // `installation.repositories_removed`. A GitHub App only receives those + // events for its OWN installation, not for the managed apps (e.g. Copilot, + // Dependabot) whose repository access safe-settings controls. They cannot + // detect drift on managed apps, so drift is reconciled by the scheduled + // (cron) full sync instead. + // ──────────────────────────────────────────────────────────────────────── + + robot.on('installation_target', async context => { + const { payload } = context + const { sender } = payload + robot.log.debug('Installation target changed by ', JSON.stringify(sender)) + if (sender.type === 'Bot') { + robot.log.debug('Installation target changed by Bot') + return + } + robot.log.debug('Installation target changed by a Human — triggering sync to revert drift') + + const orgLogin = (payload.organization && payload.organization.login) || + (payload.installation && payload.installation.account && payload.installation.account.login) + if (!orgLogin) { + robot.log.debug('Could not determine org login from installation_target event, skipping') + return + } + const updatedContext = Object.assign({}, context, { + repo: () => { return { repo: env.ADMIN_REPO, owner: orgLogin } } + }) + return syncAllSettings(false, updatedContext) + }) + robot.on('check_suite.requested', async context => { const { payload } = context const { repository } = payload @@ -570,37 +792,29 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => output: { title: 'Starting NOP', summary: 'initiating...' } } robot.log.debug(`Updating check run ${JSON.stringify(params)}`) - await context.octokit.checks.update(params) + await context.octokit.rest.checks.update(params) - // guarding against null value from upstream libary that is - // causing a 404 and the check to stall - // from issue: https://github.com/github/safe-settings/issues/185#issuecomment-1075240374 - if (check_suite.before === '0000000000000000000000000000000000000000') { - check_suite.before = check_suite.pull_requests[0].base.sha - } - params = Object.assign(context.repo(), { basehead: `${check_suite.before}...${check_suite.after}` }) - const changes = await context.octokit.repos.compareCommitsWithBasehead(params) - const files = changes.data.files.map(f => { return f.filename }) + params = Object.assign(context.repo(), { pull_number: pull_request.number }) + + const changes = await context.octokit.rest.pulls.listFiles(params) + const files = changes.data.map(f => { return f.filename }) const settingsModified = files.includes(Settings.FILE_PATH) + const repoChanges = getChangedRepoConfigName(files, context.repo().owner) + const subOrgChanges = getChangedSubOrgConfigName(files) if (settingsModified) { robot.log.debug(`Changes in '${Settings.FILE_PATH}' detected, doing a full synch...`) - return syncAllSettings(true, context, context.repo(), pull_request.head.ref) + const baseRef = pull_request.base.ref || repository.default_branch + return syncAllSettings(true, context, context.repo(), pull_request.head.ref, baseRef, { + repos: repoChanges, + subOrgs: subOrgChanges + }) } - const repoChanges = getChangedRepoConfigName(files, context.repo().owner) - if (repoChanges.length > 0) { - return Promise.all(repoChanges.map(repo => { - return syncSettings(true, context, repo, pull_request.head.ref) - })) - } - - const subOrgChanges = getChangedSubOrgConfigName(files) - if (subOrgChanges.length) { - return Promise.all(subOrgChanges.map(suborg => { - return syncSubOrgSettings(true, context, suborg, context.repo(), pull_request.head.ref) - })) + if (repoChanges.length > 0 || subOrgChanges.length > 0) { + const baseRef = pull_request.base.ref || repository.default_branch + return syncSelectedSettings(true, context, repoChanges, subOrgChanges, pull_request.head.ref, baseRef) } // if no safe-settings changes detected, send a success to the check run @@ -614,7 +828,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => output: { title: 'No Safe-settings changes detected', summary: 'No changes detected' } } robot.log.debug(`Completing check run ${JSON.stringify(params)}`) - await context.octokit.checks.update(params) + await context.octokit.rest.checks.update(params) }) robot.on('repository.created', async context => { @@ -650,6 +864,137 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => return syncSettings(false, context) }) + /** + * Generate safe-settings YAML from the current state of a repo / org / + * collection-of-repos and open a PR against the admin repo with the result. + * + * @param {import('probot').Context} context + * @param {object} opts + * @param {'repo'|'org'|'custom-property'} opts.sourceType + * @param {string} opts.sourceValue + * @param {string} [opts.propertyName] + * @param {boolean} [opts.overwrite] + */ + async function generateSettings (context, opts) { + const owner = context.repo().owner + const github = context.octokit + const generator = new SettingsGenerator(github, owner, { log: robot.log }) + + const { filePath, yaml: content } = await generator.generate({ + sourceType: opts.sourceType, + sourceValue: opts.sourceValue, + propertyName: opts.propertyName + }) + + const targetPath = await resolveOutputPath(context, filePath, opts.overwrite) + return openSettingsPR(context, targetPath, content, opts) + } + + /** + * Honor the overwrite/.sample rule against the admin repo: if overwrite is + * false and the file already exists on the default branch, target a + * `.sample.yml` path instead. + */ + async function resolveOutputPath (context, filePath, overwrite) { + if (overwrite) return filePath + const { owner } = context.repo() + try { + await context.octokit.rest.repos.getContent({ owner, repo: env.ADMIN_REPO, path: filePath }) + // File exists -> redirect to .sample + return filePath.replace(/(\.ya?ml)$/i, '.sample$1') + } catch (e) { + if (e.status === 404) return filePath + throw e + } + } + + /** + * Create a branch on the admin repo, commit the generated file, and open a PR. + */ + async function openSettingsPR (context, filePath, content, opts) { + const github = context.octokit + const { owner } = context.repo() + const repo = env.ADMIN_REPO + + const repoInfo = await github.rest.repos.get({ owner, repo }) + const baseBranch = repoInfo.data.default_branch + const baseRef = await github.rest.git.getRef({ owner, repo, ref: `heads/${baseBranch}` }) + const branchName = `safe-settings-generate/${opts.sourceType}-${opts.sourceValue}-${Date.now()}`.replace(/[^a-zA-Z0-9/_.-]/g, '-') + + await github.rest.git.createRef({ + owner, + repo, + ref: `refs/heads/${branchName}`, + sha: baseRef.data.object.sha + }) + + let existingSha + try { + const existing = await github.rest.repos.getContent({ owner, repo, path: filePath, ref: branchName }) + existingSha = existing.data.sha + } catch (e) { + if (e.status !== 404) throw e + } + + await github.rest.repos.createOrUpdateFileContents({ + owner, + repo, + path: filePath, + branch: branchName, + message: `Generate ${filePath} from current ${opts.sourceType} settings`, + content: Buffer.from(content).toString('base64'), + sha: existingSha + }) + + const pr = await github.rest.pulls.create({ + owner, + repo, + title: `Generate safe-settings config for ${opts.sourceType}: ${opts.sourceValue}`, + head: branchName, + base: baseBranch, + body: [ + `Auto-generated safe-settings configuration from the current state of \`${opts.sourceType}\` \`${opts.sourceValue}\`.`, + '', + `- File: \`${filePath}\``, + `- Overwrite: \`${!!opts.overwrite}\``, + '', + 'Review carefully before merging. Run in nop mode to confirm there are no unexpected diffs.' + ].join('\n') + }) + + robot.log.info(`Opened settings-generation PR #${pr.data.number} (${filePath})`) + return pr.data + } + + // Trigger generation via a repository_dispatch event: + // event_type: safe-settings-generate + // client_payload: { source_type, source_value, overwrite, property_name? } + robot.on('repository_dispatch', async context => { + const { payload } = context + if (payload.action !== 'safe-settings-generate') { + robot.log.debug(`Ignoring repository_dispatch action "${payload.action}"`) + return + } + const cp = payload.client_payload || {} + const sourceType = cp.source_type + const sourceValue = cp.source_value + if (!sourceType || !sourceValue) { + robot.log.error('repository_dispatch safe-settings-generate requires source_type and source_value') + return + } + try { + return await generateSettings(context, { + sourceType, + sourceValue, + propertyName: cp.property_name, + overwrite: cp.overwrite === true || cp.overwrite === 'true' + }) + } catch (e) { + robot.log.error(`Failed to generate settings: ${e.stack || e}`) + throw e + } + }) + if (process.env.CRON) { /* # ┌────────────── second (optional) @@ -672,6 +1017,7 @@ module.exports = (robot, { getRouter }, Settings = require('./lib/settings')) => info() return { - syncInstallation + syncInstallation, + generateSettings } } diff --git a/lib/appOctokitClient.js b/lib/appOctokitClient.js new file mode 100644 index 000000000..c888f97eb --- /dev/null +++ b/lib/appOctokitClient.js @@ -0,0 +1,189 @@ +const BATCH_SIZE = 50 +const API_VERSION = '2026-03-10' + +/** + * AppOctokitClient wraps an Octokit client authenticated as the GitHub App at + * the enterprise level and provides methods for managing GitHub App + * installation repository access via the Enterprise Organization Installations + * API. + * + * All endpoints are org-scoped under + * `/enterprises/{enterprise}/apps/organizations/{org}/...` and operate on + * repository **names** (not IDs). Add/remove are capped at 50 repos per call + * and are auto-batched here. + * + * Prerequisites: + * - safe-settings must be installed on the enterprise with the + * "Enterprise organization installations" permission. + * - The enterprise slug is obtained from the webhook event payload + * (payload.enterprise.slug). + * + * @see https://docs.github.com/en/enterprise-cloud@latest/rest/enterprise-admin/organization-installations + * + * @param {object} options + * @param {object} options.github - Octokit client authenticated as the app at the enterprise installation + * @param {string} options.enterpriseSlug - Enterprise slug from webhook payload + * @param {object} options.log - Logger instance + */ +class AppOctokitClient { + constructor ({ github, enterpriseSlug, log }) { + this.github = github + this.enterpriseSlug = enterpriseSlug + this.log = log + } + + /** + * List the GitHub App installations on an enterprise-owned organization. + * Returns array of installation objects with + * { id, app_slug, client_id, repository_selection, ... } + * + * @param {string} org - Organization login name + * @returns {Promise} List of installations + */ + async listOrgInstallations (org) { + try { + const options = this.github.request.endpoint.merge( + 'GET /enterprises/{enterprise}/apps/organizations/{org}/installations', + { + enterprise: this.enterpriseSlug, + org, + headers: { 'X-GitHub-Api-Version': API_VERSION } + } + ) + return await this.github.paginate(options) + } catch (e) { + if (e.status === 403 || e.status === 404) { + throw new Error( + `Cannot access enterprise installations API. Ensure safe-settings is installed on the enterprise '${this.enterpriseSlug}' with 'Enterprise organization installations' permission. Error: ${e.message}` + ) + } + throw e + } + } + + /** + * List repositories accessible to an app installation on an org. + * Returns array of { id, name, full_name }. + * + * @param {string} org - Organization login name + * @param {number} installationId - The installation ID + * @returns {Promise} List of repository objects + */ + async listInstallationRepos (org, installationId) { + try { + const options = this.github.request.endpoint.merge( + 'GET /enterprises/{enterprise}/apps/organizations/{org}/installations/{installation_id}/repositories', + { + enterprise: this.enterpriseSlug, + org, + installation_id: installationId, + headers: { 'X-GitHub-Api-Version': API_VERSION } + } + ) + return await this.github.paginate(options) + } catch (e) { + this.log.error(`Error listing repos for installation ${installationId}: ${e.message}`) + throw e + } + } + + /** + * Toggle an installation's repository access between 'all' and 'selected'. + * When setting 'selected', `repositories` (names) must contain at least one + * repo. When setting 'all', `repositories` must be omitted. + * + * @param {string} org - Organization login name + * @param {number} installationId - The installation ID + * @param {('all'|'selected')} selection - Desired repository selection + * @param {string[]} [repositories] - Repo names (required for 'selected') + * @returns {Promise} + */ + async setRepositorySelection (org, installationId, selection, repositories) { + const params = { + enterprise: this.enterpriseSlug, + org, + installation_id: installationId, + repository_selection: selection, + headers: { 'X-GitHub-Api-Version': API_VERSION } + } + if (selection === 'selected') { + params.repositories = repositories || [] + } + this.log.debug(`Setting repository_selection='${selection}' for installation ${installationId}`) + await this.github.request( + 'PATCH /enterprises/{enterprise}/apps/organizations/{org}/installations/{installation_id}/repositories', + params + ) + } + + /** + * Grant repository access to an org installation. + * Automatically batches into chunks of 50 (API limit). + * + * @param {string} org - Organization login name + * @param {number} installationId - The installation ID + * @param {string[]} repositoryNames - Repo names to add + * @returns {Promise} + */ + async addReposToInstallation (org, installationId, repositoryNames) { + if (!repositoryNames || repositoryNames.length === 0) return + + for (const batch of this._chunk(repositoryNames, BATCH_SIZE)) { + this.log.debug(`Adding ${batch.length} repos to installation ${installationId}`) + await this.github.request( + 'PATCH /enterprises/{enterprise}/apps/organizations/{org}/installations/{installation_id}/repositories/add', + { + enterprise: this.enterpriseSlug, + org, + installation_id: installationId, + repositories: batch, + headers: { 'X-GitHub-Api-Version': API_VERSION } + } + ) + } + } + + /** + * Remove repository access from an org installation. + * Automatically batches into chunks of 50 (API limit). + * + * Note: the API returns 422 if you attempt to remove repos from an + * installation set to 'all', or remove the last remaining repository. + * + * @param {string} org - Organization login name + * @param {number} installationId - The installation ID + * @param {string[]} repositoryNames - Repo names to remove + * @returns {Promise} + */ + async removeReposFromInstallation (org, installationId, repositoryNames) { + if (!repositoryNames || repositoryNames.length === 0) return + + for (const batch of this._chunk(repositoryNames, BATCH_SIZE)) { + this.log.debug(`Removing ${batch.length} repos from installation ${installationId}`) + await this.github.request( + 'PATCH /enterprises/{enterprise}/apps/organizations/{org}/installations/{installation_id}/repositories/remove', + { + enterprise: this.enterpriseSlug, + org, + installation_id: installationId, + repositories: batch, + headers: { 'X-GitHub-Api-Version': API_VERSION } + } + ) + } + } + + /** + * Split an array into chunks of the given size. + * @private + */ + _chunk (array, size) { + const chunks = [] + for (let i = 0; i < array.length; i += size) { + chunks.push(array.slice(i, i + size)) + } + return chunks + } +} + +module.exports = AppOctokitClient diff --git a/lib/commentmessage.js b/lib/commentmessage.js index b54f81bb4..dc932402b 100644 --- a/lib/commentmessage.js +++ b/lib/commentmessage.js @@ -1,31 +1,38 @@ -module.exports = `* Run on: \` <%= new Date() %> \` +module.exports = `<% const esc = s => String(s).replace(/&/g, "&").replace(//g, ">") %>Run on: \`<%= new Date().toISOString() %>\` -* Number of repos that were considered: \`<%= Object.keys(it.reposProcessed).length %> \` +* Number of repos considered: \`<%= Object.keys(it.reposProcessed).length %>\` +* Number of repos affected: \`<%= it.reposAffected || 0 %>\` ### Breakdown of changes -| Repo <% Object.keys(it.changes).forEach(plugin => { %> | <%= plugin %> settings <% }) %> | -| -- <% Object.keys(it.changes).forEach(plugin => { -%> | -- <% }) %> -| -<% Object.keys(it.reposProcessed).forEach( repo => { -%> -| <%= repo -%> - <%- Object.keys(it.changes).forEach(plugin => { -%> - <%_ if (it.changes[plugin][repo]) { -%> | :hand: <% } else { %> | :grey_exclamation: <% } -%> - <%_ }) -%> | -<% }) -%> - -:hand: -> Changes to be applied to the GitHub repository. -:grey_exclamation: -> nothing to be changed in that particular GitHub repository. + +<% if (!it.checkRunDetails || it.checkRunDetails.length === 0) { %> +No changes to apply. +<% } else { %> +<%~ it.checkRunDetails %> +<% } %> ### Breakdown of errors <% if (Object.keys(it.errors).length === 0) { %> \`None\` <% } else { %> - <% Object.keys(it.errors).forEach(repo => { %> - <%_= repo %>: - <% it.errors[repo].forEach(plugin => { %> - * <%= plugin.msg %> - <% }) %> +
+:warning: Errors by repo — <%= Object.keys(it.errors).length %> repo(s) affected + +<%~ Object.keys(it.errors).map(repo => "**" + esc(repo) + "**:\\n" + it.errors[repo].map(err => "* " + esc(err.msg)).join("\\n")).join("\\n\\n") %> + +
+<% } %> + +### Informational messages + +<% if (!it.infos || Object.keys(it.infos).length === 0) { %> +\`None\` +<% } else { %> +
+:information_source: Info — <%= Object.keys(it.infos).length %> repo(s) + +<%~ Object.keys(it.infos).map(repo => "**" + esc(repo) + "**:\\n" + it.infos[repo].map(msg => "* ℹ️ " + esc(msg)).join("\\n")).join("\\n\\n") %> - <% }) %> +
<% } %>` diff --git a/lib/configManager.js b/lib/configManager.js index 58f5bb436..b3022e1b8 100644 --- a/lib/configManager.js +++ b/lib/configManager.js @@ -19,9 +19,7 @@ module.exports = class ConfigManager { try { const repo = { owner: this.context.repo().owner, repo: env.ADMIN_REPO } const params = Object.assign(repo, { path: filePath, ref: this.ref }) - const response = await this.context.octokit.repos.getContent(params).catch(e => { - this.log.error(`Error getting settings ${e}`) - }) + const response = await this.context.octokit.rest.repos.getContent(params) // Ignore in case path is a folder // - https://developer.github.com/v3/repos/contents/#response-if-content-is-a-directory diff --git a/lib/env.js b/lib/env.js index 94c0ea742..8ed5d927e 100644 --- a/lib/env.js +++ b/lib/env.js @@ -6,5 +6,7 @@ module.exports = { CREATE_PR_COMMENT: process.env.CREATE_PR_COMMENT || 'true', CREATE_ERROR_ISSUE: process.env.CREATE_ERROR_ISSUE || 'true', BLOCK_REPO_RENAME_BY_HUMAN: process.env.BLOCK_REPO_RENAME_BY_HUMAN || 'false', - FULL_SYNC_NOP: process.env.FULL_SYNC_NOP === 'true' + FULL_SYNC_NOP: process.env.FULL_SYNC_NOP === 'true', + GHE_HOST: process.env.GHE_HOST, + GHE_PROTOCOL: process.env.GHE_PROTOCOL, } diff --git a/lib/mergeDeep.js b/lib/mergeDeep.js index ab278e5c2..32d5e972c 100644 --- a/lib/mergeDeep.js +++ b/lib/mergeDeep.js @@ -5,6 +5,44 @@ const NAME_FIELDS = ['name', 'username', 'actor_id', 'login', 'type', 'key_prefi const NAME_USERNAME_PROPERTY = item => NAME_FIELDS.find(prop => Object.prototype.hasOwnProperty.call(item, prop)) const GET_NAME_USERNAME_PROPERTY = item => { if (NAME_USERNAME_PROPERTY(item)) return item[NAME_USERNAME_PROPERTY(item)] } +// Fields within a rule's `parameters` that are managed/defaulted by the GitHub API. +// They should not be treated as user-driven deletions when omitted from config. +const PARAM_DELETION_IGNORE = ['allowed_merge_methods'] + +// Order-insensitive JSON serialization used as a fallback identity for array +// elements that have no named identifying field (e.g. `code_scanning_tools`). +// The GitHub API often returns object keys in a different order than config, so +// a plain JSON.stringify would treat semantically-equal items as different and +// produce spurious add/delete churn. Sorting keys recursively avoids that. +const stableStringify = value => { + if (Array.isArray(value)) { + return `[${value.map(stableStringify).join(',')}]` + } + if (value && typeof value === 'object') { + return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}` + } + return JSON.stringify(value) +} + +// Compute the identity value for an array element so the same logical item in +// `source` (config) and `target` (GitHub API) can be paired during comparison. +// Returns the raw identifying value (so a bare string shorthand like 'developers' +// still matches an object like { name: 'developers' }). Special-cases bypass +// actors: GitHub returns `actor_id: null` for role-based actor types such as +// `OrganizationAdmin`, so we key those on `actor_type` to avoid spurious +// add/delete churn when config supplies an explicit id. +const getItemIdentity = item => { + if (!item || typeof item !== 'object' || Array.isArray(item)) return undefined + if (Object.prototype.hasOwnProperty.call(item, 'actor_type') && + Object.prototype.hasOwnProperty.call(item, 'bypass_mode')) { + if (item.actor_id === null || item.actor_id === undefined || item.actor_type === 'OrganizationAdmin') { + return item.actor_type + } + return item.actor_id + } + return GET_NAME_USERNAME_PROPERTY(item) +} + class MergeDeep { constructor (log, github, ignorableFields = [], configvalidators = {}, overridevalidators = {}) { this.log = log @@ -53,7 +91,7 @@ class MergeDeep { * @param {*} deletions aggregated so far * @returns object with additions, modifications, and deletions */ - compareDeep (t, s, additions, modifications, deletions) { + compareDeep (t, s, additions, modifications, deletions, parentKey) { // Preemtively return if the source is not an object or array if (!this.isObject(s)) { return { additions, modifications, deletions, hasChanges: s !== t } @@ -92,8 +130,8 @@ class MergeDeep { // So any property in the target that is not in the source is not treated as a deletion for (const key in source) { // Skip prototype pollution vectors - if (key === "__proto__" || key === "constructor") { - continue; + if (key === '__proto__' || key === 'constructor') { + continue } // Logic specific for Github // API response includes urls for resources, or other ignorable fields; we can ignore them @@ -126,18 +164,25 @@ class MergeDeep { this.processArrays(key, sourceValue, targetValue, deletions, additions, modifications) } else { // recursively compare the objects until we reach a primitive - this.compareDeep(targetValue, sourceValue, additions[key], modifications[key], deletions[key]) + this.compareDeep(targetValue, sourceValue, additions[key], modifications[key], deletions[key], key) this.validateOverride(key, targetValue, sourceValue) } } else { // The entry is a simple primitive if (targetValue !== sourceValue) { - // Note: source[key] cannot be undefined here since we are iterating on source keys - // so we don't need to check for that. - // The entries are different. It is an addition - modifications[key] = sourceValue - // retroactively add `name` or `username` to the modifications - // Since those are the only fields that can be used to identify the resource - this.addIdentifyingAttribute(source, key, modifications) + // GitHub returns `actor_id: null` for role-based bypass actor types + // (e.g. OrganizationAdmin) regardless of the id supplied in config. + // Don't treat that placeholder mismatch as a modification. + if (key === 'actor_id' && (targetValue === null || sourceValue === null)) { + // treat as equal + } else { + // Note: source[key] cannot be undefined here since we are iterating on source keys + // so we don't need to check for that. + // The entries are different. It is an addition + modifications[key] = sourceValue + // retroactively add `name` or `username` to the modifications + // Since those are the only fields that can be used to identify the resource + this.addIdentifyingAttribute(source, key, modifications) + } } else { // The entry is the same in both objects } @@ -147,6 +192,22 @@ class MergeDeep { additions = this.removeEmptyAndNulls(additions, key) deletions = this.removeEmptyAndNulls(deletions, key) } + + // Detect deletions for config-meaningful nested objects (e.g. a rule's + // `parameters`). The GitHub API is additive for top-level metadata, so we + // only do this for known config subtrees to avoid flagging server-managed + // or metadata fields (timestamps, _links, source_type, etc.) as deletions. + if (parentKey === 'parameters' && this.isObjectNotArray(target) && this.isObjectNotArray(source)) { + for (const key in target) { + if (key === '__proto__' || key === 'constructor') continue + if (key.indexOf('url') >= 0 || this.ignorableFields.indexOf(key) >= 0) continue + if (PARAM_DELETION_IGNORE.indexOf(key) >= 0) continue + if (!(key in source)) { + // Present in GitHub but removed from config => a deletion + deletions[key] = target[key] + } + } + } // Unwind the topleve array from the object if (firstInvocation) { if (additions.__array) { @@ -184,7 +245,8 @@ class MergeDeep { if (source.length < target.length) { const dels = target.filter(item => { if (this.isObjectNotArray(item)) { - return !source.some(sourceItem => GET_NAME_USERNAME_PROPERTY(item) === GET_NAME_USERNAME_PROPERTY(sourceItem)) + const itemId = getItemIdentity(item) || stableStringify(item) + return !source.some(sourceItem => (getItemIdentity(sourceItem) || stableStringify(sourceItem)) === itemId) } else { return !source.includes(item) } @@ -200,9 +262,10 @@ class MergeDeep { continue } else { // Not visited yet - const id = GET_NAME_USERNAME_PROPERTY(a) - if (id) { - visited[id] = a + // Use identifying property (name, username, actor_type, etc.) or fall back to JSON representation for objects without named properties + const visitedId = getItemIdentity(a) || stableStringify(a) + if (!visited[visitedId]) { + visited[visitedId] = a } } } else { @@ -222,7 +285,11 @@ class MergeDeep { // Elements that are not in target are additions additions[key] = combined.filter(item => { if (this.isObjectNotArray(item)) { - return !target.some(targetItem => GET_NAME_USERNAME_PROPERTY(item) === GET_NAME_USERNAME_PROPERTY(targetItem)) + const itemId = getItemIdentity(item) || stableStringify(item) + return !target.some(targetItem => { + const targetId = getItemIdentity(targetItem) || stableStringify(targetItem) + return itemId === targetId + }) } else { return !target.includes(item) } @@ -233,7 +300,11 @@ class MergeDeep { // Elements that not in source are deletions deletions[key] = combined.filter(item => { if (this.isObjectNotArray(item)) { - return !source.some(sourceItem => GET_NAME_USERNAME_PROPERTY(item) === GET_NAME_USERNAME_PROPERTY(sourceItem)) + const itemId = getItemIdentity(item) || stableStringify(item) + return !source.some(sourceItem => { + const sourceId = getItemIdentity(sourceItem) || stableStringify(sourceItem) + return itemId === sourceId + }) } else { return !source.includes(item) } @@ -242,14 +313,15 @@ class MergeDeep { } compareDeepIfVisited (additions, modifications, deletions, a, visited) { - const id = GET_NAME_USERNAME_PROPERTY(a) - if (visited[id]) { + // Use identifying property or fall back to JSON representation for objects without named properties + const visitedId = getItemIdentity(a) || stableStringify(a) + if (visited[visitedId]) { // Common array in target and source modifications.push({}) additions.push({}) deletions.push({}) - if (visited[id]) { - this.compareDeep(a, visited[id], additions[additions.length - 1], modifications[modifications.length - 1], deletions[deletions.length - 1]) + if (visited[visitedId]) { + this.compareDeep(a, visited[visitedId], additions[additions.length - 1], modifications[modifications.length - 1], deletions[deletions.length - 1]) } // Any addtions for the matching key must be moved to modifications const lastAddition = additions[additions.length - 1] @@ -257,25 +329,40 @@ class MergeDeep { if (!this.isEmpty(additions)) { for (const key in lastAddition) { - if (!lastModification[key]) { - lastModification[key] = Array.isArray(lastAddition[key]) ? [] : {} - } - if (!Array.isArray(lastAddition[key])) { - Object.assign(lastModification[key], lastAddition[key]) + const addedValue = lastAddition[key] + if (Array.isArray(addedValue)) { + if (!Array.isArray(lastModification[key])) { + lastModification[key] = [] + } + lastModification[key].push(...addedValue) + } else if (this.isObjectNotArray(addedValue)) { + if (!this.isObjectNotArray(lastModification[key])) { + lastModification[key] = {} + } + Object.assign(lastModification[key], addedValue) } else { - lastModification[key].push(...lastAddition[key]) + // Primitive value (e.g. the `name`/`permission` identifying + // attribute added by addIdentifyingAttribute). Object.assign + // would box a primitive string target and throw + // "Cannot assign to read only property '0' of object + // '[object String]'" since string indices are read-only, so + // just overwrite directly instead of merging. + lastModification[key] = addedValue } } additions.length = 0 } // Add name attribute to the modifications to make it look better ; it won't be added otherwise as it would be the same if (!this.isEmpty(modifications[modifications.length - 1])) { - if (visited[id]) { - modifications[modifications.length - 1][NAME_USERNAME_PROPERTY(a)] = id + if (visited[visitedId]) { + const displayProp = NAME_USERNAME_PROPERTY(a) + if (displayProp) { + modifications[modifications.length - 1][displayProp] = a[displayProp] + } } } - if (visited[id]) { - delete visited[id] + if (visited[visitedId]) { + delete visited[visitedId] } return true } diff --git a/lib/nopcommand.js b/lib/nopcommand.js index 75965a8ba..133eae48d 100644 --- a/lib/nopcommand.js +++ b/lib/nopcommand.js @@ -1,8 +1,22 @@ class NopCommand { - constructor (pluginName, repo, endpoint, action, type = 'INFO') { + constructor (pluginName, repo, endpoint, action, type = 'INFO', subject = null) { + // Ergonomic overload: allow passing the subject in the `type` position so + // callers can supply a subject while omitting the (default 'INFO') type, + // e.g. new NopCommand(plugin, repo, endpoint, action, { name, type }). + if (type !== null && typeof type === 'object') { + subject = type + type = 'INFO' + } this.type = type this.plugin = pluginName this.repo = repo.repo + // Optional presentation subject. Some plugins (e.g. app_installations) + // operate on a non-repo entity — the "subject" of the change is a GitHub + // App, not a repository. `subject` overrides the repo as the heading in the + // PR-comment/check-run report, while `repo` is still used for grouping and + // repo counts. Defaults to the repo so existing plugins are unaffected. + this.subject = (subject && subject.name) || repo.repo + this.subjectType = (subject && subject.type) || 'repo' this.endpoint = endpoint ? endpoint.url : '' this.body = endpoint ? endpoint.body : '' // check if action is a string diff --git a/lib/plugins/appInstallations.js b/lib/plugins/appInstallations.js new file mode 100644 index 000000000..7eea8432a --- /dev/null +++ b/lib/plugins/appInstallations.js @@ -0,0 +1,389 @@ +/* eslint-disable camelcase */ +const NopCommand = require('../nopcommand') +const AppOctokitClient = require('../appOctokitClient') + +/** + * AppInstallations plugin manages which repositories are accessible to + * GitHub App installations in the organization. + * + * Unlike repo-targeting plugins (which extend Diffable), this plugin + * operates at the org level — the "target" is an app installation, + * not a repository. + * + * Supports: + * - Delta-based sync (incremental changes from config file diffs) + * - Full sync (compare desired state against live API state) + * - disable_plugins (skipped when disabled) + * - additive_plugins (only adds repos, never removes) + */ +class AppInstallations { + /** + * @param {boolean} nop - Dry-run mode + * @param {object} github - Octokit client (installation-authenticated) + * @param {object} appGithub - Octokit client (app-authenticated, for enterprise API) + * @param {object} repo - { owner, repo } context + * @param {string} enterpriseSlug - Enterprise slug from webhook payload + * @param {object} log - Logger + * @param {Array} errors - Shared errors array + */ + constructor (nop, github, appGithub, repo, enterpriseSlug, log, errors) { + this.nop = nop + this.github = github + this.repo = repo + this.org = repo.owner + this.log = log + this.errors = errors || [] + this.additive = false + + if (appGithub && enterpriseSlug) { + this.enterpriseClient = new AppOctokitClient({ + github: appGithub, + enterpriseSlug, + log + }) + } + } + + /** + * Delta-based sync: process pre-computed per-app changes. + * + * @param {Array} appChanges - Array of per-app change objects: + * { + * app_slug: string, + * installation_id: number, + * repository_selection: Set | Array | 'all', // repos to add + * repository_unselection: Set | Array, // repos to remove + * } + * @returns {Promise} NopCommand results (in nop mode) or empty + */ + async syncDelta (appChanges) { + const results = [] + + if (!appChanges || appChanges.length === 0) return results + + for (const change of appChanges) { + try { + const appResults = await this._processAppChange(change) + results.push(...appResults) + } catch (e) { + this.log.error(`Error processing app installation '${change.app_slug}': ${e.message}`) + this.errors.push({ + owner: this.repo.owner, + repo: this.repo.repo, + msg: e.message, + plugin: 'app_installations' + }) + if (this.nop) { + results.push(new NopCommand( + 'app_installations', + this.repo, + null, + `Error: ${e.message}`, + 'ERROR' + )) + } + } + } + + return results + } + + /** + * Full sync: compute full desired state for all managed apps, + * compare against live API state, and reconcile. + * + * @param {object} desiredState - Map of app_slug → { + * installation_id, repos: Set | 'all', current_selection: 'all' | 'selected' + * } + * @returns {Promise} NopCommand results (in nop mode) or empty + */ + async syncFull (desiredState) { + const results = [] + + if (!desiredState || Object.keys(desiredState).length === 0) return results + + if (!this.enterpriseClient) { + const msg = 'Cannot sync app installations: enterprise client not configured. Ensure safe-settings is installed on the enterprise.' + this.log.error(msg) + if (this.nop) { + results.push(new NopCommand('app_installations', this.repo, null, msg, 'ERROR')) + } + return results + } + + for (const [appSlug, desired] of Object.entries(desiredState)) { + try { + const appResults = await this._reconcileApp(appSlug, desired) + results.push(...appResults) + } catch (e) { + this.log.error(`Error in full sync for app '${appSlug}': ${e.message}`) + this.errors.push({ + owner: this.repo.owner, + repo: this.repo.repo, + msg: e.message, + plugin: 'app_installations' + }) + if (this.nop) { + results.push(new NopCommand('app_installations', this.repo, null, `Error: ${e.message}`, 'ERROR')) + } + } + } + + return results + } + + /** + * Reconcile a single app's desired state against its live installation state. + * @private + */ + async _reconcileApp (appSlug, desired) { + const results = [] + const { installation_id, repos, current_selection } = desired + + // Desired = all repos in the org → toggle the installation to 'all'. + if (repos === 'all') { + if (current_selection === 'all') { + this.log.debug(`App '${appSlug}': already set to all repositories, no change`) + return results + } + if (this.nop) { + results.push(new NopCommand('app_installations', this.repo, null, { + msg: `App '${appSlug}': set repository_selection to 'all'`, + additions: ['(all repositories)'], + modifications: null, + deletions: null + }, { name: appSlug, type: 'app' })) + return results + } + await this.enterpriseClient.setRepositorySelection(this.org, installation_id, 'all') + this.log.debug(`App '${appSlug}': set repository_selection to 'all'`) + return results + } + + const desiredNames = repos instanceof Set ? repos : new Set(repos) + + // Installation is currently 'all' but desired is a specific set → switch + // the installation to 'selected' with the desired repos. In additive mode + // we must not narrow access, so leave 'all' untouched. + if (current_selection === 'all') { + if (this.additive) { + this.log.debug(`App '${appSlug}': additive mode, leaving 'all' selection untouched`) + return results + } + if (desiredNames.size === 0) { + // The desired set is empty but the installation is 'all'. We cannot + // narrow to zero repositories (the API rejects selecting none), so the + // over-broad 'all' access would otherwise be silently preserved. + // Surface this as an error so operators notice the (likely) + // misconfiguration; the installation is left unchanged. + const msg = `App '${appSlug}': desired repo set is empty, so safe-settings cannot narrow repository_selection from 'all' to 'selected' (the API rejects selecting zero repositories). Add at least one repository to the app's configuration, set the app to all repos at the org level, or remove the app_installations entry.` + this.log.error(msg) + this.errors.push({ owner: this.repo.owner, repo: this.repo.repo, msg, plugin: 'app_installations' }) + if (this.nop) { + results.push(new NopCommand('app_installations', this.repo, null, msg, 'ERROR', { name: appSlug, type: 'app' })) + } + return results + } + if (this.nop) { + results.push(new NopCommand('app_installations', this.repo, null, { + msg: `App '${appSlug}': narrow repository_selection from 'all' to selected`, + additions: [...desiredNames], + modifications: null, + deletions: ['(all repositories)'] + }, { name: appSlug, type: 'app' })) + return results + } + await this.enterpriseClient.setRepositorySelection(this.org, installation_id, 'selected', [...desiredNames]) + this.log.debug(`App '${appSlug}': set repository_selection to 'selected' with ${desiredNames.size} repos`) + return results + } + + // Installation is 'selected' → diff against live repos and add/remove. + const liveRepos = await this.enterpriseClient.listInstallationRepos(this.org, installation_id) + const liveRepoNames = new Set(liveRepos.map(r => r.name)) + + const toAdd = [...desiredNames].filter(r => !liveRepoNames.has(r)) + const toRemove = this.additive + ? [] // Additive mode: never remove + : [...liveRepoNames].filter(r => !desiredNames.has(r)) + + if (toAdd.length === 0 && toRemove.length === 0) { + this.log.debug(`App '${appSlug}': no changes needed`) + return results + } + + // A 'selected' installation must retain at least one repository. When the + // resolved desired set is empty we would have to remove every repo, which + // the Enterprise API rejects (422). There is no valid reconciliation to + // zero repos — surface a descriptive error instead of attempting it. + if (desiredNames.size === 0) { + const msg = `App '${appSlug}': cannot reconcile a 'selected' installation to zero repositories (the resolved repo set is empty). Add at least one repository to the app's configuration, set the app to all repos at the org level, or remove the app_installations entry.` + this.log.error(msg) + this.errors.push({ owner: this.repo.owner, repo: this.repo.repo, msg, plugin: 'app_installations' }) + if (this.nop) { + results.push(new NopCommand('app_installations', this.repo, null, msg, 'ERROR', { name: appSlug, type: 'app' })) + } + return results + } + + if (this.nop) { + results.push(new NopCommand('app_installations', this.repo, null, { + msg: `App '${appSlug}' installation repos`, + additions: toAdd.length > 0 ? toAdd : null, + modifications: null, + deletions: toRemove.length > 0 ? toRemove : null + }, { name: appSlug, type: 'app' })) + return results + } + + // Apply additions BEFORE removals. In a swap (e.g. live={A}, desired={B}) + // removing first would momentarily drop the installation to zero repos, + // which the Enterprise API rejects with 422 ('selected' installations must + // keep at least one repo). Adding first guarantees the installation never + // passes through an empty state. In full sync toAdd/toRemove are disjoint, + // so ordering does not change the final repo set. + if (toAdd.length > 0) { + await this.enterpriseClient.addReposToInstallation(this.org, installation_id, toAdd) + this.log.debug(`App '${appSlug}': added ${toAdd.length} repos`) + } + if (toRemove.length > 0) { + try { + await this.enterpriseClient.removeReposFromInstallation(this.org, installation_id, toRemove) + this.log.debug(`App '${appSlug}': removed ${toRemove.length} repos`) + } catch (e) { + if (e && e.status === 422) { + const msg = `App '${appSlug}': removing ${toRemove.length} repo(s) was rejected by the Enterprise API (422); a 'selected' installation must retain at least one repository.` + this.log.error(msg) + this.errors.push({ owner: this.repo.owner, repo: this.repo.repo, msg, plugin: 'app_installations' }) + return results + } + throw e + } + } + + return results + } + + /** + * Process a single app's delta change. + * @private + */ + async _processAppChange (change) { + const results = [] + const { app_slug, installation_id } = change + + // Normalise selection/unselection to Sets. Delta changes computed by + // Settings._buildAppChangesFromDelta arrive as arrays, while direct callers + // and unit tests pass Sets — accept both. 'all' is a sentinel for the + // whole-org toggle and is handled separately below. + const repository_selection = change.repository_selection === 'all' + ? 'all' + : (change.repository_selection instanceof Set + ? change.repository_selection + : new Set(change.repository_selection || [])) + const repository_unselection = change.repository_unselection instanceof Set + ? change.repository_unselection + : new Set(change.repository_unselection || []) + + if (!this.enterpriseClient) { + const msg = 'Cannot sync app installations: enterprise client not configured. Ensure safe-settings is installed on the enterprise.' + this.log.error(msg) + this.errors.push({ + owner: this.repo.owner, + repo: this.repo.repo, + msg, + plugin: 'app_installations' + }) + if (this.nop) { + results.push(new NopCommand('app_installations', this.repo, null, msg, 'ERROR')) + } + return results + } + + const hasSelections = repository_selection === 'all' || + (repository_selection instanceof Set && repository_selection.size > 0) + const hasUnselections = !this.additive && + (repository_unselection instanceof Set && repository_unselection.size > 0) + + if (!hasSelections && !hasUnselections) { + return results + } + + // Handle "all" selection — toggle the installation to 'all' via the API + if (repository_selection === 'all') { + if (this.nop) { + results.push(new NopCommand( + 'app_installations', + this.repo, + null, + { + msg: `App '${app_slug}': set repository_selection to 'all'`, + additions: ['(all repositories)'], + modifications: null, + deletions: null + }, + { name: app_slug, type: 'app' } + )) + return results + } + + await this.enterpriseClient.setRepositorySelection(this.org, installation_id, 'all') + this.log.debug(`App '${app_slug}': set repository_selection to 'all'`) + return results + } + + // Handle specific repos + if (this.nop) { + const additions = hasSelections ? [...repository_selection] : null + const deletions = hasUnselections ? [...repository_unselection] : null + results.push(new NopCommand( + 'app_installations', + this.repo, + null, + { + msg: `App '${app_slug}' installation repos`, + additions, + modifications: null, + deletions + }, + { name: app_slug, type: 'app' } + )) + return results + } + + // Apply additions BEFORE removals. Delta selection/unselection sets are + // disjoint (Settings._buildAppChangesFromDelta drops any repo appearing in + // both, selection winning), so ordering does not change the final set. + // Adding first avoids a transient empty selection during a "swap" (e.g. a + // suborg retargeted from repo-A to repo-B where the installation currently + // holds only repo-A), which the Enterprise API rejects with 422 ('selected' + // installations must keep at least one repo). + if (hasSelections) { + await this.enterpriseClient.addReposToInstallation(this.org, installation_id, [...repository_selection]) + this.log.debug(`App '${app_slug}': added ${repository_selection.size} repos`) + } + + if (hasUnselections) { + try { + await this.enterpriseClient.removeReposFromInstallation(this.org, installation_id, [...repository_unselection]) + this.log.debug(`App '${app_slug}': removed ${repository_unselection.size} repos`) + } catch (e) { + if (e && e.status === 422) { + // Delta does not fetch live installation state, so it cannot detect + // up-front that a removal would drop the installation to zero repos. + // Surface a descriptive error; the scheduled full sync reconciles the + // correct end state. + const msg = `App '${app_slug}': removing ${repository_unselection.size} repo(s) was rejected by the Enterprise API (422); a 'selected' installation must retain at least one repository. This will be reconciled on the next full sync.` + this.log.error(msg) + this.errors.push({ owner: this.repo.owner, repo: this.repo.repo, msg, plugin: 'app_installations' }) + return results + } + throw e + } + } + + return results + } +} + +module.exports = AppInstallations diff --git a/lib/plugins/archive.js b/lib/plugins/archive.js index f3d7c4c3a..c00c827bb 100644 --- a/lib/plugins/archive.js +++ b/lib/plugins/archive.js @@ -11,7 +11,7 @@ module.exports = class Archive { async getRepo () { try { - const { data } = await this.github.repos.get({ + const { data } = await this.github.rest.repos.get({ owner: this.repo.owner, repo: this.repo.repo }) @@ -32,13 +32,13 @@ module.exports = class Archive { return new NopCommand( this.constructor.name, this.repo, - this.github.repos.update.endpoint(this.settings), + this.github.rest.repos.update.endpoint(this.settings), change, 'INFO' ) } - const { data } = await this.github.repos.update({ + const { data } = await this.github.rest.repos.update({ owner: this.repo.owner, repo: this.repo.repo, archived diff --git a/lib/plugins/autolinks.js b/lib/plugins/autolinks.js index 128e40214..5145fe9cd 100644 --- a/lib/plugins/autolinks.js +++ b/lib/plugins/autolinks.js @@ -8,7 +8,7 @@ module.exports = class Autolinks extends Diffable { // } async find () { - const { data } = await this.github.repos.listAutolinks(this.repo) + const { data } = await this.github.rest.repos.listAutolinks(this.repo) return data } @@ -19,7 +19,7 @@ module.exports = class Autolinks extends Diffable { changed (existing, attr) { // is_alphanumeric was added mid-2023. In order to continue to support settings yamls which dont specify this // attribute, consider an unset is_alphanumeric as `true` (since that is the default value in the API) - // https://docs.github.com/en/rest/repos/autolinks?apiVersion=2022-11-28#create-an-autolink-reference-for-a-repository + // https://docs.github.com/en/rest/repos/autolinks?apiVersion=2026-03-10#create-an-autolink-reference-for-a-repository const isAlphaNumericMatch = attr.is_alphanumeric === undefined ? existing.is_alphanumeric // === true, the default : attr.is_alphanumeric === existing.is_alphanumeric @@ -43,13 +43,13 @@ module.exports = class Autolinks extends Diffable { return new NopCommand( this.constructor.name, this.repo, - this.github.repos.createAutolink.endpoint(attrs), + this.github.rest.repos.createAutolink.endpoint(attrs), 'Add autolink' ) } try { - return this.github.repos.createAutolink(attrs) + return this.github.rest.repos.createAutolink(attrs) } catch (e) { if (e?.response?.data?.errors?.[0]?.code === 'already_exists') { this.log.debug(`Did not update ${key_prefix}, as it already exists`) @@ -68,10 +68,10 @@ module.exports = class Autolinks extends Diffable { return new NopCommand( this.constructor.name, this.repo, - this.github.repos.deleteAutolink.endpoint(attrs), + this.github.rest.repos.deleteAutolink.endpoint(attrs), 'Remove autolink' ) } - return this.github.repos.deleteAutolink(attrs) + return this.github.rest.repos.deleteAutolink(attrs) } } diff --git a/lib/plugins/branches.js b/lib/plugins/branches.js index d28e2f905..ec3ab7c79 100644 --- a/lib/plugins/branches.js +++ b/lib/plugins/branches.js @@ -5,10 +5,18 @@ const Overrides = require('./overrides') const ignorableFields = [] const previewHeaders = { accept: 'application/vnd.github.hellcat-preview+json,application/vnd.github.luke-cage-preview+json,application/vnd.github.zzzax-preview+json' } const overrides = { - 'contexts': { - 'action': 'reset', - 'type': 'array' - }, + contexts: { + action: 'reset', + type: 'array' + } +} + +// GitHub API requires these fields to be present in updateBranchProtection calls +// See: https://docs.github.com/rest/branches/branch-protection#update-branch-protection +const requiredBranchProtectionDefaults = { + required_status_checks: null, + enforce_admins: null, + restrictions: null } module.exports = class Branches extends ErrorStash { @@ -23,7 +31,7 @@ module.exports = class Branches extends ErrorStash { sync () { const resArray = [] - return this.github.repos.get(this.repo).then((currentRepo) => { + return this.github.rest.repos.get(this.repo).then((currentRepo) => { return Promise.all( this.branches .filter(branch => branch.protection !== undefined) @@ -39,12 +47,12 @@ module.exports = class Branches extends ErrorStash { const params = Object.assign({}, p) if (this.nop) { resArray.push( - new NopCommand(this.constructor.name, this.repo, this.github.repos.deleteBranchProtection.endpoint(params), 'Delete Branch Protection') + new NopCommand(this.constructor.name, this.repo, this.github.rest.repos.deleteBranchProtection.endpoint(params), 'Delete Branch Protection') ) return Promise.resolve(resArray) } - return this.github.repos.deleteBranchProtection(params).catch(e => { return [] }) + return this.github.rest.repos.deleteBranchProtection(params).catch(e => { return [] }) } else { // Branch protection is not empty let p = Object.assign(this.repo, { branch: branch.name }) @@ -54,11 +62,11 @@ module.exports = class Branches extends ErrorStash { } // Hack to handle closures and keep params from changing const params = Object.assign({}, p) - return this.github.repos.getBranchProtection(params).then((result) => { + return this.github.rest.repos.getBranchProtection(params).then((result) => { const mergeDeep = new MergeDeep(this.log, this.github, ignorableFields) - const changes = mergeDeep.compareDeep({ branch: { protection: this.reformatAndReturnBranchProtection(result.data) } }, { branch: { protection: Overrides.removeOverrides(overrides, branch.protection, result.data) } }) - const results = { msg: `Followings changes will be applied to the branch protection for ${params.branch.name} branch`, additions: changes.additions, modifications: changes.modifications, deletions: changes.deletions } - this.log.debug(`Result of compareDeep = ${results}`) + const changes = mergeDeep.compareDeep({ branch: { protection: this.reformatAndReturnBranchProtection(structuredClone(result.data)) } }, { branch: { protection: Overrides.removeOverrides(overrides, branch.protection, result.data) } }) + const results = { msg: `The following changes will be applied to the branch protection for ${params.branch} branch`, additions: changes.additions, modifications: changes.modifications, deletions: changes.deletions } + this.log.debug(`Result of compareDeep = ${JSON.stringify(results)}`) if (!changes.hasChanges) { this.log.debug(`There are no changes for branch ${JSON.stringify(params)}. Skipping branch protection changes`) @@ -73,27 +81,27 @@ module.exports = class Branches extends ErrorStash { resArray.push(new NopCommand(this.constructor.name, this.repo, null, results)) } - Object.assign(params, branch.protection, { headers: previewHeaders }) + Object.assign(params, requiredBranchProtectionDefaults, this.reformatAndReturnBranchProtection(structuredClone(result.data)), Overrides.removeOverrides(overrides, branch.protection, result.data), { headers: previewHeaders }) if (this.nop) { - resArray.push(new NopCommand(this.constructor.name, this.repo, this.github.repos.updateBranchProtection.endpoint(params), 'Add Branch Protection')) + resArray.push(new NopCommand(this.constructor.name, this.repo, this.github.rest.repos.updateBranchProtection.endpoint(params), 'Update Branch Protection')) return Promise.resolve(resArray) } - this.log.debug(`Adding branch protection ${JSON.stringify(params)}`) - return this.github.repos.updateBranchProtection(params).then(res => this.log.debug(`Branch protection applied successfully ${JSON.stringify(res.url)}`)).catch(e => { this.logError(`Error applying branch protection ${JSON.stringify(e)}`); return [] }) + this.log.debug(`Updating branch protection ${JSON.stringify(params)}`) + return this.github.rest.repos.updateBranchProtection(params).then(res => this.log.debug(`Branch protection applied successfully ${JSON.stringify(res.url)}`)).catch(e => { this.logError(`Error applying branch protection ${JSON.stringify(e)}`); return [] }) }).catch((e) => { if (e.status === 404) { - Object.assign(params, Overrides.removeOverrides(overrides, branch.protection, {}), { headers: previewHeaders }) + Object.assign(params, requiredBranchProtectionDefaults, Overrides.removeOverrides(overrides, branch.protection, {}), { headers: previewHeaders }) if (this.nop) { - resArray.push(new NopCommand(this.constructor.name, this.repo, this.github.repos.updateBranchProtection.endpoint(params), 'Add Branch Protection')) + resArray.push(new NopCommand(this.constructor.name, this.repo, this.github.rest.repos.updateBranchProtection.endpoint(params), 'Add Branch Protection')) return Promise.resolve(resArray) } this.log.debug(`Adding branch protection ${JSON.stringify(params)}`) - return this.github.repos.updateBranchProtection(params).then(res => this.log.debug(`Branch protection applied successfully ${JSON.stringify(res.url)}`)).catch(e => { this.logError(`Error applying branch protection ${JSON.stringify(e)}`); return [] }) + return this.github.rest.repos.updateBranchProtection(params).then(res => this.log.debug(`Branch protection applied successfully ${JSON.stringify(res.url)}`)).catch(e => { this.logError(`Error applying branch protection ${JSON.stringify(e)}`); return [] }) } else { this.logError(e) if (this.nop) { - resArray.push(new NopCommand(this.constructor.name, this.repo, this.github.repos.updateBranchProtection.endpoint(params), `${e}`, 'ERROR')) + resArray.push(new NopCommand(this.constructor.name, this.repo, this.github.rest.repos.updateBranchProtection.endpoint(params), `${e}`, 'ERROR')) return Promise.resolve(resArray) } } @@ -123,6 +131,29 @@ module.exports = class Branches extends ErrorStash { protection.required_linear_history = protection.required_linear_history && protection.required_linear_history.enabled protection.enforce_admins = protection.enforce_admins && protection.enforce_admins.enabled protection.required_signatures = protection.required_signatures && protection.required_signatures.enabled + protection.allow_force_pushes = protection.allow_force_pushes && protection.allow_force_pushes.enabled + protection.block_creations = protection.block_creations && protection.block_creations.enabled + protection.lock_branch = protection.lock_branch && protection.lock_branch.enabled + protection.allow_fork_syncing = protection.allow_fork_syncing && protection.allow_fork_syncing.enabled + if (protection.restrictions) { + delete protection.restrictions.url + protection.restrictions.users = Array.isArray(protection.restrictions.users) + ? protection.restrictions.users.map(user => user.login || user) + : [] + protection.restrictions.teams = Array.isArray(protection.restrictions.teams) + ? protection.restrictions.teams.map(team => team.slug || team) + : [] + protection.restrictions.apps = Array.isArray(protection.restrictions.apps) + ? protection.restrictions.apps.map(app => app.slug || app) + : [] + } + if (protection.required_status_checks) { + delete protection.required_status_checks.url + delete protection.required_status_checks.contexts_url + if (Array.isArray(protection.required_status_checks.contexts) && protection.required_status_checks.contexts.length === 0) { + delete protection.required_status_checks.contexts + } + } if (protection.required_pull_request_reviews && !protection.required_pull_request_reviews.bypass_pull_request_allowances) { protection.required_pull_request_reviews.bypass_pull_request_allowances = { apps: [], teams: [], users: [] } } diff --git a/lib/plugins/collaborators.js b/lib/plugins/collaborators.js index f3758709a..5fba30eb2 100644 --- a/lib/plugins/collaborators.js +++ b/lib/plugins/collaborators.js @@ -15,13 +15,13 @@ module.exports = class Collaborators extends Diffable { } find () { - // https://docs.github.com/en/rest/collaborators/collaborators?apiVersion=2022-11-28 + // https://docs.github.com/en/rest/collaborators/collaborators?apiVersion=2026-03-10 // 'outside' means all outside collaborators of an organization-owned repository. // 'direct' means all collaborators with permissions to an organization-owned repository, regardless of organization membership status. (includes outside collaborators) // 'all' means all collaborators the authenticated user can see. // We are using 'direct' to avoid double listing users outside collaborators and team members. - return Promise.all([this.github.repos.listCollaborators({ repo: this.repo.repo, owner: this.repo.owner, affiliation: 'direct' }), - this.github.repos.listInvitations({ repo: this.repo.repo, owner: this.repo.owner })]) + return Promise.all([this.github.rest.repos.listCollaborators({ repo: this.repo.repo, owner: this.repo.owner, affiliation: 'direct' }), + this.github.rest.repos.listInvitations({ repo: this.repo.repo, owner: this.repo.owner })]) .then(res => { const mapCollaborator = user => { return { @@ -74,10 +74,10 @@ module.exports = class Collaborators extends Diffable { const data = Object.assign({}, attrs, this.repo) if (this.nop) { return Promise.resolve([ - new NopCommand(this.constructor.name, this.repo, this.github.repos.addCollaborator.endpoint(data), 'Add Collaborators') + new NopCommand(this.constructor.name, this.repo, this.github.rest.repos.addCollaborator.endpoint(data), 'Add Collaborators') ]) } - return this.github.repos.addCollaborator(data) + return this.github.rest.repos.addCollaborator(data) } updateInvite (invitation_id, permissions) { @@ -89,10 +89,10 @@ module.exports = class Collaborators extends Diffable { }, this.repo) if (this.nop) { return Promise.resolve([ - new NopCommand(this.constructor.name, this.repo, this.github.repos.updateInvitation.endpoint(data), 'Update Invitation') + new NopCommand(this.constructor.name, this.repo, this.github.rest.repos.updateInvitation.endpoint(data), 'Update Invitation') ]) } - return this.github.repos.updateInvitation(data) + return this.github.rest.repos.updateInvitation(data) } remove (existing) { @@ -100,20 +100,20 @@ module.exports = class Collaborators extends Diffable { const data = Object.assign({ invitation_id: existing.invitation_id }, this.repo) if (this.nop) { return Promise.resolve([ - new NopCommand(this.constructor.name, this.repo, this.github.repos.deleteInvitation.endpoint(data), + new NopCommand(this.constructor.name, this.repo, this.github.rest.repos.deleteInvitation.endpoint(data), 'Delete Invitation') ]) } - return this.github.repos.deleteInvitation(data) + return this.github.rest.repos.deleteInvitation(data) } else { const data = Object.assign({ username: existing.username }, this.repo) if (this.nop) { return Promise.resolve([ - new NopCommand(this.constructor.name, this.repo, this.github.repos.removeCollaborator.endpoint(data), + new NopCommand(this.constructor.name, this.repo, this.github.rest.repos.removeCollaborator.endpoint(data), 'Remove Collaborator') ]) } - return this.github.repos.removeCollaborator(data) + return this.github.rest.repos.removeCollaborator(data) } } } diff --git a/lib/plugins/custom_properties.js b/lib/plugins/custom_properties.js index 6b1f3ab36..ed9202b8c 100644 --- a/lib/plugins/custom_properties.js +++ b/lib/plugins/custom_properties.js @@ -12,10 +12,24 @@ module.exports = class CustomProperties extends Diffable { // Force all names to lowercase to avoid comparison issues. normalizeEntries () { - this.entries = this.entries.map(({ name, value }) => ({ - name: name.toLowerCase(), - value - })) + this.entries = this.entries.reduce((normalizedEntries, entry) => { + if (!entry || typeof entry !== 'object') { + return normalizedEntries + } + + const entryName = entry.name || entry.property_name + + if (typeof entryName !== 'string') { + return normalizedEntries + } + + normalizedEntries.push({ + name: entryName.toLowerCase(), + value: entry.value + }) + + return normalizedEntries + }, []) } async find () { @@ -25,7 +39,7 @@ module.exports = class CustomProperties extends Diffable { this.log.debug(`Getting all custom properties for the repo ${repoFullName}`) const customProperties = await this.github.paginate( - this.github.repos.getCustomPropertiesValues, + 'GET /repos/{owner}/{repo}/properties/values', { owner, repo, @@ -38,10 +52,24 @@ module.exports = class CustomProperties extends Diffable { // Force all names to lowercase to avoid comparison issues. normalize (properties) { - return properties.map(({ property_name: propertyName, value }) => ({ - name: propertyName.toLowerCase(), - value - })) + return properties.reduce((normalizedProperties, property) => { + if (!property || typeof property !== 'object') { + return normalizedProperties + } + + const propertyName = property.property_name || property.name + + if (typeof propertyName !== 'string') { + return normalizedProperties + } + + normalizedProperties.push({ + name: propertyName.toLowerCase(), + value: property.value + }) + + return normalizedProperties + }, []) } comparator (existing, attrs) { @@ -82,14 +110,14 @@ module.exports = class CustomProperties extends Diffable { return new NopCommand( this.constructor.name, this.repo, - this.github.repos.createOrUpdateCustomPropertiesValues.endpoint(params), + this.github.request.endpoint('PATCH /repos/{owner}/{repo}/properties/values', params), `${operation} Custom Property` ) } try { this.log.debug(`${operation} Custom Property "${name}" for the repo ${repoFullName}`) - await this.github.repos.createOrUpdateCustomPropertiesValues(params) + await this.github.request('PATCH /repos/{owner}/{repo}/properties/values', params) this.log.debug(`Successfully ${operation.toLowerCase()}d Custom Property "${name}" for the repo ${repoFullName}`) } catch (e) { this.logError(`Error during ${operation} Custom Property "${name}" for the repo ${repoFullName}: ${e.message || e}`) diff --git a/lib/plugins/custom_repository_roles.js b/lib/plugins/custom_repository_roles.js new file mode 100644 index 000000000..1931b47cc --- /dev/null +++ b/lib/plugins/custom_repository_roles.js @@ -0,0 +1,119 @@ +const Diffable = require('./diffable') +const NopCommand = require('../nopcommand') +const MergeDeep = require('../mergeDeep') + +// Fields returned by the API that we should ignore when diffing +const ignorableFields = ['id', 'organization', 'created_at', 'updated_at'] + +const version = { + 'X-GitHub-Api-Version': '2026-03-10' +} + +module.exports = class CustomRepositoryRoles extends Diffable { + constructor (nop, github, repo, entries, log, errors) { + super(nop, github, repo, entries, log, errors) + this.github = github + this.repo = repo + this.entries = entries + this.log = log + this.nop = nop + } + + // Find all Custom Repository Roles for the org + find () { + this.log.debug(`Getting all custom repository roles for the org ${this.repo.owner}`) + + return this.github.request('GET /orgs/{org}/custom-repository-roles', { + org: this.repo.owner, + headers: version + }).then(res => { + const roles = (res && res.data && res.data.custom_roles) || [] + // Strip noise so deep-diff focuses on the configurable fields + return roles.map(r => ({ + id: r.id, + name: r.name, + description: r.description, + base_role: r.base_role, + permissions: r.permissions + })) + }).catch(e => { + return this.handleError(e, []) + }) + } + + comparator (existing, attrs) { + return existing.name === attrs.name + } + + changed (existing, attrs) { + const mergeDeep = new MergeDeep(this.log, this.github, ignorableFields) + const merged = mergeDeep.compareDeep(existing, attrs) + return merged.hasChanges + } + + update (existing, attrs) { + const parms = this.wrapAttrs(Object.assign({ role_id: existing.id }, attrs)) + if (this.nop) { + return Promise.resolve([ + new NopCommand(this.constructor.name, this.repo, this.github.request.endpoint('PATCH /orgs/{org}/custom-repository-roles/{role_id}', parms), 'Update Custom Repository Role') + ]) + } + this.log.debug(`Updating Custom Repository Role with the following values ${JSON.stringify(parms, null, 2)}`) + return this.github.request('PATCH /orgs/{org}/custom-repository-roles/{role_id}', parms).then(res => { + this.log.debug(`Custom Repository Role updated successfully ${JSON.stringify(res.url)}`) + return res + }).catch(e => { + return this.handleError(e) + }) + } + + add (attrs) { + const parms = this.wrapAttrs(attrs) + if (this.nop) { + return Promise.resolve([ + new NopCommand(this.constructor.name, this.repo, this.github.request.endpoint('POST /orgs/{org}/custom-repository-roles', parms), 'Create Custom Repository Role') + ]) + } + this.log.debug(`Creating Custom Repository Role with the following values ${JSON.stringify(parms, null, 2)}`) + return this.github.request('POST /orgs/{org}/custom-repository-roles', parms).then(res => { + this.log.debug(`Custom Repository Role created successfully ${JSON.stringify(res.url)}`) + return res + }).catch(e => { + return this.handleError(e) + }) + } + + remove (existing) { + const parms = this.wrapAttrs({ role_id: existing.id }) + if (this.nop) { + return Promise.resolve([ + new NopCommand(this.constructor.name, this.repo, this.github.request.endpoint('DELETE /orgs/{org}/custom-repository-roles/{role_id}', parms), 'Delete Custom Repository Role') + ]) + } + this.log.debug(`Deleting Custom Repository Role with the following values ${JSON.stringify(parms, null, 2)}`) + return this.github.request('DELETE /orgs/{org}/custom-repository-roles/{role_id}', parms).then(res => { + this.log.debug(`Custom Repository Role deleted successfully ${JSON.stringify(res.url)}`) + return res + }).catch(e => { + if (e.status === 404) { + return + } + return this.handleError(e) + }) + } + + wrapAttrs (attrs) { + return Object.assign({}, attrs, { + org: this.repo.owner, + headers: version + }) + } + + handleError (e, returnValue) { + this.logError(e) + if (this.nop) { + return Promise.resolve([(new NopCommand(this.constructor.name, this.repo, null, `error: ${e}`, 'ERROR'))]) + } + return Promise.resolve(returnValue) + } +} diff --git a/lib/plugins/diffable.js b/lib/plugins/diffable.js index 069c68c78..1b5774160 100644 --- a/lib/plugins/diffable.js +++ b/lib/plugins/diffable.js @@ -32,6 +32,9 @@ module.exports = class Diffable extends ErrorStash { this.entries = entries this.log = log this.nop = nop + // When true, remove() calls are suppressed (additive_plugins feature). + // Callers (updateRepos) set this after construction; defaults to false. + this.additive = false } filterEntries () { @@ -62,16 +65,27 @@ module.exports = class Diffable extends ErrorStash { sync () { const resArray = [] + // Will be set to true when this plugin makes (or would make, in nop mode) + // any add/update/remove. Consumers (e.g. Settings suborg re-evaluation) + // can read `plugin.hasChanges` after `sync()` resolves to know whether + // anything actually changed for this repo. + this.hasChanges = false if (this.entries) { let filteredEntries = this.filterEntries() // this.log.debug(`filtered entries are ${JSON.stringify(filteredEntries)}`) return this.find().then(existingRecords => { + // Let plugins resolve config placeholders (e.g. {{EXTERNALLY_DEFINED}}) against + // the live records before any comparison, so placeholders never report changes. + if (typeof this.resolveOverrides === 'function') { + filteredEntries = this.resolveOverrides(existingRecords, filteredEntries) + } this.log.debug(` ${JSON.stringify(existingRecords, null, 2)} \n\n ${JSON.stringify(filteredEntries, null, 2)} `) const mergeDeep = new MergeDeep(this.log, this.github, ignorableFields) const compare = mergeDeep.compareDeep(existingRecords, filteredEntries) const results = { msg: 'Changes found', additions: compare.additions, modifications: compare.modifications, deletions: compare.deletions } this.log.debug(`Results of comparing ${this.constructor.name} diffable target ${JSON.stringify(existingRecords)} with source ${JSON.stringify(filteredEntries)} is ${JSON.stringify(results)}`) + this.hasChanges = !!compare.hasChanges if (!compare.hasChanges) { this.log.debug(`There are no changes for ${this.constructor.name} for repo ${this.repo.repo}. Skipping changes`) return Promise.resolve() @@ -94,17 +108,31 @@ module.exports = class Diffable extends ErrorStash { const changes = [] - existingRecords.forEach(x => { - if (!filteredEntries.find(y => this.comparator(x, y))) { - const change = this.remove(x).then(res => { - if (this.nop) { - return resArray.push(res) - } - return res - }) - changes.push(change) + if (this.additive) { + // Additive mode: skip all remove() calls. In NOP mode, emit an INFO + // message so PR reviewers can see what deletions are being suppressed. + if (this.nop && compare.deletions && compare.deletions.length > 0) { + resArray.push(new NopCommand( + this.constructor.name, + this.repo, + null, + `Additive mode active: ${compare.deletions.length} deletion(s) suppressed by additive_plugins`, + 'INFO' + )) } - }) + } else { + existingRecords.forEach(x => { + if (!filteredEntries.find(y => this.comparator(x, y))) { + const change = this.remove(x).then(res => { + if (this.nop) { + return resArray.push(res) + } + return res + }) + changes.push(change) + } + }) + } filteredEntries.forEach(attrs => { const existing = existingRecords.find(record => { diff --git a/lib/plugins/labels.js b/lib/plugins/labels.js index 49558cc08..6e48d8dcb 100644 --- a/lib/plugins/labels.js +++ b/lib/plugins/labels.js @@ -34,8 +34,8 @@ module.exports = class Labels extends Diffable { find () { this.log.debug(`Finding labels for ${JSON.stringify(this.wrapAttrs({ per_page: 100 }))}`) - const options = this.github.issues.listLabelsForRepo.endpoint.merge(this.wrapAttrs({ per_page: 100 })) - return this.github.repos.get(this.repo).then(() => { + const options = this.github.rest.issues.listLabelsForRepo.endpoint.merge(this.wrapAttrs({ per_page: 100 })) + return this.github.rest.repos.get(this.repo).then(() => { return this.github.paginate(options) }) .catch(e => { @@ -63,20 +63,20 @@ module.exports = class Labels extends Diffable { delete attrs.oldname if (this.nop) { return Promise.resolve([ - new NopCommand(this.constructor.name, this.repo, this.github.issues.updateLabel.endpoint(this.wrapAttrs(attrs)), 'Update label') + new NopCommand(this.constructor.name, this.repo, this.github.rest.issues.updateLabel.endpoint(this.wrapAttrs(attrs)), 'Update label') ]) } - return this.github.issues.updateLabel(this.wrapAttrs(attrs)) + return this.github.rest.issues.updateLabel(this.wrapAttrs(attrs)) } add (attrs) { if (this.nop) { return Promise.resolve([ - new NopCommand(this.constructor.name, this.repo, this.github.issues.createLabel.endpoint(this.wrapAttrs(attrs)), 'Create label') + new NopCommand(this.constructor.name, this.repo, this.github.rest.issues.createLabel.endpoint(this.wrapAttrs(attrs)), 'Create label') ]) } this.log.debug(`Creating labels for ${JSON.stringify(attrs, null, 4)}`) - return this.github.issues.createLabel(this.wrapAttrs(attrs)).catch(e => this.logError(` ${JSON.stringify(e)}`)) + return this.github.rest.issues.createLabel(this.wrapAttrs(attrs)).catch(e => this.logError(` ${JSON.stringify(e)}`)) } remove (existing) { @@ -85,10 +85,10 @@ module.exports = class Labels extends Diffable { } if (this.nop) { return Promise.resolve([ - new NopCommand(this.constructor.name, this.repo, this.github.issues.deleteLabel.endpoint(this.wrapAttrs({ name: existing.name })), 'Delete label') + new NopCommand(this.constructor.name, this.repo, this.github.rest.issues.deleteLabel.endpoint(this.wrapAttrs({ name: existing.name })), 'Delete label') ]) } - return this.github.issues.deleteLabel(this.wrapAttrs({ name: existing.name })) + return this.github.rest.issues.deleteLabel(this.wrapAttrs({ name: existing.name })) } wrapAttrs (attrs) { diff --git a/lib/plugins/milestones.js b/lib/plugins/milestones.js index 9fc93ab22..cdb242496 100644 --- a/lib/plugins/milestones.js +++ b/lib/plugins/milestones.js @@ -14,7 +14,7 @@ module.exports = class Milestones extends Diffable { } find () { - const options = this.github.issues.listMilestones.endpoint.merge(Object.assign({ per_page: 100, state: 'all' }, this.repo)) + const options = this.github.rest.issues.listMilestones.endpoint.merge(Object.assign({ per_page: 100, state: 'all' }, this.repo)) return this.github.paginate(options) } @@ -29,18 +29,18 @@ module.exports = class Milestones extends Diffable { update (existing, attrs) { const { owner, repo } = this.repo - return this.github.issues.updateMilestone(Object.assign({ milestone_number: existing.number }, attrs, { owner, repo })) + return this.github.rest.issues.updateMilestone(Object.assign({ milestone_number: existing.number }, attrs, { owner, repo })) } add (attrs) { const { owner, repo } = this.repo - return this.github.issues.createMilestone(Object.assign({}, attrs, { owner, repo })) + return this.github.rest.issues.createMilestone(Object.assign({}, attrs, { owner, repo })) } remove (existing) { const { owner, repo } = this.repo - return this.github.issues.deleteMilestone(Object.assign({ milestone_number: existing.number }, { owner, repo })) + return this.github.rest.issues.deleteMilestone(Object.assign({ milestone_number: existing.number }, { owner, repo })) } } diff --git a/lib/plugins/repository.js b/lib/plugins/repository.js index 14599f608..4d0e14ce8 100644 --- a/lib/plugins/repository.js +++ b/lib/plugins/repository.js @@ -62,9 +62,13 @@ module.exports = class Repository extends ErrorStash { const resArray = [] this.log.debug(`Syncing Repo ${this.settings.name}`) this.settings.name = this.settings.name || this.settings.repo + // Change signals consumed by Settings suborg re-evaluation. + this.hasChanges = false + this.renamed = false + this.created = false // let hasChanges = false // let hasTopicChanges = false - return this.github.repos.get(this.repo) + return this.github.rest.repos.get(this.repo) .then(resp => { const mergeDeep = new MergeDeep(this.log, this.github, ignorableFields) @@ -74,6 +78,12 @@ module.exports = class Repository extends ErrorStash { const topicChanges = mergeDeep.compareDeep({ entries: resp.data.topics }, { entries: this.topics }) // hasTopicChanges = topicChanges.additions.length > 0 || topicChanges.modifications.length > 0 + this.hasChanges = !!(changes.hasChanges || topicChanges.hasChanges) + // A repo rename (changing the slug) shows up as a `name` modification. + if (changes.hasChanges && this.settings.name && resp.data.name && this.settings.name !== resp.data.name) { + this.renamed = true + } + // const results = JSON.stringify(changes, null, 2) const results = { msg: `${this.constructor.name} settings changes`, additions: changes.additions, modifications: changes.modifications, deletions: changes.deletions } @@ -120,28 +130,30 @@ module.exports = class Repository extends ErrorStash { }).catch(e => { if (e.status === 404) { if (this.force_create) { + this.hasChanges = true + this.created = true if (this.template) { this.log.debug(`Creating repo using template ${this.template}`) const options = { template_owner: this.repo.owner, template_repo: this.template, owner: this.repo.owner, name: this.repo.repo, private: (this.settings.private ? this.settings.private : true), description: this.settings.description ? this.settings.description : '' } if (this.nop) { - this.log.debug(`Creating Repo using template ${JSON.stringify(this.github.repos.createInOrg.endpoint(this.settings))} `) - resArray.push(new NopCommand(this.constructor.name, this.repo, this.github.repos.createUsingTemplate.endpoint(options), 'Create Repo Using Template')) + this.log.debug(`Creating Repo using template ${JSON.stringify(this.github.rest.repos.createUsingTemplate.endpoint(options))} `) + resArray.push(new NopCommand(this.constructor.name, this.repo, this.github.rest.repos.createUsingTemplate.endpoint(options), 'Create Repo Using Template')) return Promise.resolve(resArray) } - return this.github.repos.createUsingTemplate(options) + return this.github.rest.repos.createUsingTemplate(options) } else { // https://docs.github.com/en/rest/repos/repos#create-an-organization-repository uses org instead of owner like // the API to create a repo with a template this.settings.org = this.settings.owner this.log.debug('Creating repo with settings ', this.settings) if (this.nop) { - this.log.debug(`Creating Repo ${JSON.stringify(this.github.repos.createInOrg.endpoint(this.settings))} `) + this.log.debug(`Creating Repo ${JSON.stringify(this.github.rest.repos.createInOrg.endpoint(this.settings))} `) return Promise.resolve([ - new NopCommand(this.constructor.name, this.repo, this.github.repos.createInOrg.endpoint(this.settings), 'Create Repo') + new NopCommand(this.constructor.name, this.repo, this.github.rest.repos.createInOrg.endpoint(this.settings), 'Create Repo') ]) } - return this.github.repos.createInOrg(this.settings) + return this.github.rest.repos.createInOrg(this.settings) } } else { if (this.nop) { @@ -158,7 +170,7 @@ module.exports = class Repository extends ErrorStash { updateDefaultBranch (oldname, newname, resArray) { this.log.debug(`Checking if ${newname} is already a branch`) - return this.github.repos.getBranch({ + return this.github.rest.repos.getBranch({ owner: this.settings.owner, repo: this.settings.repo, branch: newname @@ -175,10 +187,10 @@ module.exports = class Repository extends ErrorStash { default_branch: newname } if (this.nop) { - resArray.push(new NopCommand(this.constructor.name, this.repo, this.github.repos.update.endpoint(parms), 'Update Repo')) + resArray.push(new NopCommand(this.constructor.name, this.repo, this.github.rest.repos.update.endpoint(parms), 'Update Repo')) } else { this.log.debug(`Updating repo with settings ${JSON.stringify(parms)}`) - return this.github.repos.update(parms) + return this.github.rest.repos.update(parms) } } }).catch(e => { @@ -200,19 +212,19 @@ module.exports = class Repository extends ErrorStash { } this.log.info(`Rename default branch repo with settings ${JSON.stringify(parms)}`) if (this.nop) { - resArray.push(new NopCommand(this.constructor.name, this.repo, this.github.repos.renameBranch.endpoint(oldname, this.settings.default_branch), `Repo rename default branch to ${this.settings.default_branch}`)) + resArray.push(new NopCommand(this.constructor.name, this.repo, this.github.rest.repos.renameBranch.endpoint(parms), `Repo rename default branch to ${this.settings.default_branch}`)) } else { - return this.github.repos.renameBranch(parms) + return this.github.rest.repos.renameBranch(parms) } } updaterepo (resArray) { this.log.debug(`Updating repo with settings ${JSON.stringify(this.topics)} ${JSON.stringify(this.settings)}`) if (this.nop) { - resArray.push(new NopCommand(this.constructor.name, this.repo, this.github.repos.update.endpoint(this.settings), 'Update Repo')) + resArray.push(new NopCommand(this.constructor.name, this.repo, this.github.rest.repos.update.endpoint(this.settings), 'Update Repo')) return Promise.resolve(resArray) } - return this.github.repos.update(this.settings) + return this.github.rest.repos.update(this.settings) } updatetopics (repoData, resArray) { @@ -230,10 +242,10 @@ module.exports = class Repository extends ErrorStash { // !repoData.data?.topics.every(t => this.topics.includes(t))) { this.log.debug(`Updating repo with topics ${this.topics.join(',')}`) if (this.nop) { - resArray.push((new NopCommand(this.constructor.name, this.repo, this.github.repos.replaceAllTopics.endpoint(parms), 'Update Topics'))) + resArray.push((new NopCommand(this.constructor.name, this.repo, this.github.rest.repos.replaceAllTopics.endpoint(parms), 'Update Topics'))) return Promise.resolve(resArray) } - return this.github.repos.replaceAllTopics(parms) + return this.github.rest.repos.replaceAllTopics(parms) // } else { // this.log.debug(`no need to update topics for ${repoData.data.name}`) // if (this.nop) { @@ -251,26 +263,26 @@ module.exports = class Repository extends ErrorStash { if (this.security.enableVulnerabilityAlerts === true) { this.log.debug(`Enabling Dependabot alerts for owner: ${repoData.owner.login} and repo ${repoData.name}`) if (this.nop) { - resArray.push((new NopCommand(this.constructor.name, this.repo, this.github.repos.enableVulnerabilityAlerts.endpoint({ + resArray.push((new NopCommand(this.constructor.name, this.repo, this.github.rest.repos.enableVulnerabilityAlerts.endpoint({ owner: repoData.owner.login, repo: repoData.name }), 'Enabling Dependabot alerts'))) return Promise.resolve(resArray) } - return this.github.repos.enableVulnerabilityAlerts({ + return this.github.rest.repos.enableVulnerabilityAlerts({ owner: repoData.owner.login, repo: repoData.name }) } else { this.log.debug(`Disabling Dependabot alerts for for owner: ${repoData.owner.login} and repo ${repoData.name}`) if (this.nop) { - resArray.push((new NopCommand(this.constructor.name, this.github.repos.disableVulnerabilityAlerts.endpoint({ + resArray.push((new NopCommand(this.constructor.name, this.repo, this.github.rest.repos.disableVulnerabilityAlerts.endpoint({ owner: repoData.owner.login, repo: repoData.name }), 'Disabling Dependabot alerts'))) return Promise.resolve(resArray) } - return this.github.repos.disableVulnerabilityAlerts({ + return this.github.rest.repos.disableVulnerabilityAlerts({ owner: repoData.owner.login, repo: repoData.name }) @@ -289,26 +301,26 @@ module.exports = class Repository extends ErrorStash { if (this.security.enableAutomatedSecurityFixes === true) { this.log.debug(`Enabling Dependabot security updates for owner: ${repoData.owner.login} and repo ${repoData.name}`) if (this.nop) { - resArray.push((new NopCommand(this.constructor.name, this.repo, this.github.repos.enableAutomatedSecurityFixes.endpoint({ + resArray.push((new NopCommand(this.constructor.name, this.repo, this.github.rest.repos.enableAutomatedSecurityFixes.endpoint({ owner: repoData.owner.login, repo: repoData.name }), 'Enabling Dependabot security updates'))) return Promise.resolve(resArray) } - return this.github.repos.enableAutomatedSecurityFixes({ + return this.github.rest.repos.enableAutomatedSecurityFixes({ owner: repoData.owner.login, repo: repoData.name }) } else { this.log.debug(`Disabling Dependabot security updates for owner: ${repoData.owner.login} and repo ${repoData.name}`) if (this.nop) { - resArray.push((new NopCommand(this.constructor.name, this.github.repos.disableAutomatedSecurityFixes.endpoint({ + resArray.push((new NopCommand(this.constructor.name, this.repo, this.github.rest.repos.disableAutomatedSecurityFixes.endpoint({ owner: repoData.owner.login, repo: repoData.name }), 'Disabling Dependabot security updates'))) return Promise.resolve(resArray) } - return this.github.repos.disableAutomatedSecurityFixes({ + return this.github.rest.repos.disableAutomatedSecurityFixes({ owner: repoData.owner.login, repo: repoData.name }) diff --git a/lib/plugins/rulesets.js b/lib/plugins/rulesets.js index b77ead1bd..d17b413a4 100644 --- a/lib/plugins/rulesets.js +++ b/lib/plugins/rulesets.js @@ -4,16 +4,29 @@ const MergeDeep = require('../mergeDeep') const Overrides = require('./overrides') const ignorableFields = [] const overrides = { - 'required_status_checks': { - 'action': 'delete', - 'parents': 3, - 'type': 'dict' - }, + required_status_checks: { + action: 'delete', + parents: 3, + type: 'dict' + } } const version = { - 'X-GitHub-Api-Version': '2022-11-28' + 'X-GitHub-Api-Version': '2026-03-10' +} + +// GitHub's built-in (base) repository role IDs. These are not returned by the +// custom-repository-roles API, so they are mapped statically here to allow +// users to reference them by name in a ruleset's bypass_actors. Custom roles +// are resolved dynamically via GET /orgs/{org}/custom-repository-roles. +const BASE_REPOSITORY_ROLE_IDS = { + read: 1, + triage: 2, + write: 3, + maintain: 4, + admin: 5 } + module.exports = class Rulesets extends Diffable { constructor (nop, github, repo, entries, log, errors, scope) { super(nop, github, repo, entries, log, errors) @@ -23,6 +36,137 @@ module.exports = class Rulesets extends Diffable { this.log = log this.nop = nop this.scope = scope || 'repo' + // Cache for name -> id lookups, scoped to a single sync() invocation. + this.idCache = new Map() + } + + // Resolve human-friendly names to the numeric ids GitHub expects before the + // normal Diffable sync runs. This lets users define rulesets using a team + // slug, username, GitHub App slug, or repository role name instead of having + // to look up the corresponding id. Names are resolved in place and the helper + // attribute is removed so the payload matches what GitHub returns (which only + // contains ids), keeping compareDeep stable and backward compatible with + // policies that already use ids. + async sync () { + try { + await this.resolveNamesToIds() + } catch (e) { + return this.handleError(e) + } + return super.sync() + } + + async resolveNamesToIds () { + if (!this.entries) return + this.idCache = new Map() + for (const ruleset of this.entries) { + if (Array.isArray(ruleset.bypass_actors)) { + for (const actor of ruleset.bypass_actors) { + await this.resolveBypassActor(actor) + } + } + const rules = Array.isArray(ruleset.rules) ? ruleset.rules : [] + for (const rule of rules) { + const reviewers = rule && rule.parameters && rule.parameters.required_reviewers + if (Array.isArray(reviewers)) { + for (const entry of reviewers) { + await this.resolveReviewer(entry) + } + } + } + } + } + + async resolveBypassActor (actor) { + if (!actor || actor.name === undefined || actor.name === null) return + if (actor.actor_id !== undefined && actor.actor_id !== null) { + throw new Error(`Ruleset bypass_actor cannot specify both 'name' ('${actor.name}') and 'actor_id' (${actor.actor_id}). Use one or the other.`) + } + actor.actor_id = await this.resolveActorId(actor.actor_type, actor.name) + delete actor.name + } + + async resolveReviewer (entry) { + const reviewer = entry && entry.reviewer + if (!reviewer || reviewer.slug === undefined || reviewer.slug === null) return + if (reviewer.id !== undefined && reviewer.id !== null) { + throw new Error(`Ruleset required_reviewer cannot specify both 'slug' ('${reviewer.slug}') and 'id' (${reviewer.id}). Use one or the other.`) + } + reviewer.id = await this.resolveTeamId(reviewer.slug) + delete reviewer.slug + } + + async resolveActorId (actorType, name) { + switch (actorType) { + case 'Team': + return this.resolveTeamId(name) + case 'User': + return this.resolveUserId(name) + case 'Integration': + return this.resolveIntegrationId(name) + case 'RepositoryRole': + return this.resolveRepositoryRoleId(name) + default: + throw new Error(`Cannot resolve 'name' '${name}' for actor_type '${actorType}'. Name resolution is only supported for Team, User, Integration, and RepositoryRole. Use 'actor_id' instead.`) + } + } + + async cachedLookup (key, fn) { + if (this.idCache.has(key)) return this.idCache.get(key) + const value = await fn() + this.idCache.set(key, value) + return value + } + + async resolveTeamId (slug) { + return this.cachedLookup(`Team:${slug}`, async () => { + try { + const res = await this.github.rest.teams.getByName({ org: this.repo.owner, team_slug: slug }) + return res.data.id + } catch (e) { + throw new Error(`Unable to resolve Team slug '${slug}' to an id in org '${this.repo.owner}': ${e.status || e.message}`) + } + }) + } + + async resolveUserId (username) { + return this.cachedLookup(`User:${username}`, async () => { + try { + const res = await this.github.request('GET /users/{username}', { username }) + return res.data.id + } catch (e) { + throw new Error(`Unable to resolve User '${username}' to an id: ${e.status || e.message}`) + } + }) + } + + async resolveIntegrationId (slug) { + return this.cachedLookup(`Integration:${slug}`, async () => { + try { + const res = await this.github.request('GET /apps/{app_slug}', { app_slug: slug }) + return res.data.id + } catch (e) { + throw new Error(`Unable to resolve Integration (GitHub App) slug '${slug}' to an id: ${e.status || e.message}`) + } + }) + } + + async resolveRepositoryRoleId (name) { + return this.cachedLookup(`RepositoryRole:${name}`, async () => { + const baseId = BASE_REPOSITORY_ROLE_IDS[String(name).toLowerCase()] + if (baseId !== undefined) return baseId + try { + const res = await this.github.request('GET /orgs/{org}/custom-repository-roles', { org: this.repo.owner }) + const roles = (res.data && res.data.custom_roles) || [] + const match = roles.find(role => role.name === name) + if (!match) { + throw new Error(`no custom repository role named '${name}' found in org '${this.repo.owner}'`) + } + return match.id + } catch (e) { + throw new Error(`Unable to resolve RepositoryRole '${name}' to an id: ${e.status || e.message}`) + } + }) } // Find all Rulesets for this org @@ -81,6 +225,7 @@ module.exports = class Rulesets extends Diffable { return res ? res.flat(1) : [] }) }).catch(e => { + if (this.nop && e.status === 404) return [] return this.handleError(e, []) }) } @@ -94,6 +239,17 @@ module.exports = class Rulesets extends Diffable { return existing.name === attrs.name } + // Resolve {{EXTERNALLY_DEFINED}} placeholders against the matching live ruleset + // before diffing, mirroring how branches.js resolves overrides inside its + // comparison. Without this the comparison sees the literal placeholder string, + // so every plan reports an update for rulesets that use overrides. + resolveOverrides (existingRecords, entries) { + return entries.map(attrs => { + const existing = existingRecords.find(record => this.comparator(record, attrs)) + return Overrides.removeOverrides(overrides, structuredClone(attrs), existing || {}) + }) + } + changed (existing, attrs) { const mergeDeep = new MergeDeep(this.log, this.github, ignorableFields) const merged = mergeDeep.compareDeep(existing, attrs) @@ -146,7 +302,7 @@ module.exports = class Rulesets extends Diffable { this.log.debug(`Ruleset created successfully ${JSON.stringify(res.url)}`) return res }).catch(e => { - return this.handleError(e) + return this.handleDuplicateOrError(e, attrs) }) } else { if (this.nop) { @@ -160,11 +316,45 @@ module.exports = class Rulesets extends Diffable { this.log.debug(`Ruleset created successfully ${JSON.stringify(res.url)}`) return res }).catch(e => { - return this.handleError(e) + return this.handleDuplicateOrError(e, attrs) }) } } + // A ruleset create (POST) is not idempotent. Octokit's retry / auth-app + // layers can re-send a POST that already succeeded, and a repo can also be + // processed by two overlapping syncs (e.g. a full sync racing with the + // repository.created webhook). In both cases the second create hits + // "Name must be unique" (422) even though the ruleset now exists. Instead of + // failing the whole run, reconcile by looking the ruleset up by name and + // updating it in place so the create effectively becomes idempotent. + isDuplicateNameError (e) { + if (!e || e.status !== 422) return false + const errors = e.response && e.response.data && e.response.data.errors + const list = Array.isArray(errors) ? errors : [] + return list.some(err => { + const msg = typeof err === 'string' ? err : (err && err.message) + return typeof msg === 'string' && /name must be unique/i.test(msg) + }) + } + + handleDuplicateOrError (e, attrs) { + if (!this.isDuplicateNameError(e)) { + return this.handleError(e) + } + this.log.debug(`Ruleset '${attrs && attrs.name}' already exists (concurrent or retried create); reconciling by update`) + return this.find().then(existing => { + const match = Array.isArray(existing) + ? existing.find(record => this.comparator(record, attrs)) + : undefined + if (!match) { + return this.handleError(e) + } + const { id: _ignoredId, ...attrsWithoutId } = attrs || {} + return this.update(match, attrsWithoutId) + }).catch(err => this.handleError(err)) + } + remove (existing) { const parms = this.wrapAttrs(Object.assign({ id: existing.id })) if (this.scope === 'org') { diff --git a/lib/plugins/teams.js b/lib/plugins/teams.js index 4d7f79273..2762d53a6 100644 --- a/lib/plugins/teams.js +++ b/lib/plugins/teams.js @@ -2,10 +2,37 @@ const Diffable = require('./diffable') const NopCommand = require('../nopcommand') const teamRepoEndpoint = '/orgs/:owner/teams/:team_slug/repos/:owner/:repo' +const listExternalGroupsEndpoint = 'GET /orgs/{org}/external-groups' +const teamExternalGroupsEndpoint = '/orgs/{org}/teams/{team_slug}/external-groups' +const securityManagerRoleName = 'security_manager' +const safeSecurityManagerStatuses = [403, 404, 422] + module.exports = class Teams extends Diffable { + // Override Diffable.sync to also reconcile the optional `external_group` + // link on each team entry after the normal team-repo permission sync. + // This runs regardless of whether the team-repo association was added, + // updated, or already in sync -- so updating only `external_group` on a + // team that already has correct repo permissions still triggers the link. + async sync () { + const res = await super.sync() + if (!this.entries) return res + + const filtered = this.filterEntries() + const entriesWithExternalGroup = filtered.filter(e => e && e.external_group) + if (entriesWithExternalGroup.length === 0) return res + + const nopCommands = Array.isArray(res) ? res : [] + for (const attrs of entriesWithExternalGroup) { + await this.syncExternalGroup(attrs, this.nop ? nopCommands : undefined) + } + return this.nop ? nopCommands : res + } + async find () { + this.skipTeamDeletion = false + this.securityManagerTeamIdentifiers = new Set() this.log.debug(`Finding teams for ${this.repo.owner}/${this.repo.repo}`) - return this.github.paginate(this.github.repos.listTeams, this.repo).then(res => { + return this.github.paginate(this.github.rest.repos.listTeams, this.repo).then(res => { this.log.debug(`Found teams ${JSON.stringify(res)}`) return this.checkSecurityManager(res) }) @@ -14,47 +41,117 @@ module.exports = class Teams extends Diffable { // remove all security manager teams async checkSecurityManager (teams) { try { - // Uncomment the following lines to handle the deprecation of the teams api https://gh.io/security-managers-rest-api-sunset - // but this would require a new permission on the app - // - // const roles = await this.github.paginate('GET /orgs/{org}/roles', { org: this.repo.owner }) - // const securityManagerRole = roles.find(role => role.name === 'security_manager') - // - // this.log.debug(`Calling API to get security managers ${JSON.stringify(this.github.request.endpoint('GET /orgs/{org}/roles/{role_id}/teams', - // { - // org: this.repo.owner, - // role_id: securityManagerRole.id - // }))} `) - // const resp = await this.github.paginate('GET /orgs/{org}/roles/{role_id}/teams', - // { - // org: this.repo.owner, - // role_id: securityManagerRole.id - // }) - this.log.debug('Removing all security manager teams since they should not be handled here') - this.log.debug(`Calling API to get security managers ${JSON.stringify(this.github.request.endpoint('GET /orgs/{org}/security-managers', - { - org: this.repo.owner - }))} `) - const resp = await this.github.paginate('GET /orgs/{org}/security-managers', + this.log.debug(`Calling API to get organization roles ${JSON.stringify(this.github.request.endpoint('GET /orgs/{org}/organization-roles', + { + org: this.repo.owner + }))} `) + const rolesResp = await this.github.paginate('GET /orgs/{org}/organization-roles', { org: this.repo.owner }) + const roles = this.toArray(rolesResp, 'roles') + const securityManagerRole = roles.find(role => this.isSecurityManagerRole(role)) + + if (!securityManagerRole || !securityManagerRole.id) { + this.log.debug(`${this.repo.owner} Org does not have a security manager organization role set up`) + return teams + } + + const params = { + org: this.repo.owner, + role_id: securityManagerRole.id + } + this.log.debug(`Calling API to get security manager teams ${JSON.stringify(this.github.request.endpoint('GET /orgs/{org}/organization-roles/{role_id}/teams', params))} `) + const resp = await this.github.paginate('GET /orgs/{org}/organization-roles/{role_id}/teams', params) this.log.debug(`Response from the call is ${JSON.stringify(resp)}`) - return teams.filter(team => !resp.some(sec => sec.name === team.name)) + const securityManagerTeams = this.toArray(resp, 'teams') + const securityManagerTeamIdentifiers = new Set(securityManagerTeams.flatMap(team => [team.slug, team.name].map(name => this.normalizeTeamIdentifier(name))).filter(Boolean)) + // Persist the identifiers so add()/update()/remove() can no-op for + // security manager teams even when they appear in the config. Without + // this, a configured security manager team would look "missing" from the + // filtered existing list and Diffable.sync() would (re)add it here. + this.securityManagerTeamIdentifiers = securityManagerTeamIdentifiers + + return teams.filter(team => !this.isSecurityManagerTeam(team, securityManagerTeamIdentifiers)) } catch (e) { - if (e.status === 404) { - this.log.debug(`${this.repo.owner} Org does not have Security manager teams set up ${e}`) + this.skipTeamDeletion = true + const status = e && e.status + if (safeSecurityManagerStatuses.includes(status)) { + this.log.debug(`${this.repo.owner} Org security manager teams could not be fetched with status ${status}; keeping repository teams unchanged ${e}`) } else { this.log.error( - `Unexpected error when fetching for security manager teams org ${this.repo.owner} = ${e}` + `Unexpected error when fetching security manager teams for org ${this.repo.owner}; keeping repository teams unchanged ${e}` ) } return teams } } + toArray (resp, propertyName) { + if (Array.isArray(resp)) { + return resp + } + + if (resp && Array.isArray(resp[propertyName])) { + return resp[propertyName] + } + + return [] + } + + isSecurityManagerRole (role) { + return [role && role.name, role && role.slug] + .map(name => this.normalizeRoleName(name)) + .includes(securityManagerRoleName) + } + + normalizeRoleName (name) { + if (typeof name !== 'string') { + return '' + } + + return name.trim().toLowerCase().replace(/[\s-]+/g, '_') + } + + normalizeTeamIdentifier (name) { + if (typeof name !== 'string') { + return '' + } + + return name.trim().toLowerCase().replace(/['’]/g, '').replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') + } + + isSecurityManagerTeam (team, securityManagerTeamIdentifiers) { + return [team.slug, team.name] + .map(name => this.normalizeTeamIdentifier(name)) + .filter(Boolean) + .some(name => securityManagerTeamIdentifiers.has(name)) + } + + // True when the given attrs/record refers to a discovered security manager + // team. Security manager teams are intentionally not managed by this plugin, + // so add()/update()/remove() must no-op for them even if they are present in + // the config file. + isConfiguredSecurityManagerTeam (attrs) { + if (!this.securityManagerTeamIdentifiers || this.securityManagerTeamIdentifiers.size === 0) { + return false + } + return this.isSecurityManagerTeam(attrs, this.securityManagerTeamIdentifiers) + } + + skipSecurityManagerTeam (attrs, verb) { + const teamName = (attrs && (attrs.name || attrs.slug)) || 'unknown' + this.log.debug(`Skipping ${verb} of security manager team ${teamName} for repo ${this.repo.repo}; security manager teams are not managed here`) + if (this.nop) { + return Promise.resolve([ + new NopCommand(this.constructor.name, this.repo, null, `Skipping ${verb} of security manager team ${teamName}; security manager teams are not managed by safe-settings`, 'INFO') + ]) + } + return Promise.resolve() + } + comparator (existing, attrs) { - return existing.slug === attrs.name.toLowerCase() + return this.normalizeTeamIdentifier(existing.slug || existing.name) === this.normalizeTeamIdentifier(attrs.name) } changed (existing, attrs) { @@ -62,6 +159,9 @@ module.exports = class Teams extends Diffable { } update (existing, attrs) { + if (this.isConfiguredSecurityManagerTeam(attrs)) { + return this.skipSecurityManagerTeam(attrs, 'update') + } if (this.nop) { return Promise.resolve([ new NopCommand(this.constructor.name, this.repo, this.github.request.endpoint(`PUT ${teamRepoEndpoint}`, this.toParams(existing, attrs)), 'Add Teams to Repo') @@ -71,17 +171,20 @@ module.exports = class Teams extends Diffable { } add (attrs) { + if (this.isConfiguredSecurityManagerTeam(attrs)) { + return this.skipSecurityManagerTeam(attrs, 'add') + } let existing = { team_id: 1 } this.log.debug(`Getting team with the parms ${JSON.stringify(attrs)}`) - return this.github.teams.getByName({ org: this.repo.owner, team_slug: attrs.name }).then(res => { + return this.github.rest.teams.getByName({ org: this.repo.owner, team_slug: this.normalizeTeamIdentifier(attrs.name) }).then(res => { existing = res.data this.log.debug(`adding team ${attrs.name} to repo ${this.repo.repo}`) if (this.nop) { return Promise.resolve([ - new NopCommand(this.constructor.name, this.repo, this.github.teams.addOrUpdateRepoPermissionsInOrg.endpoint(this.toParams(existing, attrs)), 'Add Teams to Repo') + new NopCommand(this.constructor.name, this.repo, this.github.rest.teams.addOrUpdateRepoPermissionsInOrg.endpoint(this.toParams(existing, attrs)), 'Add Teams to Repo') ]) } - return this.github.teams.addOrUpdateRepoPermissionsInOrg(this.toParams(existing, attrs)).then(res => { + return this.github.rest.teams.addOrUpdateRepoPermissionsInOrg(this.toParams(existing, attrs)).then(res => { this.log.debug(`team added ${res}`) }).catch(e => { this.logError(`Error adding team to repo ${JSON.stringify(e)} with parms ${JSON.stringify(this.toParams(existing, attrs))}:\n`, e) @@ -98,14 +201,14 @@ module.exports = class Teams extends Diffable { this.log.debug(`Creating teams ${JSON.stringify(createParam)}`) if (this.nop) { return Promise.resolve([ - new NopCommand(this.constructor.name, this.repo, this.github.teams.create.endpoint(createParam), 'Create Team') + new NopCommand(this.constructor.name, this.repo, this.github.rest.teams.create.endpoint(createParam), 'Create Team') ]) } - return this.github.teams.create(createParam).then(res => { + return this.github.rest.teams.create(createParam).then(res => { this.log.debug(`team ${createParam.name} created`) existing = res.data this.log.debug(`adding team ${attrs.name} to repo ${this.repo.repo}`) - return this.github.teams.addOrUpdateRepoPermissionsInOrg(this.toParams(existing, attrs)) + return this.github.rest.teams.addOrUpdateRepoPermissionsInOrg(this.toParams(existing, attrs)) }).catch(e => { this.logError('Error adding team: ', e) }) @@ -114,6 +217,21 @@ module.exports = class Teams extends Diffable { } remove (existing) { + if (this.isConfiguredSecurityManagerTeam(existing)) { + return this.skipSecurityManagerTeam(existing, 'removal') + } + + if (this.skipTeamDeletion) { + const msg = `Skipping deletion of team ${existing.slug} from repo ${this.repo.repo} because security manager team discovery failed` + this.log.debug(msg) + if (this.nop) { + return Promise.resolve([ + new NopCommand(this.constructor.name, this.repo, null, msg, 'INFO') + ]) + } + return Promise.resolve() + } + if (this.nop) { return Promise.resolve([ new NopCommand(this.constructor.name, this.repo, this.github.request.endpoint( @@ -132,10 +250,133 @@ module.exports = class Teams extends Diffable { return { team_id: existing.id, org: this.repo.owner, - team_slug: attrs.name, + team_slug: existing.slug || this.normalizeTeamIdentifier(attrs.name), owner: this.repo.owner, repo: this.repo.repo, permission: attrs.permission } } + + // Resolve the org's external-group display name -> group_id. Lazily builds + // a per-org Map (name -> id) the first time it's needed within a sync, and + // caches it on the shared `github` client so multiple repos / teams in the + // same sync only paginate `GET /orgs/{org}/external-groups` once per org. + // Returns null when the named group does not exist for the org (logs an + // error so the user can correct their yaml). + async resolveExternalGroupId (groupName) { + if (!this.github.__externalGroupsCache) { + this.github.__externalGroupsCache = new Map() + } + const cache = this.github.__externalGroupsCache + const org = this.repo.owner + if (!cache.has(org)) { + try { + // The external-groups endpoint returns { total_count, groups: [...] } + // and is not in Octokit's known-pagination list, so we must pass a + // map function that extracts the `groups` array from each page; + // otherwise paginate() yields the raw response objects and we'd + // silently fail to find any names. + const groups = await this.github.paginate( + listExternalGroupsEndpoint, + { org, per_page: 100 }, + (response) => (response && response.data && response.data.groups) || [] + ) + const byName = new Map() + for (const g of groups) { + // Keys are lower-cased so lookups are case-insensitive, matching + // the comparison used by the SCIM-sync workflow's + // wait-for-scim-sync.py (which lower-cases both sides). Without + // this, a yaml `external_group` value that differs only in case + // from the IdP-provisioned display name would be reported as + // "not found" even though the group exists and the CI gate + // considers it synced. + if (g && g.group_name) byName.set(g.group_name.toLowerCase(), g.group_id) + } + this.log.debug(`Loaded ${byName.size} external group(s) for org ${org}: ${JSON.stringify(Array.from(byName.keys()))}`) + cache.set(org, byName) + } catch (e) { + this.logError(`Error listing external groups for org ${org}: ${e}`) + // Cache an empty map so we don't retry-storm the API within this sync. + cache.set(org, new Map()) + } + } + const id = cache.get(org).get(groupName.toLowerCase()) + if (id === undefined) { + return null + } + return id + } + + // Link a team to an external IdP group identified by display name. Only + // acts when the team entry carries an `external_group` property. Idempotent: + // checks the current link first and skips the PATCH if already linked to + // the same group_id. Sets `this.hasChanges = true` only when a PATCH + // actually fires, so the suborg re-evaluation logic in lib/settings.js sees + // a real change signal. + async syncExternalGroup (attrs, nopCommands) { + const groupName = attrs && attrs.external_group + if (!groupName) return + + const groupId = await this.resolveExternalGroupId(groupName) + if (groupId === null) { + const msg = `External group '${groupName}' not found for org ${this.repo.owner} (team '${attrs.name}'). This is expected if the team/group is newly added and has not finished SCIM-provisioning yet.` + // Non-fatal: a brand-new team's external group commonly doesn't exist + // yet until the Azure AD -> GitHub SCIM cycle completes, so this is + // logged as a warning (not logError) to avoid failing the whole sync. + this.log.warn(msg) + // For PR dry-run / nop mode, surface it as a WARNING (not ERROR) in the + // check_run output, so it's visible but doesn't mark the run as failed. + if (this.nop && Array.isArray(nopCommands)) { + nopCommands.push(new NopCommand(this.constructor.name, this.repo, null, msg, 'WARNING')) + } + return + } + + const linkParams = { + org: this.repo.owner, + team_slug: attrs.name, + group_id: groupId + } + + if (this.nop) { + if (Array.isArray(nopCommands)) { + nopCommands.push(new NopCommand( + this.constructor.name, + this.repo, + this.github.request.endpoint(`PATCH ${teamExternalGroupsEndpoint}`, linkParams), + `Link team ${attrs.name} to external group '${groupName}'` + )) + } + return + } + + // Idempotency: skip the PATCH if the team is already linked to this group. + try { + const current = await this.github.request(`GET ${teamExternalGroupsEndpoint}`, { + org: this.repo.owner, + team_slug: attrs.name + }) + const currentGroups = (current && current.data && current.data.groups) || [] + if (currentGroups.some(g => g.group_id === groupId)) { + this.log.debug(`Team ${attrs.name} is already linked to external group '${groupName}' (id=${groupId}); skipping.`) + return + } + } catch (e) { + // 404 here means no current link; fall through to PATCH. Any other + // error is non-fatal -- the PATCH itself is idempotent on the server. + if (e.status !== 404) { + this.logError(`Error fetching current external group for team ${attrs.name}: ${e}`) + } + } + + try { + await this.github.request(`PATCH ${teamExternalGroupsEndpoint}`, linkParams) + this.log.debug(`Linked team ${attrs.name} to external group '${groupName}' (id=${groupId}).`) + // Surface this change so suborg re-evaluation (in lib/settings.js) and + // other consumers see that the team plugin made a real change. + this.hasChanges = true + } catch (e) { + this.logError(`Error linking team ${attrs.name} to external group '${groupName}' (id=${groupId}): ${e}`) + } + } } diff --git a/lib/plugins/validator.js b/lib/plugins/validator.js index 8ee72af9a..25b39cfe0 100644 --- a/lib/plugins/validator.js +++ b/lib/plugins/validator.js @@ -12,7 +12,7 @@ module.exports = class Validator { sync () { try { - return this.github.repos.getAllTopics({ + return this.github.rest.repos.getAllTopics({ owner: this.repo.owner, repo: this.repo.repo, mediaType: { @@ -28,7 +28,7 @@ module.exports = class Validator { } if (res.data.names.find(x => x === 'validation-error')) { res.data.names = res.data.names.filter(x => x !== 'validation-error') - return this.github.repos.replaceAllTopics({ + return this.github.rest.repos.replaceAllTopics({ owner: this.repo.owner, repo: this.repo.repo, names: res.data.names, @@ -46,7 +46,7 @@ module.exports = class Validator { } if (!res.data.names.find(x => x === 'validation-error')) { res.data.names.push('validation-error') - return this.github.repos.replaceAllTopics({ + return this.github.rest.repos.replaceAllTopics({ owner: this.repo.owner, repo: this.repo.repo, names: res.data.names, diff --git a/lib/plugins/variables.js b/lib/plugins/variables.js index 25795c408..292c9b1b2 100644 --- a/lib/plugins/variables.js +++ b/lib/plugins/variables.js @@ -1,5 +1,5 @@ -const _ = require('lodash') const Diffable = require('./diffable') +const NopCommand = require('../nopcommand') module.exports = class Variables extends Diffable { constructor (...args) { @@ -14,183 +14,106 @@ module.exports = class Variables extends Diffable { } /** - * Look-up existing variables for a given repository - * - * @see {@link https://docs.github.com/en/rest/actions/variables?apiVersion=2022-11-28#list-repository-variables} list repository variables - * @returns {Array.} Returns a list of variables that exist in a repository - */ - async find () { + * Look up existing variables for a given repository. + * Strips API-only metadata fields (created_at, updated_at) so that + * changed() can do a clean value comparison. + * + * @see {@link https://docs.github.com/en/rest/actions/variables?apiVersion=2022-11-28#list-repository-variables} + * @returns {Promise>} + */ + find () { this.log.debug(`Finding repo vars for ${this.repo.owner}/${this.repo.repo}`) - const { data: { variables } } = await this.github.request('GET /repos/:org/:repo/actions/variables', { + return this.github.request('GET /repos/:org/:repo/actions/variables', { org: this.repo.owner, repo: this.repo.repo - }) - return variables + }).then(({ data: { variables } }) => variables.map(({ name, value }) => ({ name, value }))) } /** - * Compare the existing variables with what we've defined as code - * - * @param {Array.} existing Existing variables defined in the repository - * @param {Array.} variables Variables that we have defined as code - * - * @returns {object} The results of a list comparison - */ - getChanged (existing, variables = []) { - const result = - JSON.stringify( - existing.sort((x1, x2) => { - return x1.name.toUpperCase().localeCompare(x2.name.toUpperCase()) - }) - ) !== - JSON.stringify( - variables.sort((x1, x2) => { - return x1.name.toUpperCase().localeCompare(x2.name.toUpperCase()) - }) - ) - return result - } - - /** - * Compare existing variables with what's defined - * - * @param {Object} existing The existing entries in GitHub - * @param {Object} attrs The entries defined as code - * - * @returns - */ + * Identify which existing variable matches the desired attrs by name. + * + * @param {object} existing An existing variable from the API + * @param {object} attrs A variable defined as code + * @returns {boolean} + */ comparator (existing, attrs) { return existing.name === attrs.name } /** - * Return a list of changed entries - * - * @param {Object} existing The existing entries in GitHub - * @param {Object} attrs The entries defined as code - * - * @returns - */ + * Return true if the existing variable's value differs from the desired value. + * + * @param {object} existing The existing variable from the API + * @param {object} attrs The variable defined as code + * @returns {boolean} + */ changed (existing, attrs) { - return this.getChanged(_.castArray(existing), _.castArray(attrs)) + return existing.value !== attrs.value } /** - * Update an existing variable if the value has changed - * - * @param {Array.} existing Existing variables defined in the repository - * @param {Array.} variables Variables that we have defined as code - * - * @see {@link https://docs.github.com/en/rest/actions/variables?apiVersion=2022-11-28#update-a-repository-variable} update a repository variable - * @returns - */ - async update (existing, variables = []) { - this.log.debug(`Updating a repo var existing params ${JSON.stringify(existing)} and new ${JSON.stringify(variables)}`) - existing = _.castArray(existing) - variables = _.castArray(variables) - const changed = this.getChanged(existing, variables) - - if (changed) { - let existingVariables = [...existing] - for (const variable of variables) { - const existingVariable = existingVariables.find((_var) => _var.name === variable.name) - if (existingVariable) { - existingVariables = existingVariables.filter((_var) => _var.name !== variable.name) - if (existingVariable.value !== variable.value) { - await this.github - .request('PATCH /repos/:org/:repo/actions/variables/:variable_name', { - org: this.repo.owner, - repo: this.repo.repo, - variable_name: variable.name.toUpperCase(), - value: variable.value.toString() - }) - .then((res) => { - return res - }) - .catch((e) => { - this.logError(e) - }) - } - } else { - await this.github - .request('POST /repos/:org/:repo/actions/variables', { - org: this.repo.owner, - repo: this.repo.repo, - name: variable.name.toUpperCase(), - value: variable.value.toString() - }) - .then((res) => { - return res - }) - .catch((e) => { - this.logError(e) - }) - } - } - - for (const variable of existingVariables) { - await this.github - .request('DELETE /repos/:org/:repo/actions/variables/:variable_name', { - org: this.repo.owner, - repo: this.repo.repo, - variable_name: variable.name.toUpperCase() - }) - .then((res) => { - return res - }) - .catch((e) => { - this.logError(e) - }) - } + * Update an existing variable with a new value. + * + * @param {object} existing The existing variable from the API + * @param {object} attrs The desired variable state defined as code + * + * @see {@link https://docs.github.com/en/rest/actions/variables?apiVersion=2022-11-28#update-a-repository-variable} + * @returns {Promise} + */ + update (existing, attrs) { + if (this.nop) { + return Promise.resolve([ + new NopCommand(this.constructor.name, this.repo, null, `Update variable ${attrs.name}`) + ]) } + return this.github.request('PATCH /repos/:org/:repo/actions/variables/:variable_name', { + org: this.repo.owner, + repo: this.repo.repo, + variable_name: attrs.name.toUpperCase(), + value: attrs.value.toString() + }) } /** - * Add a new variable to a given repository - * - * @param {object} variable The variable to add, with name and value - * - * @see {@link https://docs.github.com/en/rest/actions/variables?apiVersion=2022-11-28#create-a-repository-variable} create a repository variable - * @returns - */ - async add (variable) { - this.log.debug(`Adding a repo var with the params ${JSON.stringify(variable)}`) - await this.github - .request('POST /repos/:org/:repo/actions/variables', { - org: this.repo.owner, - repo: this.repo.repo, - name: variable.name, - value: variable.value.toString() - }) - .then((res) => { - return res - }) - .catch((e) => { - this.logError(e) - }) + * Add a new variable to the repository. + * + * @param {object} attrs The variable to add, with name and value + * + * @see {@link https://docs.github.com/en/rest/actions/variables?apiVersion=2022-11-28#create-a-repository-variable} + * @returns {Promise} + */ + add (attrs) { + if (this.nop) { + return Promise.resolve([ + new NopCommand(this.constructor.name, this.repo, null, `Add variable ${attrs.name}`) + ]) + } + return this.github.request('POST /repos/:org/:repo/actions/variables', { + org: this.repo.owner, + repo: this.repo.repo, + name: attrs.name.toUpperCase(), + value: attrs.value.toString() + }) } /** - * Remove variables that aren't defined as code - * - * @param {String} existing Name of the existing variable to remove - * - * @see {@link https://docs.github.com/en/rest/actions/variables?apiVersion=2022-11-28#delete-a-repository-variable} delete a repository variable - * @returns - */ - async remove (existing) { - this.log.debug(`Removing a repo var with the params ${JSON.stringify(existing)}`) - await this.github - .request('DELETE /repos/:org/:repo/actions/variables/:variable_name', { - org: this.repo.owner, - repo: this.repo.repo, - variable_name: existing.name - }) - .then((res) => { - return res - }) - .catch((e) => { - this.logError(e) - }) + * Remove a variable that is no longer defined as code. + * + * @param {object} existing The existing variable to remove + * + * @see {@link https://docs.github.com/en/rest/actions/variables?apiVersion=2022-11-28#delete-a-repository-variable} + * @returns {Promise} + */ + remove (existing) { + if (this.nop) { + return Promise.resolve([ + new NopCommand(this.constructor.name, this.repo, null, `Remove variable ${existing.name}`) + ]) + } + return this.github.request('DELETE /repos/:org/:repo/actions/variables/:variable_name', { + org: this.repo.owner, + repo: this.repo.repo, + variable_name: existing.name.toUpperCase() + }) } } diff --git a/lib/repoSelector.js b/lib/repoSelector.js new file mode 100644 index 000000000..47b40163f --- /dev/null +++ b/lib/repoSelector.js @@ -0,0 +1,159 @@ +const Glob = require('./glob') + +/** + * RepoSelector resolves a set of repository names from fixed criteria. + * + * Supported criteria: + * - name: explicit repo names (or glob patterns) + * - team: repos belonging to a GitHub team + * - custom_properties: repos matching custom property values + * - all: all repos visible to the installation + * + * @param {object} github - Authenticated Octokit client + * @param {string} org - Organization name + * @param {object} log - Logger instance + */ +class RepoSelector { + constructor (github, org, log) { + this.github = github + this.org = org + this.log = log + } + + /** + * Resolve repos from a list of criteria. Returns a Set of repo names. + * + * @param {object} criteria - Selection criteria + * @param {boolean} [criteria.all] - Select all repos in the org + * @param {string[]} [criteria.names] - Explicit repo names or glob patterns + * @param {string[]} [criteria.teams] - Team slugs + * @param {object[]} [criteria.custom_properties] - Array of { name: value } property filters + * @returns {Promise>} Set of resolved repo names + */ + async resolve (criteria) { + if (!criteria) return new Set() + + // "all" takes precedence — return all repos without filtering + if (criteria.all) { + return this.getAllRepos() + } + + const results = new Set() + const promises = [] + + if (criteria.names && Array.isArray(criteria.names)) { + promises.push(this.resolveByName(criteria.names)) + } + + if (criteria.teams && Array.isArray(criteria.teams)) { + promises.push(this.resolveByTeam(criteria.teams)) + } + + if (criteria.custom_properties && Array.isArray(criteria.custom_properties)) { + promises.push(this.resolveByCustomProperties(criteria.custom_properties)) + } + + const resolved = await Promise.all(promises) + for (const repoSet of resolved) { + for (const name of repoSet) { + results.add(name) + } + } + + return results + } + + /** + * Get all repos visible to the installation. + */ + async getAllRepos () { + const repos = new Set() + const repositories = await this.github.paginate('GET /installation/repositories') + for (const repo of repositories) { + repos.add(repo.name) + } + return repos + } + + /** + * Resolve repos by explicit name or glob pattern. + */ + async resolveByName (names) { + const repos = new Set() + const hasGlobs = names.some(n => n.includes('*') || n.includes('?')) + + if (hasGlobs) { + // Need to fetch all repos and match against globs + const allRepos = await this.github.paginate('GET /installation/repositories') + for (const name of names) { + const glob = new Glob(name) + for (const repo of allRepos) { + if (glob.test(repo.name)) { + repos.add(repo.name) + } + } + } + } else { + // Plain names — add directly + for (const name of names) { + repos.add(name) + } + } + + return repos + } + + /** + * Resolve repos by team membership. + */ + async resolveByTeam (teams) { + const repos = new Set() + const teamPromises = teams.map(teamSlug => { + const options = this.github.rest.teams.listReposInOrg.endpoint.merge({ + org: this.org, + team_slug: teamSlug, + per_page: 100 + }) + return this.github.paginate(options) + }) + + const results = await Promise.all(teamPromises) + for (const teamRepos of results) { + for (const repo of teamRepos) { + repos.add(repo.name) + } + } + + return repos + } + + /** + * Resolve repos by custom property values. + * Each entry in the array is an object { propertyName: propertyValue }. + */ + async resolveByCustomProperties (properties) { + const repos = new Set() + const propPromises = properties.map(async (propertyFilter) => { + const [name] = Object.keys(propertyFilter) + const value = propertyFilter[name] + + const query = `props.${name}:${value}` + const encodedQuery = encodeURIComponent(query) + const options = this.github.request.endpoint( + `/orgs/${this.org}/properties/values?repository_query=${encodedQuery}` + ) + return this.github.paginate(options) + }) + + const results = await Promise.all(propPromises) + for (const propRepos of results) { + for (const repo of propRepos) { + repos.add(repo.repository_name) + } + } + + return repos + } +} + +module.exports = RepoSelector diff --git a/lib/settings.js b/lib/settings.js index 6c42e439b..e36ee07c7 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -6,22 +6,662 @@ const Glob = require('./glob') const NopCommand = require('./nopcommand') const MergeDeep = require('./mergeDeep') const Archive = require('./plugins/archive') +const AppInstallations = require('./plugins/appInstallations') +const RepoSelector = require('./repoSelector') +const DeploymentConfig = require('./deploymentConfig') const env = require('./env') + +// Valid `target` values for a disable_plugins entry. +const DISABLE_TARGETS = new Set(['self', 'children', 'all']) +// Valid declaration layers (where a disable_plugins entry can be authored). +const DISABLE_LEVELS = ['deployment', 'org', 'suborg', 'repo'] +// For each declared layer + target, the set of layers from which to STRIP the +// named plugin's config. See plan-v3 matrix. +const DISABLE_STRIP_MATRIX = { + deployment: { + self: ['deployment'], + children: ['org', 'suborg', 'repo'], + all: ['deployment', 'org', 'suborg', 'repo'] + }, + org: { + self: ['org'], + children: ['suborg', 'repo'], + all: ['org', 'suborg', 'repo'] + }, + suborg: { + self: ['suborg'], + children: ['repo'], + all: ['suborg', 'repo'] + }, + repo: { + self: ['repo'], + children: ['repo'], // normalized; repo has no children + all: ['repo'] + } +} const CONFIG_PATH = env.CONFIG_PATH const eta = new Eta({ views: path.join(__dirname) }) const SCOPE = { ORG: 'org', REPO: 'repo' } // Determine if the setting is a org setting or repo setting +// Maximum size (in characters) of a single PR comment / check-run summary body. +const COMMENT_LIMIT = 55536 const yaml = require('js-yaml') +// When a repo-yml change applies teams/properties/etc to a repo, the repo may +// change suborg config matches (via suborgteams/suborgproperties/suborgrepos). +// Re-run updateRepos for the same repo at most this many times. Depth=1 is the +// tightest cap: we resolve a single hop of newly-matched suborg per sync. +const MAX_REEVALUATION_DEPTH = 1 + +// --------------------------------------------------------------------------- +// NOP change-detection helpers +// --------------------------------------------------------------------------- + +// Recursively determines whether a value is "empty" (null/undefined, empty +// array/object, or a structure containing only empty values). +function isDeepEmpty (value) { + if (value === null || value === undefined) return true + if (Array.isArray(value)) return value.length === 0 || value.every(isDeepEmpty) + if (typeof value === 'object') { + const keys = Object.keys(value) + return keys.length === 0 || keys.every(k => isDeepEmpty(value[k])) + } + return false +} + +// Determines whether a NopCommand action represents no meaningful change. +// String actions (message-only NOP results) are treated as non-empty so they +// are not silently dropped from reporting. +function isEmptyChange (action) { + if (!action) return true + if (typeof action === 'string') return action.length === 0 + const { additions, deletions, modifications } = action + if (additions === null && deletions === null && modifications === null) return true + return isDeepEmpty(additions) && isDeepEmpty(deletions) && isDeepEmpty(modifications) +} + +// Produce a canonical (key-sorted) clone so deep equality is order-independent. +function canonicalize (value) { + if (value === null || typeof value !== 'object') return value + if (Array.isArray(value)) return value.map(canonicalize) + return Object.keys(value).sort().reduce((acc, key) => { + acc[key] = canonicalize(value[key]) + return acc + }, {}) +} + +function stableStringify (value) { + return JSON.stringify(canonicalize(value)) +} + +/** + * Determines which named entries in an array-based config section actually + * changed between the base branch and the PR branch. Returns a Set of entry + * names that differ. Uses name-indexed Maps (O(n)) and order-independent deep + * equality to avoid false positives from key ordering. + */ +function getChangedEntryNames (baseEntries, prEntries) { + const changed = new Set() + if (!baseEntries && !prEntries) return changed + if (!baseEntries || !Array.isArray(baseEntries)) { + // All PR entries are new + if (Array.isArray(prEntries)) prEntries.forEach(e => { if (e && e.name) changed.add(e.name) }) + return changed + } + if (!prEntries || !Array.isArray(prEntries)) { + // All base entries are deleted + baseEntries.forEach(e => { if (e && e.name) changed.add(e.name) }) + return changed + } + const baseByName = new Map() + baseEntries.forEach(e => { if (e && e.name) baseByName.set(e.name, e) }) + const prByName = new Map() + prEntries.forEach(e => { if (e && e.name) prByName.set(e.name, e) }) + // Added or modified entries + for (const [name, prEntry] of prByName) { + const baseEntry = baseByName.get(name) + if (!baseEntry || stableStringify(baseEntry) !== stableStringify(prEntry)) { + changed.add(name) + } + } + // Deleted entries + for (const name of baseByName.keys()) { + if (!prByName.has(name)) changed.add(name) + } + return changed +} + +/** + * Filters a NOP action's arrays to only include entries whose 'name' is in the + * changedNames set. Returns a new action with filtered arrays, or null if + * nothing meaningful remains. + */ +function filterActionByChangedNames (action, changedNames) { + if (!action || typeof action === 'string') return action + + const { additions, deletions, modifications, ...rest } = action + + const filterArray = (arr) => { + if (!arr || !Array.isArray(arr)) return arr + return arr.filter(entry => { + if (!entry || typeof entry !== 'object') return true + // Keep entries whose name is in the changed set + if (entry.name && changedNames.has(entry.name)) return true + // Keep entries without a name field (structural entries like conditions) + if (!entry.name) return true + return false + }) + } + + const filtered = { + ...rest, + additions: filterArray(additions), + deletions: filterArray(deletions), + modifications: filterArray(modifications) + } + + // Return null if everything was filtered out + if (isEmptyChange(filtered)) return null + return filtered +} + +// --------------------------------------------------------------------------- +// Centralized ruleset bypass_actors helpers +// --------------------------------------------------------------------------- + +// Builds a de-duplication key for a bypass actor entry, keyed on actor_type +// plus either actor_id or name (whichever is present). +function bypassActorKey (actor) { + const actorType = actor.actor_type || 'unknown' + if (actor.actor_id !== undefined && actor.actor_id !== null) return `actor_id:${actorType}:${actor.actor_id}` + if (actor.name) return `name:${actorType}:${actor.name}` + return JSON.stringify(actor) +} + +// Merges centrally-declared bypass actors into a ruleset's existing +// bypass_actors, de-duplicating by (actor_type, actor_id|name). Centrally +// declared actors take precedence over a repo/suborg-declared entry with the +// same key (e.g. to update bypass_mode). Every entry is shallow-cloned so +// the Rulesets plugin's in-place name->id resolution (resolveBypassActor) +// never mutates a shared object across multiple rulesets/repos, which sync +// concurrently (see Settings#updateAll). +function mergeBypassActors (existing, centralized) { + if (!Array.isArray(centralized) || centralized.length === 0) return existing + const merged = new Map() + for (const actor of (existing || [])) merged.set(bypassActorKey(actor), { ...actor }) + for (const actor of centralized) merged.set(bypassActorKey(actor), { ...actor }) + return Array.from(merged.values()) +} + +// Returns a copy of `rulesetEntries` with centrally-declared bypass actors +// merged into every entry's bypass_actors. This runs before the Rulesets +// plugin's own current-vs-desired diffing (Diffable.sync), so a ruleset only +// results in an actual GitHub API call when its resulting bypass_actors +// differ from what's already applied on GitHub — i.e. new or updated +// rulesets, or ones missing the centralized actors. Unaffected, unchanged +// rulesets are left as no-ops by the normal diff, with no special-casing +// needed here. +function applyCentralizedBypassActors (rulesetEntries, centralizedBypassActors) { + if (!Array.isArray(rulesetEntries) || !Array.isArray(centralizedBypassActors) || centralizedBypassActors.length === 0) { + return rulesetEntries + } + return rulesetEntries.map(ruleset => { + if (!ruleset || typeof ruleset !== 'object') return ruleset + return { ...ruleset, bypass_actors: mergeBypassActors(ruleset.bypass_actors, centralizedBypassActors) } + }) +} + +// --------------------------------------------------------------------------- +// NOP change-rendering helpers (collapsible, field-level diff summaries) +// --------------------------------------------------------------------------- + +function buildChangeSections (changes, baseConfig, config) { + return Object.keys(changes).map(plugin => { + const isAppInstallations = plugin === 'app_installations' + const repoSections = [] + Object.keys(changes[plugin]).forEach(repo => { + const targetMap = new Map() + changes[plugin][repo].forEach(action => { + const actionTargets = isAppInstallations + ? appInstallationTargets(action) + : targetsForAction(plugin, repo, action, baseConfig, config) + actionTargets.forEach(target => { + if (!targetMap.has(target.target)) { + targetMap.set(target.target, { + target: target.target, + flat: target.flat === true, + rows: [] + }) + } + targetMap.get(target.target).rows.push(...target.rows) + }) + }) + repoSections.push({ + repo, + targets: Array.from(targetMap.values()).filter(target => target.rows.length > 0) + }) + }) + + const filteredRepoSections = repoSections.filter(repoSection => repoSection.targets.length > 0) + const changeCount = filteredRepoSections.reduce((count, repoSection) => { + return count + repoSection.targets.reduce((targetCount, target) => targetCount + target.rows.length, 0) + }, 0) + // For flat targets (app_installations) each row is a distinct change; for + // regular targets each target counts as one changed setting. + const targetCount = filteredRepoSections.reduce((count, repoSection) => { + return count + repoSection.targets.reduce((tc, target) => tc + (target.flat ? target.rows.length : 1), 0) + }, 0) + const repoCount = filteredRepoSections.length + const subjectSingular = isAppInstallations ? 'app' : 'repo' + const subjectPlural = isAppInstallations ? 'apps' : 'repos' + const targetSingular = plugin.toLowerCase() === 'rulesets' ? 'policy' : 'setting' + const targetPlural = plugin.toLowerCase() === 'rulesets' ? 'policies' : 'settings' + const impactSummary = `${repoCount} ${pluralize(repoCount, subjectSingular, subjectPlural)}, ${targetCount} ${pluralize(targetCount, targetSingular, targetPlural)} changed` + return { + plugin, + repoSections: filteredRepoSections, + repoCount, + targetCount, + changeCount, + impactSummary, + summary: `${plugin} - ${impactSummary}` + } + }).filter(section => section.repoSections.length > 0) +} + +// app_installations changes are presented with the GitHub App as the subject +// (heading) and a flat list of repositories added/removed (or a toggle to +// "all"). Returns a single flat target whose rows carry a `label` per change. +function appInstallationTargets (action) { + if (!action || typeof action === 'string') { + return [{ target: '', flat: true, rows: action ? [{ change: 'Info', label: action }] : [] }] + } + const toList = value => { + if (value === null || value === undefined) return [] + const arr = Array.isArray(value) ? value : [value] + return arr + .filter(entry => !isDeepEmpty(entry)) + .map(entry => (typeof entry === 'string' ? entry : (getEntryIdentityValue(entry) || JSON.stringify(entry)))) + } + const rows = [] + toList(action.additions).forEach(label => rows.push({ change: 'Added', label })) + toList(action.modifications).forEach(label => rows.push({ change: 'Modified', label })) + toList(action.deletions).forEach(label => rows.push({ change: 'Deleted', label })) + if (rows.length === 0 && action.msg) rows.push({ change: 'Info', label: action.msg }) + return [{ target: '', flat: true, rows }] +} + +function renderChangeSections (changeSections) { + return changeSections.map(section => { + const repoBlocks = section.repoSections.map(repoSection => { + const targetBlocks = repoSection.targets.map(target => { + if (target.flat) { + return target.rows.map(row => { + const marker = changeMarker(row.change) + return row.change === 'Info' + ? `- ${marker} ${markdownText(row.label)}` + : `- ${marker} ${markdownInlineCode(row.label)}` + }).join('\n') + } + return `- ${markdownInlineCode(target.target)}\n${renderFieldChangeList(target.rows, ' ')}` + }) + return `**${markdownText(displayRepoName(repoSection.repo))}**\n${targetBlocks.join('\n')}` + }) + + return `
\n${escapeHtml(section.plugin)} — ${escapeHtml(section.impactSummary)}\n\n${repoBlocks.join('\n\n')}\n\n
` + }) +} + +function affectedRepoCount (changeSections) { + return new Set(changeSections + // app_installations sections are keyed by app subject, not repositories, + // so they must not inflate the "repos affected" count. + .filter(section => section.plugin !== 'app_installations') + .flatMap(section => { + return section.repoSections.map(repoSection => displayRepoName(repoSection.repo)) + })).size +} + +function displayRepoName (repo) { + return repo && repo.endsWith('(org)') ? env.ADMIN_REPO : repo +} + +function renderFieldChangeList (rows, indent = '') { + return rows.map(row => { + const marker = changeMarker(row.change) + if (row.change === 'Info') { + return `${indent}- ${marker} ${markdownText(row.after || row.before || row.field)}` + } + if (row.change === 'Modified') { + return `${indent}- ${marker} ${markdownInlineCode(row.field)}\n${indent} - before: ${markdownInlineCode(row.before, row.after)}\n${indent} - after: ${markdownInlineCode(row.after, row.before)}` + } + const value = row.change === 'Deleted' ? row.before : row.after + return `${indent}- ${marker} ${markdownInlineCode(row.field)}: ${markdownInlineCode(value)}` + }).join('\n') +} + +function changeMarker (change) { + if (change === 'Added') return '+' + if (change === 'Deleted') return '-' + if (change === 'Modified') return '~' + return 'i' +} + +function targetsForAction (plugin, repo, action, baseConfig, config) { + if (typeof action === 'string') { + return [createTarget(plugin, [createFieldChangeRow('Info', 'message', '', action)])] + } + + const configTargets = targetsFromConfigDiff(plugin, repo, action, baseConfig, config) + if (configTargets) return configTargets + + const additions = normalizeChangeEntries(action && action.additions) + const deletions = normalizeChangeEntries(action && action.deletions) + const modifications = normalizeChangeEntries(action && action.modifications) + + const usedDeletions = new Set() + const targets = [] + + additions.forEach(entry => { + const target = getChangeTarget(entry, plugin) + targets.push(createTarget(target, rowsForAddedOrDeleted('Added', entry, target))) + }) + + modifications.forEach((entry, index) => { + const target = getChangeTarget(entry, plugin) + const match = findMatchingDeletion(entry, index, modifications, deletions, usedDeletions) + if (match.index !== -1) usedDeletions.add(match.index) + targets.push(createTarget(target, rowsForModification(match.entry, entry, target))) + }) + + deletions.forEach((entry, index) => { + if (usedDeletions.has(index)) return + const target = getChangeTarget(entry, plugin) + targets.push(createTarget(target, rowsForAddedOrDeleted('Deleted', entry, target))) + }) + + if (targets.length === 0 && action && action.msg) { + return [createTarget(plugin, [createFieldChangeRow('Info', 'message', '', action.msg)])] + } + + return targets +} + +function targetsFromConfigDiff (plugin, repo, action, baseConfig, config) { + if (!baseConfig || !config || !action || typeof action === 'string') return null + + const pluginSection = plugin.toLowerCase() + const isOrgRulesets = repo && repo.endsWith('(org)') && pluginSection === 'rulesets' + const baseEntries = baseConfig[pluginSection] + const prEntries = config[pluginSection] + + if (!isOrgRulesets) return null + if (!Array.isArray(baseEntries) || !Array.isArray(prEntries)) return null + + const actionNames = getActionEntryNames(action) + if (actionNames.size === 0) return null + + const changedNames = new Set(Array.from(getChangedEntryNames(baseEntries, prEntries)).filter(name => actionNames.has(name))) + if (changedNames.size === 0) return null + + const targets = [] + Array.from(changedNames).sort().forEach(name => { + const oldEntry = findEntryByIdentity(baseEntries, name) + const newEntry = findEntryByIdentity(prEntries, name) + let rows = [] + + if (oldEntry && newEntry) { + rows = rowsForModification(oldEntry, newEntry, name) + } else if (newEntry) { + rows = rowsForAddedOrDeleted('Added', newEntry, name) + } else if (oldEntry) { + rows = rowsForAddedOrDeleted('Deleted', oldEntry, name) + } + + if (rows.length > 0) targets.push(createTarget(name, rows)) + }) + + return targets.length > 0 ? targets : null +} + +function getActionEntryNames (action) { + const names = new Set() + ;['additions', 'deletions', 'modifications'].forEach(actionField => { + normalizeChangeEntries(action[actionField]).forEach(entry => { + const identity = getEntryIdentityValue(entry) + if (identity) names.add(identity) + }) + }) + return names +} + +function findEntryByIdentity (entries, identity) { + return entries.find(entry => getEntryIdentityValue(entry) === identity) +} + +function createTarget (target, rows) { + return { + target, + rows: rows.filter(row => row) + } +} + +function normalizeChangeEntries (value) { + if (isDeepEmpty(value)) return [] + return Array.isArray(value) ? value.filter(entry => !isDeepEmpty(entry)) : [value] +} + +function findMatchingDeletion (entry, index, modifications, deletions, usedDeletions) { + const identity = getChangeIdentity(entry) + if (identity) { + const matchIndex = deletions.findIndex((deletion, deletionIndex) => { + if (usedDeletions.has(deletionIndex)) return false + return getChangeIdentity(deletion) === identity + }) + if (matchIndex !== -1) return { entry: deletions[matchIndex], index: matchIndex } + } + + if (modifications.length === 1 && deletions.length === 1 && !usedDeletions.has(0)) { + return { entry: deletions[0], index: 0 } + } + + return { entry: null, index: -1 } +} + +function getChangeIdentity (entry) { + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null + const field = MergeDeep.NAME_FIELDS.find(field => Object.prototype.hasOwnProperty.call(entry, field)) + if (!field) return null + return `${field}:${formatValue(entry[field]).text}` +} + +function getChangeTarget (entry, fallback) { + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return formatValue(entry).text || fallback + return getEntryIdentityValue(entry) || fallback +} + +function getEntryIdentityValue (entry) { + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null + const field = MergeDeep.NAME_FIELDS.find(field => Object.prototype.hasOwnProperty.call(entry, field)) + return field ? formatValue(entry[field]).text : null +} + +function rowsForAddedOrDeleted (change, entry, target) { + const flattened = flattenForSummary(entry, true) + const fields = Object.keys(flattened) + if (fields.length === 0) return [createFieldChangeRow(change, 'value', change === 'Added' ? '' : target, change === 'Added' ? target : '')] + + return fields.map(path => { + const value = flattened[path] + return createFieldChangeRow(change, path, change === 'Deleted' ? value : '', change === 'Deleted' ? '' : value) + }) +} + +function rowsForModification (oldEntry, newEntry, target) { + if (!oldEntry || typeof oldEntry !== 'object' || !newEntry || typeof newEntry !== 'object') { + return rowsForAddedOrDeleted('Modified', newEntry, target) + } + + const oldPaths = flattenForSummary(oldEntry, true) + const newPaths = flattenForSummary(newEntry, true) + const paths = Array.from(new Set([...Object.keys(oldPaths), ...Object.keys(newPaths)])).sort() + const rows = paths.map(path => { + const hasOld = Object.prototype.hasOwnProperty.call(oldPaths, path) + const hasNew = Object.prototype.hasOwnProperty.call(newPaths, path) + if (hasOld && hasNew && comparableValue(oldPaths[path]) !== comparableValue(newPaths[path])) { + return createFieldChangeRow('Modified', path, oldPaths[path], newPaths[path]) + } + if (!hasOld && hasNew) { + return createFieldChangeRow('Added', path, '', newPaths[path]) + } + if (hasOld && !hasNew) { + return createFieldChangeRow('Deleted', path, oldPaths[path], '') + } + return null + }).filter(row => row) + + if (rows.length > 0) return rows + return rowsForAddedOrDeleted('Modified', newEntry, target) +} + +function createFieldChangeRow (change, field, before, after) { + return { + change, + field, + before, + after + } +} + +function flattenForSummary (value, skipRootIdentity = false, prefix = '') { + if (value === null || value === undefined || typeof value !== 'object') { + return { [prefix || 'value']: formatValue(value) } + } + + if (Array.isArray(value)) { + return { [prefix || 'value']: formatValue(value) } + } + + const result = {} + Object.keys(value).forEach(key => { + if (!prefix && skipRootIdentity && MergeDeep.NAME_FIELDS.includes(key)) return + const path = prefix ? `${prefix}.${key}` : key + const child = value[key] + + if (child && typeof child === 'object' && !Array.isArray(child)) { + Object.assign(result, flattenForSummary(child, false, path)) + } else { + result[path] = formatValue(child) + } + }) + + return result +} + +function formatValue (value) { + if (value && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, 'text')) return value + if (value === null) return { text: 'null', compare: 'null' } + if (value === undefined) return { text: '', compare: '' } + if (typeof value === 'string') return { text: value, compare: value } + if (typeof value === 'number' || typeof value === 'boolean') return { text: `${value}`, compare: `${value}` } + if (Array.isArray(value) && value.every(item => item === null || ['string', 'number', 'boolean'].includes(typeof item))) { + const text = value.map(item => formatValue(item).text).join(', ') + return { text, compare: text } + } + const json = JSON.stringify(value) + return { + text: truncate(json, 180), + compare: json + } +} + +function comparableValue (value) { + const displayValue = formatValue(value) + return Object.prototype.hasOwnProperty.call(displayValue, 'compare') ? displayValue.compare : displayValue.text +} + +function truncate (value, limit = 180) { + if (!value || value.length <= limit) return value + return `${value.substring(0, limit - 3)}...` +} + +function truncateAroundDifference (value, otherValue, limit = 180) { + if (!value || value.length <= limit) return value + if (!otherValue || value === otherValue) return truncate(value, limit) + + let prefixLength = 0 + while ( + prefixLength < value.length && + prefixLength < otherValue.length && + value[prefixLength] === otherValue[prefixLength] + ) { + prefixLength++ + } + + let suffixLength = 0 + while ( + suffixLength < value.length - prefixLength && + suffixLength < otherValue.length - prefixLength && + value[value.length - 1 - suffixLength] === otherValue[otherValue.length - 1 - suffixLength] + ) { + suffixLength++ + } + + const contextLength = Math.floor((limit - 6) / 2) + const start = Math.max(0, prefixLength - contextLength) + const end = Math.min(value.length, value.length - suffixLength + contextLength) + const prefix = start > 0 ? '...' : '' + const suffix = end < value.length ? '...' : '' + return truncate(`${prefix}${value.substring(start, end)}${suffix}`, limit) +} + +function truncateWithSuffix (value, limit, suffix) { + if (!value || value.length <= limit) return value + return `${value.substring(0, limit - suffix.length)}${suffix}` +} + +function pluralize (count, singular, plural) { + return count === 1 ? singular : plural +} + +function markdownInlineCode (value, comparedWith) { + return `\`${markdownText(value, comparedWith).replaceAll('`', '\\`')}\`` +} + +function markdownText (value, comparedWith) { + const displayValue = formatValue(value) + const otherDisplayValue = comparedWith === undefined ? null : formatValue(comparedWith) + const text = otherDisplayValue + ? truncateAroundDifference(displayValue.compare || displayValue.text, otherDisplayValue.compare || otherDisplayValue.text) + : displayValue.text + return escapeHtml(text) + .replaceAll('\n', ' ') +} + +function escapeHtml (value) { + return `${value}` + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') +} + class Settings { static fileCache = {} - static async syncAll (nop, context, repo, config, ref) { - const settings = new Settings(nop, context, repo, config, ref) + static async syncAll (nop, context, repo, config, ref, baseConfig, changedFiles = {}) { + const settings = new Settings(nop, context, repo, config, ref, null, baseConfig) + settings.setChangedConfigTargets(changedFiles.repos, changedFiles.subOrgs) try { + settings.checkValidatorsCompiled() await settings.loadConfigs() + settings.trackChangedReposFromSubOrgConfigs() // settings.repoConfigs = await settings.getRepoConfigs() await settings.updateOrg() + await settings.syncAppInstallations({ + appGithub: context.appGithub, + enterpriseSlug: context.enterpriseSlug + }) await settings.updateAll() + await settings.updateChangedRepoConfigs(changedFiles.repos) await settings.handleResults() } catch (error) { settings.logError(error.message) @@ -33,6 +673,7 @@ class Settings { static async syncSubOrgs (nop, context, suborg, repo, config, ref) { const settings = new Settings(nop, context, repo, config, ref, suborg) try { + settings.checkValidatorsCompiled() await settings.loadConfigs() await settings.updateAll() await settings.handleResults() @@ -42,9 +683,94 @@ class Settings { } } + static async syncSelectedRepos (nop, context, repos, subOrgs, config, ref, baseConfig, baseRef) { + const settings = new Settings(nop, context, context.repo(), config, ref, null, baseConfig) + settings.setChangedConfigTargets(repos, subOrgs) + + try { + settings.checkValidatorsCompiled() + // Track repos affected by changed suborg config files so base-config + // filtering knows which repo-level results to keep during NOP runs. + settings.subOrgConfigs = await settings.getSubOrgConfigs() + settings.trackChangedReposFromSubOrgConfigs() + + // Identify repos removed from suborg targeting due to targeting rule + // changes in the suborg config file. These repos need processing so + // their suborg-applied settings (e.g. rulesets) are cleaned up. + if (subOrgs.length > 0 && baseRef) { + const removalResult = await settings.getReposRemovedFromSubOrgTargeting(subOrgs, baseRef) + const removedRepos = removalResult.repos + const previousPluginSections = removalResult.previousPluginSections + if (removedRepos.length > 0) { + settings.log.debug(`Repos removed from suborg targeting: ${JSON.stringify(removedRepos)}`) + settings.log.debug(`Previous suborg plugin sections to clean up: ${JSON.stringify(previousPluginSections)}`) + // Add removed repos to changedRepoNames so NOP filtering keeps their results + if (!settings.changedRepoNames) { + settings.changedRepoNames = new Set() + } + for (const repoName of removedRepos) { + settings.changedRepoNames.add(repoName) + } + // Process removed repos with org-only config (no suborg layer). + // Inject empty arrays for plugin sections that were in the previous + // suborg config so the plugins are instantiated and can detect/remove + // existing entries that are no longer desired. + settings.removedFromSubOrgPluginSections = previousPluginSections + for (const repoName of removedRepos) { + if (settings.isRestricted(repoName)) continue + if (settings.processedRepoNames.has(repoName)) continue + const repo = { owner: context.repo().owner, repo: repoName } + settings.repoConfigs = await settings.getRepoConfigs(repo) + await settings.updateRepos(repo) + } + settings.removedFromSubOrgPluginSections = null + } + } + + // Re-eval is enabled only for the per-repo iteration (repo-yml change + // path). The trailing suborg iteration below already iterates all suborg + // repos, so it is left with the flag off. + settings.reevaluateOnChange = true + for (const repo of repos) { + settings.repo = repo + await settings.loadConfigs(repo) + if (settings.isRestricted(repo.repo)) { + continue + } + await settings.updateRepos(repo) + } + settings.reevaluateOnChange = false + for (const suborg of subOrgs) { + settings.subOrgConfigMap = [suborg] + settings.suborgChange = !!suborg + await settings.loadConfigs() + await settings.updateAll() + } + + // Sync app installations for affected apps (delta mode) + await settings.syncAppInstallations({ + appGithub: context.appGithub, + enterpriseSlug: context.enterpriseSlug, + changedSubOrgs: subOrgs, + changedRepos: repos, + baseRef + }) + + await settings.handleResults() + } catch (error) { + settings.logError(error.message) + await settings.handleResults() + } + } + static async sync (nop, context, repo, config, ref) { const settings = new Settings(nop, context, repo, config, ref) try { + settings.checkValidatorsCompiled() + // Repo-yml change path: re-evaluate suborg membership for this repo if + // the applied changes (teams/custom_properties/new repo) cause it to + // newly match a suborg config. + settings.reevaluateOnChange = true await settings.loadConfigs(repo) if (settings.isRestricted(repo.repo)) { return @@ -63,13 +789,14 @@ class Settings { await settings.handleResults() } - constructor (nop, context, repo, config, ref, suborg) { + constructor (nop, context, repo, config, ref, suborg, baseConfig) { this.ref = ref this.context = context this.installation_id = context.payload.installation.id this.github = context.octokit this.repo = repo this.config = config + this.baseConfig = baseConfig || null this.nop = nop this.suborgChange = !!suborg // If suborg config has been updated, do not load the entire suborg config, and only process repos restricted to it. @@ -81,24 +808,205 @@ class Settings { this.errors = [] this.configvalidators = {} this.overridevalidators = {} + // Collect any validator scripts that fail to compile. We cannot throw from + // the constructor: every static entry point calls `new Settings(...)` + // OUTSIDE its try/catch, so a throw here would bypass handleResults and the + // check run would never be marked as failed. Instead we record the failures + // here and abort the sync via checkValidatorsCompiled() inside each flow. + this.validatorCompileErrors = [] const overridevalidators = config.overridevalidators if (this.isIterable(overridevalidators)) { for (const validator of overridevalidators) { - // eslint-disable-next-line no-new-func - const f = new Function('baseconfig', 'overrideconfig', 'githubContext', validator.script) - this.overridevalidators[validator.plugin] = { canOverride: f, error: validator.error } + try { + // eslint-disable-next-line no-new-func + const f = new Function('baseconfig', 'overrideconfig', 'githubContext', validator.script) + this.overridevalidators[validator.plugin] = { canOverride: f, error: validator.error } + } catch (e) { + this.validatorCompileErrors.push(`Invalid overridevalidator script for plugin '${validator.plugin}': ${e.message}`) + } } } const configvalidators = config.configvalidators if (this.isIterable(configvalidators)) { for (const validator of configvalidators) { this.log.debug(`Logging each script: ${typeof validator.script}`) - // eslint-disable-next-line no-new-func - const f = new Function('baseconfig', 'githubContext', validator.script) - this.configvalidators[validator.plugin] = { isValid: f, error: validator.error } + try { + // eslint-disable-next-line no-new-func + const f = new Function('baseconfig', 'githubContext', validator.script) + this.configvalidators[validator.plugin] = { isValid: f, error: validator.error } + } catch (e) { + this.validatorCompileErrors.push(`Invalid configvalidator script for plugin '${validator.plugin}': ${e.message}`) + } } } this.mergeDeep = new MergeDeep(this.log, this.github, [], this.configvalidators, this.overridevalidators) + // Suborg re-evaluation state (used only when reevaluateOnChange is true). + // - reevaluationDepth: repo name -> number of re-evaluation passes done. + // - reevaluatedRepos: repo name -> set of suborg source paths seen so far + // (used for stability comparison; if no new sources appear, we stop). + this.reevaluateOnChange = false + this.reevaluationDepth = new Map() + this.reevaluatedRepos = new Map() + this.processedRepoNames = new Set() + } + + // Record which repo override files and suborg config files changed in the PR. + // Used during NOP runs to keep repo-level results whose config actually + // changed (and filter out pre-existing drift). + setChangedConfigTargets (changedRepos = [], changedSubOrgs = []) { + const repoNames = Array.isArray(changedRepos) + ? changedRepos.map(repo => repo && repo.repo).filter(Boolean) + : [] + + this.changedRepoNames = new Set(repoNames) + this.changedSubOrgConfigs = Array.isArray(changedSubOrgs) ? changedSubOrgs : [] + } + + // Expand changedSubOrgConfigs (changed suborg config files) into the set of + // repos they affect, adding them to changedRepoNames. + trackChangedReposFromSubOrgConfigs () { + if (!Array.isArray(this.changedSubOrgConfigs) || this.changedSubOrgConfigs.length === 0 || !this.subOrgConfigs) { + return + } + + const changedSubOrgPaths = new Set( + this.changedSubOrgConfigs + .map(subOrg => subOrg && subOrg.path) + .filter(Boolean) + ) + + if (changedSubOrgPaths.size === 0) { + return + } + + if (!this.changedRepoNames) { + this.changedRepoNames = new Set() + } + + Object.entries(this.subOrgConfigs).forEach(([repoName, subOrgConfig]) => { + if (subOrgConfig && subOrgConfig.source && changedSubOrgPaths.has(subOrgConfig.source)) { + this.changedRepoNames.add(repoName) + } + }) + } + + // Identify repos that were previously targeted by suborg config files but + // are no longer targeted after the targeting rules changed. Loads the + // previous version of each changed suborg file from `baseRef`, resolves its + // targeting, and returns repo names present in the old targeting but absent + // from the current `this.subOrgConfigs`. + async getReposRemovedFromSubOrgTargeting (changedSubOrgs, baseRef) { + const emptyResult = { repos: [], previousPluginSections: [] } + if (!changedSubOrgs || changedSubOrgs.length === 0 || !baseRef) { + return emptyResult + } + + const removedRepos = [] + let previousPluginSections = null + + for (const suborg of changedSubOrgs) { + const filePath = suborg.path + if (!filePath) continue + + // Load the previous version of this suborg config file + let previousData + try { + previousData = await this.loadYamlFromRef(filePath, baseRef) + } catch (e) { + this.log.debug(`Could not load previous suborg config from ref ${baseRef}: ${e.message}`) + continue + } + + if (!previousData) continue + + // Resolve repos targeted by the old config + const previouslyTargetedRepos = new Set() + + // 1. suborgrepos: resolve glob patterns to concrete repo names + if (previousData.suborgrepos && Array.isArray(previousData.suborgrepos)) { + const allRepos = await this.github.paginate('GET /installation/repositories') + for (const repoPattern of previousData.suborgrepos) { + const glob = new Glob(repoPattern) + for (const repo of allRepos) { + if (glob.test(repo.name)) { + previouslyTargetedRepos.add(repo.name) + } + } + } + } + + // 2. suborgteams: resolve via GitHub API (team membership is live state) + if (previousData.suborgteams && Array.isArray(previousData.suborgteams)) { + try { + const teamPromises = previousData.suborgteams.map(teamslug => + this.getReposForTeam(teamslug) + ) + const teamResults = await Promise.all(teamPromises) + for (const repos of teamResults) { + for (const repo of repos) { + previouslyTargetedRepos.add(repo.name) + } + } + } catch (e) { + this.log.debug(`Error resolving previous suborgteams: ${e.message}`) + } + } + + // 3. suborgproperties: resolve via GitHub API (property values are live state) + if (previousData.suborgproperties && Array.isArray(previousData.suborgproperties)) { + try { + const subOrgRepositories = await this.getSubOrgRepositories(previousData.suborgproperties) + for (const repo of subOrgRepositories) { + previouslyTargetedRepos.add(repo.repository_name) + } + } catch (e) { + this.log.debug(`Error resolving previous suborgproperties: ${e.message}`) + } + } + + // Find repos in previous targeting that are NOT in current targeting + for (const repoName of previouslyTargetedRepos) { + if (!this.getSubOrgConfig(repoName)) { + removedRepos.push(repoName) + } + } + + // Collect plugin sections from previous config that need cleanup + // (these are sections that were applied by the suborg and need to be + // synced with empty config so existing entries are removed) + if (!previousPluginSections) { + previousPluginSections = new Set() + } + for (const key of Object.keys(previousData)) { + if (key in Settings.PLUGINS) { + previousPluginSections.add(key) + } + } + } + + return { + repos: [...new Set(removedRepos)], + previousPluginSections: previousPluginSections ? [...previousPluginSections] : [] + } + } + + // Load a YAML file from a specific git ref, bypassing the file cache. + // Used to load previous versions of config files for comparison. + async loadYamlFromRef (filePath, ref) { + const repo = { owner: this.repo.owner, repo: env.ADMIN_REPO } + const params = Object.assign(repo, { path: filePath, ref }) + + const response = await this.github.rest.repos.getContent(params) + + if (Array.isArray(response.data)) { + return null + } + + if (typeof response.data.content !== 'string') { + return null + } + + return yaml.load(Buffer.from(response.data.content, 'base64').toString()) || {} } // Create a check in the Admin repo for safe-settings. @@ -115,12 +1023,12 @@ class Settings { } // Use the latest commit to create the check against - return this.github.repos.listCommits({ + return this.github.rest.repos.listCommits({ owner: this.repo.owner, repo: env.ADMIN_REPO }) .then(commits => { - return this.github.checks.create( + return this.github.rest.checks.create( { owner: this.repo.owner, repo: env.ADMIN_REPO, @@ -156,6 +1064,28 @@ class Settings { msg, plugin: this.constructor.name }) + // In NOP mode, also surface the error as an ERROR NopCommand so the NOP + // check run conclusion reflects the failure. Without this, errors caught + // by the syncAll/syncSelectedRepos top-level catch (e.g. invalid + // disable_plugins entries) would go unnoticed by PR reviewers. + if (this.nop) { + const nopcommand = new NopCommand(this.constructor.name, this.repo, null, msg, 'ERROR') + this.appendToResults([nopcommand]) + } + } + + // Abort the sync if any validator script failed to compile. Called at the top + // of every sync flow (inside the try) so the thrown error is caught by the + // static entry point and routed through handleResults, which marks the check + // run as failed. Throwing from the constructor is not an option (it runs + // outside the try/catch), so the failure is deferred to here. + checkValidatorsCompiled () { + if (this.validatorCompileErrors && this.validatorCompileErrors.length > 0) { + for (const msg of this.validatorCompileErrors) { + this.logError(msg) + } + throw new Error(`Aborting sync: ${this.validatorCompileErrors.length} validator script(s) failed to compile`) + } } async handleResults () { @@ -168,104 +1098,230 @@ class Settings { return } - // remove duplicate rows in this.results + // Remove duplicate rows. The key includes endpoint + action.msg so that: + // - per-operation NopCommands (individual add/update/remove from diffable + // plugins) survive alongside the overall diff-summary NopCommand, and + // - distinct disable_plugins skip messages (each with a unique msg but + // the same empty endpoint) are all retained. this.results = this.results.filter((thing, index, self) => { return index === self.findIndex((t) => { - return t.type === thing.type && t.repo === thing.repo && t.plugin === thing.plugin + return t.type === thing.type && t.repo === thing.repo && t.plugin === thing.plugin && t.endpoint === thing.endpoint && t.action?.msg === thing.action?.msg }) }) + // When a base-branch config is available (NOP / dry-run on a PR), filter + // out results that reflect pre-existing drift rather than changes the PR + // actually introduces. + if (this.baseConfig) { + this.log.debug('Filtering NOP results using base config comparison') + this.results = this.results.filter(res => { + if (!res || res.type === 'ERROR' || res.type === 'WARNING') return true + + if (res.type === 'INFO' && res.action?.msg && res.action?.additions === null && res.action?.deletions === null && res.action?.modifications === null) { + return true + } + + const isOrgLevel = res.repo && res.repo.endsWith('(org)') + const pluginSection = res.plugin ? res.plugin.toLowerCase() : null + + if (isOrgLevel && pluginSection === 'rulesets') { + // Org-level rulesets: keep only rulesets whose definition changed. + const changedNames = getChangedEntryNames(this.baseConfig.rulesets, this.config.rulesets) + if (changedNames.size === 0) return false + const filtered = filterActionByChangedNames(res.action, changedNames) + if (!filtered) return false + res.action = filtered + return true + } + + if (!isOrgLevel && pluginSection) { + // Keep results for repos whose override/suborg config files changed. + if (this.changedRepoNames && this.changedRepoNames.has(res.repo)) { + return true + } + + // Repo-level rulesets originate from override files, not the global + // config — when no override changed for this repo it is drift. + if (pluginSection === 'rulesets') { + return false + } + + // Other repo-level plugins: drop when the global config section for + // this plugin is unchanged between base and PR. + const baseSection = this.baseConfig[pluginSection] + const prSection = this.config[pluginSection] + if (baseSection !== undefined && prSection !== undefined) { + if (JSON.stringify(baseSection) === JSON.stringify(prSection)) { + return false + } + } + } + + return true + }) + } + + // Full-sync NOP runs do not have the webhook fields needed to report to a + // check run. Keep potentially sensitive diff values at debug level and log + // only a value-free summary at info level. + if (!payload?.check_run || !payload?.repository) { + this.log.debug({ results: this.results }, 'Dry-run results') + const summary = this.results + .map(res => `${res.type} ${res.plugin} ${res.repo}: ${res.action?.msg ?? ''}`) + .join('\n') + this.log.info(`Dry-run finished with ${this.results.length} planned change(s); the full diff is logged at debug level.\n${summary}`) + return + } + let error = false - // Different logic const stats = { - // noOfReposProcessed: new Map(), reposProcessed: {}, changes: {}, - errors: {} - } - /* - Result fields - res.type - res.plugin - res.repo - res.endpoint - res.body - res.action - */ + errors: {}, + // Non-fatal entries (type === 'WARNING'), e.g. an external_group that + // doesn't exist yet because SCIM provisioning hasn't completed. Keyed + // by repo. Unlike errors, these do not flip the check_run conclusion. + warnings: {}, + // Informational entries (type === 'INFO', all-null diff fields), e.g. + // disable_plugins skip messages. Keyed by repo. + infos: {} + } this.results.forEach(res => { if (res) { stats.reposProcessed[res.repo] = true - // if (res.action.additions === null && res.action.deletions === null && res.action.modifications === null) { - // // No changes - // } else if (res.type === 'ERROR') { error = true if (!stats.errors[res.repo]) { stats.errors[res.repo] = [] } - stats.errors[res.repo].push(res.action) - } else if (!(res.action?.additions === null && res.action?.deletions === null && res.action?.modifications === null)) { + const msg = res.action && (res.action.msg || res.action.message) + ? (res.action.msg || res.action.message) + : `${res.action}` + stats.errors[res.repo].push({ msg }) + } else if (res.type === 'WARNING') { + if (!stats.warnings[res.repo]) { + stats.warnings[res.repo] = [] + } + const msg = res.action && (res.action.msg || res.action.message) + ? (res.action.msg || res.action.message) + : `${res.action}` + stats.warnings[res.repo].push({ msg }) + } else if (res.action?.additions === null && res.action?.deletions === null && res.action?.modifications === null) { + // No diff data — informational message (e.g. disable_plugins skip). + if (res.action?.msg) { + if (!stats.infos[res.repo]) { + stats.infos[res.repo] = [] + } + stats.infos[res.repo].push(`[${res.plugin}] ${res.action.msg}`) + } + } else if (!isEmptyChange(res.action)) { if (!stats.changes[res.plugin]) { stats.changes[res.plugin] = {} } - if (!stats.changes[res.plugin][res.repo]) { - stats.changes[res.plugin][res.repo] = [] + // Group by the result's subject (defaults to the repo). Plugins that + // act on a non-repo entity — e.g. app_installations, whose subject is + // a GitHub App — group under that subject instead of the (org) repo. + const subject = res.subject || res.repo + if (!stats.changes[res.plugin][subject]) { + stats.changes[res.plugin][subject] = [] } - stats.changes[res.plugin][res.repo].push(`${res.action}`) + stats.changes[res.plugin][subject].push(res.action) } } }) this.log.debug(`Stats ${JSON.stringify(this.results, null, 2)}`) - const table = ` - - - - - - - - - - - - ` + stats.changeSections = buildChangeSections(stats.changes, this.baseConfig, this.config) + stats.reposAffected = affectedRepoCount(stats.changeSections) + stats.changeDetails = stats.changeSections.length > 0 + ? renderChangeSections(stats.changeSections).join('\n\n') + : '' + stats.checkRunDetails = stats.changeDetails.length > 50000 + ? 'Detailed changed-field output is available in the pull request comment.' + : stats.changeDetails const renderedCommentMessage = await eta.renderString(commetMessageTemplate, stats) if (env.CREATE_PR_COMMENT === 'true') { - const summary = ` -#### :robot: Safe-Settings config changes detected: - -${this.results.reduce((x, y) => { - if (!y) { - return x + const pluginSectionList = renderChangeSections(stats.changeSections) + + const errorRepos = Object.keys(stats.errors) + const errorSection = errorRepos.length === 0 + ? '### Errors\n`None`' + : `### Errors\n
\n:warning: Errors — ${errorRepos.length} ${pluralize(errorRepos.length, 'repo', 'repos')} affected\n\n${ + errorRepos.map(repo => + `**${repo}**:\n${stats.errors[repo].map(e => `* ${e.msg}`).join('\n')}` + ).join('\n\n') + }\n\n
` + + const warningRepos = Object.keys(stats.warnings) + const warningSection = warningRepos.length === 0 + ? '' + : `### Warnings\n
\n:warning: Warnings — ${warningRepos.length} ${pluralize(warningRepos.length, 'repo', 'repos')} affected\n\n${ + warningRepos.map(repo => + `**${repo}**:\n${stats.warnings[repo].map(w => `* ${w.msg}`).join('\n')}` + ).join('\n\n') + }\n\n
` + + // Preserve disable_plugins informational messages in the PR comment. + const infoRepos = Object.keys(stats.infos) + const infoSection = infoRepos.length === 0 + ? '' + : `### Informational messages\n
\n:information_source: Info — ${infoRepos.length} ${pluralize(infoRepos.length, 'repo', 'repos')}\n\n${ + infoRepos.map(repo => + `**${repo}**:\n${stats.infos[repo].map(msg => `* :information_source: ${msg}`).join('\n')}` + ).join('\n\n') + }\n\n
` + + const trailingSections = [errorSection, warningSection, infoSection].filter(Boolean) + const bodySections = stats.changeSections.length === 0 + ? ['_No changes to apply._', ...trailingSections] + : [...pluginSectionList, ...trailingSections] + + const repoCount = Object.keys(stats.reposProcessed).length + const makeHeader = (page, total) => total > 1 + ? `#### :robot: Safe-Settings config changes detected (${page}/${total}):\n\n**Repos considered:** ${repoCount}\n**Repos affected:** ${stats.reposAffected}\n\n` + : `#### :robot: Safe-Settings config changes detected:\n\n**Repos considered:** ${repoCount}\n**Repos affected:** ${stats.reposAffected}\n\n` + + // Reserve room for the largest possible header so pages never overflow + // the comment limit regardless of the final page count. + const headerOverhead = makeHeader(9999, 9999).length + const bodyLimit = COMMENT_LIMIT - headerOverhead + + const pages = [] + let currentChunks = [] + let currentLength = 0 + const flushPage = () => { + if (currentChunks.length > 0) { + pages.push(currentChunks.join('\n\n')) + currentChunks = [] + currentLength = 0 } - if (y.type === 'ERROR') { - error = true - return `${x} -` - } else if (y.action.additions === null && y.action.deletions === null && y.action.modifications === null) { - return `${x}` - } else { - if (y.action === undefined) { - return `${x}` - } - return `${x} -` + } + for (const section of bodySections) { + const sectionLength = section.length + 2 + if (currentChunks.length > 0 && currentLength + sectionLength > bodyLimit) { + flushPage() } - }, table)} -` + currentChunks.push(section) + currentLength += sectionLength + } + flushPage() + if (pages.length === 0) pages.push('') + const totalPages = pages.length const pullRequest = payload.check_run.check_suite.pull_requests[0] - await this.github.issues.createComment({ - owner: payload.repository.owner.login, - repo: payload.repository.name, - issue_number: pullRequest.number, - body: summary.length > 55536 ? `${summary.substring(0, 55536)}... (too many changes to report)` : summary - }) + for (let i = 0; i < pages.length; i++) { + const body = `${makeHeader(i + 1, totalPages)}${pages[i]}` + await this.github.rest.issues.createComment({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: pullRequest.number, + body: truncateWithSuffix(body, COMMENT_LIMIT, '... (too many changes to report)') + }) + } } const params = { @@ -277,12 +1333,12 @@ ${this.results.reduce((x, y) => { completed_at: new Date().toISOString(), output: { title: error ? 'Safe-Settings Dry-Run Finished with Error' : 'Safe-Settings Dry-Run Finished with success', - summary: renderedCommentMessage.length > 55536 ? `${renderedCommentMessage.substring(0, 55536)}... (too many changes to report)` : renderedCommentMessage + summary: truncateWithSuffix(renderedCommentMessage, COMMENT_LIMIT, '... (too many changes to report)') } } this.log.debug(`Completing check run ${JSON.stringify(params)}`) - await this.github.checks.update(params) + await this.github.rest.checks.update(params) } async loadConfigs (repo) { @@ -290,18 +1346,681 @@ ${this.results.reduce((x, y) => { this.repoConfigs = await this.getRepoConfigs(repo) } + // ──────────────────────────────────────────────────────────────────────── + // disable_plugins helpers + // ──────────────────────────────────────────────────────────────────────── + + // Returns the set of plugin names that are valid `disable_plugins` targets. + static getValidDisablePluginNames () { + return new Set([...Object.keys(Settings.PLUGINS), 'repository', 'archive']) + } + + // Normalize a raw `disable_plugins` list (mixed strings / objects) into + // [{ plugin, target, declaredAt }]. Validates plugin names and target + // values; throws on invalid entries. For declaredAt='repo', `children` + // collapses to `all` (repo has no children). + normalizeDisableEntries (rawList, declaredAt) { + if (rawList === undefined || rawList === null) return [] + if (!Array.isArray(rawList)) { + throw new Error(`disable_plugins at ${declaredAt} must be an array; got ${typeof rawList}`) + } + if (!DISABLE_LEVELS.includes(declaredAt)) { + throw new Error(`Internal: invalid declaredAt '${declaredAt}'`) + } + const validPlugins = Settings.getValidDisablePluginNames() + const normalized = [] + for (const raw of rawList) { + let plugin + let target = 'all' + if (typeof raw === 'string') { + plugin = raw + } else if (raw && typeof raw === 'object') { + plugin = raw.plugin + if (raw.target !== undefined) target = raw.target + } else { + throw new Error(`disable_plugins entry at ${declaredAt} must be a string or {plugin, target}; got ${JSON.stringify(raw)}`) + } + if (!plugin || typeof plugin !== 'string') { + throw new Error(`disable_plugins entry at ${declaredAt} is missing a valid 'plugin' name: ${JSON.stringify(raw)}`) + } + if (!validPlugins.has(plugin)) { + throw new Error(`disable_plugins at ${declaredAt}: unknown plugin '${plugin}'. Valid: ${[...validPlugins].sort().join(', ')}`) + } + if (!DISABLE_TARGETS.has(target)) { + throw new Error(`disable_plugins at ${declaredAt} for plugin '${plugin}': invalid target '${target}'. Valid: ${[...DISABLE_TARGETS].join(', ')}`) + } + if (declaredAt === 'repo' && target === 'children') { + this.log.debug(`disable_plugins: normalizing repo-level target 'children' to 'all' for plugin '${plugin}' (repo has no children)`) + target = 'all' + } + normalized.push({ plugin, target, declaredAt }) + } + return normalized + } + + // Aggregate disable_plugins entries from all four layers (deployment, org, + // suborg matching repoName, repo override for repoName) and expand them via + // the strip matrix into a Map>. If repoName is + // undefined, only deployment + org layers contribute (used by updateOrg). + computeStripMap (repoName) { + const stripMap = new Map() + for (const level of DISABLE_LEVELS) stripMap.set(level, new Set()) + + const layers = [] + // Deployment layer (singleton) + const deploymentRaw = (DeploymentConfig && DeploymentConfig.config && DeploymentConfig.config.disable_plugins) || null + if (deploymentRaw) layers.push(['deployment', deploymentRaw]) + // Org layer + if (this.config && this.config.disable_plugins) { + layers.push(['org', this.config.disable_plugins]) + } + if (repoName !== undefined && repoName !== null) { + const suborg = this.getSubOrgConfig(repoName) + if (suborg && suborg.disable_plugins) { + layers.push(['suborg', suborg.disable_plugins]) + } + const repoOverride = this.getRepoOverrideConfig(repoName) + if (repoOverride && repoOverride.disable_plugins) { + layers.push(['repo', repoOverride.disable_plugins]) + } + } + + for (const [declaredAt, rawList] of layers) { + const entries = this.normalizeDisableEntries(rawList, declaredAt) + for (const { plugin, target } of entries) { + const affected = DISABLE_STRIP_MATRIX[declaredAt][target] || [] + for (const lvl of affected) { + stripMap.get(lvl).add(plugin) + } + } + } + this.log.debug(`disable_plugins stripMap for repo=${repoName || ''}: ${JSON.stringify([...stripMap].map(([k, v]) => [k, [...v]]))}`) + return stripMap + } + + // True if the given plugin appears in ANY layer of the stripMap. Used by + // gates around `repository` / `archive` (and updateOrg's rulesets / + // custom_repository_roles) where the plugin runs per-org or per-repo and + // there's no merge-time pipeline to strip into. + isPluginDisabledAnywhere (stripMap, pluginName) { + if (!stripMap) return false + for (const set of stripMap.values()) { + if (set.has(pluginName)) return true + } + return false + } + + // Returns the declaredAt layer(s) responsible for disabling `pluginName` + // in the given stripMap. Used to build informative NopCommand / log + // messages. Note: stripMap layers are *target* layers, not declaration + // layers — to report the source we re-walk the raw disable_plugins lists. + whoDisabled (pluginName, repoName) { + const sources = [] + const probe = (declaredAt, raw) => { + if (!raw) return + let entries = [] + try { entries = this.normalizeDisableEntries(raw, declaredAt) } catch { return } + for (const e of entries) { + if (e.plugin === pluginName) sources.push(`${declaredAt}(target=${e.target})`) + } + } + probe('deployment', DeploymentConfig && DeploymentConfig.config && DeploymentConfig.config.disable_plugins) + probe('org', this.config && this.config.disable_plugins) + if (repoName !== undefined && repoName !== null) { + const suborg = this.getSubOrgConfig(repoName) + probe('suborg', suborg && suborg.disable_plugins) + const repoOverride = this.getRepoOverrideConfig(repoName) + probe('repo', repoOverride && repoOverride.disable_plugins) + } + return sources + } + + // Apply strips to a `{ deployment, org, suborg, repo }` map of cloned + // configs. Mutates clones in place and returns them. Emits NopCommand + // entries when in nop mode. + applyStrips (stripMap, sources, repoName) { + if (!stripMap) return sources + for (const [level, pluginSet] of stripMap) { + const layer = sources[level] + if (!layer) continue + for (const plugin of pluginSet) { + if (Object.prototype.hasOwnProperty.call(layer, plugin)) { + delete layer[plugin] + this.log.debug(`disable_plugins: stripped '${plugin}' from ${level} layer (repo=${repoName || ''})`) + if (this.nop) { + const declaredBy = this.whoDisabled(plugin, repoName).join(', ') + const nopcommand = new NopCommand('disable_plugins', this.repo, null, `Plugin '${plugin}' stripped from ${level} layer (declared by: ${declaredBy || 'unknown'})`, 'INFO') + this.appendToResults([nopcommand]) + } + } + } + } + return sources + } + + // Emit a NopCommand recording that a per-execution-point plugin + // (rulesets / custom_repository_roles / repository / archive) was skipped + // because it appears in the stripMap. + emitDisableSkip (pluginName, repoName) { + if (!this.nop) return + const declaredBy = this.whoDisabled(pluginName, repoName).join(', ') + const nopcommand = new NopCommand('disable_plugins', this.repo, null, `Plugin '${pluginName}' skipped (declared by: ${declaredBy || 'unknown'})`, 'INFO') + this.appendToResults([nopcommand]) + } + async updateOrg () { - const rulesetsConfig = this.config.rulesets + // Org-execution stripMap: no repo context, so only deployment + org + // disable_plugins contribute. + const stripMap = this.computeStripMap() + const additiveSet = this.normalizeAdditivePlugins() + + const rulesetsConfig = applyCentralizedBypassActors(this.config.rulesets, this.config.centralized_ruleset_bypass_actors) if (rulesetsConfig) { - const RulesetsPlugin = Settings.PLUGINS.rulesets - return new RulesetsPlugin(this.nop, this.github, this.repo, rulesetsConfig, this.log, this.errors, SCOPE.ORG).sync().then(res => { - this.appendToResults(res) + if (this.isPluginDisabledAnywhere(stripMap, 'rulesets')) { + this.log.debug("disable_plugins: skipping org-level 'rulesets' plugin") + this.emitDisableSkip('rulesets') + } else { + const RulesetsPlugin = Settings.PLUGINS.rulesets + const rulesetsPlugin = new RulesetsPlugin(this.nop, this.github, this.repo, rulesetsConfig, this.log, this.errors, SCOPE.ORG) + rulesetsPlugin.additive = additiveSet.has('rulesets') + await rulesetsPlugin.sync().then(res => { + if (this.nop && Array.isArray(res)) { + res.forEach(r => { if (r) r.repo = `${this.repo.owner} (org)` }) + } + this.appendToResults(res) + }) + } + } + + const customRepositoryRolesConfig = this.config.custom_repository_roles + if (customRepositoryRolesConfig) { + if (this.isPluginDisabledAnywhere(stripMap, 'custom_repository_roles')) { + this.log.debug("disable_plugins: skipping org-level 'custom_repository_roles' plugin") + this.emitDisableSkip('custom_repository_roles') + } else { + const CustomRepositoryRolesPlugin = Settings.PLUGINS.custom_repository_roles + const customRepositoryRolesPlugin = new CustomRepositoryRolesPlugin(this.nop, this.github, this.repo, customRepositoryRolesConfig, this.log, this.errors) + customRepositoryRolesPlugin.additive = additiveSet.has('custom_repository_roles') + await customRepositoryRolesPlugin.sync().then(res => { + this.appendToResults(res) + }) + } + } + } + + /** + * Sync app installations as a separate phase. + * In full sync mode, computes desired state for all managed apps across all + * config layers and reconciles against live API state. + * In delta mode, processes only the apps affected by changed config files. + * + * @param {object} [options] + * @param {object} [options.appGithub] - App-authenticated Octokit (for enterprise API) + * @param {string} [options.enterpriseSlug] - Enterprise slug from payload + * @param {Array} [options.appChanges] - Pre-computed per-app changes (delta mode); takes precedence over changedSubOrgs/changedRepos + * @param {Array} [options.changedSubOrgs] - Changed suborg config descriptors ({ repo|name, path }) to diff (delta mode) + * @param {Array} [options.changedRepos] - Changed repo config descriptors ({ owner, repo }) to diff (delta mode) + * @param {string} [options.baseRef] - Base git ref used to load the previous config versions when diffing (delta mode) + */ + async syncAppInstallations (options = {}) { + const { appGithub, enterpriseSlug, appChanges, changedSubOrgs, changedRepos, baseRef } = options + + const appInstallationsConfig = this.config.app_installations + // Check if any layer has app_installations config (org, suborg, or repo) + const hasOrgConfig = appInstallationsConfig && Array.isArray(appInstallationsConfig) && appInstallationsConfig.length > 0 + const hasChangedConfigs = (changedSubOrgs && changedSubOrgs.length > 0) || (changedRepos && changedRepos.length > 0) + const hasPrecomputedChanges = appChanges && appChanges.length > 0 + + // In full-sync mode (no delta inputs) app_installations may be defined only + // at the repo or suborg layer, with nothing at the org level. Detect those + // so the plugin still runs when org settings.yml has no app_installations. + const hasLayeredConfig = !hasChangedConfigs && !hasPrecomputedChanges && this._hasLayeredAppInstallations() + + if (!hasOrgConfig && !hasChangedConfigs && !hasPrecomputedChanges && !hasLayeredConfig) { + this.log.debug('No app_installations config found, skipping') + return + } + + // Check disable_plugins + const stripMap = this.computeStripMap() + if (this.isPluginDisabledAnywhere(stripMap, 'app_installations')) { + this.log.debug("disable_plugins: skipping 'app_installations' plugin") + this.emitDisableSkip('app_installations') + return + } + + if (!enterpriseSlug) { + const msg = 'Cannot sync app installations: enterprise slug not available in context (webhook payload missing enterprise info and no fallback configured).' + this.errors.push({ owner: this.repo.owner, repo: this.repo.repo, msg, plugin: 'app_installations' }) + this.log.error(msg) + if (this.nop) { + this.appendToResults([new NopCommand('app_installations', this.repo, null, msg, 'ERROR')]) + } + return + } + + if (!appGithub) { + const msg = `Cannot sync app installations: enterprise-authenticated client not available for '${enterpriseSlug}'. Ensure safe-settings is installed on the enterprise with 'Enterprise organization installations' permission.` + this.errors.push({ owner: this.repo.owner, repo: this.repo.repo, msg, plugin: 'app_installations' }) + this.log.error(msg) + if (this.nop) this.appendToResults([new NopCommand('app_installations', this.repo, null, msg, 'ERROR')]) + return + } + const additiveSet = this.normalizeAdditivePlugins() + const plugin = new AppInstallations( + this.nop, + this.github, + appGithub, + this.repo, + enterpriseSlug, + this.log, + this.errors + ) + plugin.additive = additiveSet.has('app_installations') + + let results + if (appChanges && appChanges.length > 0) { + // Pre-computed delta mode + results = await plugin.syncDelta(appChanges) + } else if (hasChangedConfigs) { + // Delta mode: build app changes from changed suborg/repo configs + const deltaChanges = await this._buildAppChangesFromDelta(appGithub, enterpriseSlug, changedSubOrgs, changedRepos, baseRef) + if (deltaChanges.length > 0) { + results = await plugin.syncDelta(deltaChanges) + } else { + results = [] + } + } else { + // Full sync mode: compute desired state from all config layers + const desiredState = await this._computeFullAppDesiredState(appInstallationsConfig, appGithub, enterpriseSlug) + results = await plugin.syncFull(desiredState) + } + + if (this.nop && Array.isArray(results)) { + results.forEach(r => { if (r) r.repo = `${this.repo.owner} (org)` }) + } + this.appendToResults(results) + } + + /** + * Detect app_installations defined at the repo or suborg layer (used in + * full-sync mode where org settings.yml may have no app_installations of its + * own but repo/suborg configs still declare apps to manage). + * @private + */ + _hasLayeredAppInstallations () { + const hasInMap = (map) => { + if (!map) return false + for (const cfg of Object.values(map)) { + if (cfg && Array.isArray(cfg.app_installations) && cfg.app_installations.length > 0) return true + } + return false + } + return hasInMap(this.repoConfigs) || hasInMap(this.subOrgConfigs) + } + + /** + * Report a configured `app_installations` app_slug that is not installed on + * the org (typically a typo, or an app that has not been installed yet). + * Surfaced as an ERROR so the PR check run / sync fails visibly instead of + * silently skipping the entry. + * @private + */ + _reportUnknownApp (slug, layer) { + const where = layer ? ` (${layer})` : '' + const msg = `app_installations: app '${slug}'${where} is not installed on org '${this.repo.owner}'. Check the app_slug for typos and ensure the GitHub App is installed. Skipping this app.` + this.log.error(msg) + this.errors.push({ owner: this.repo.owner, repo: this.repo.repo, msg, plugin: 'app_installations' }) + if (this.nop) { + this.appendToResults([new NopCommand('app_installations', this.repo, null, msg, 'ERROR', { name: slug, type: 'app' })]) + } + } + + /** + * Build delta-based app changes from changed suborg/repo config files. + * Loads both current and previous (baseRef) versions of each changed config, + * diffs the app_installations sections, and computes repository_selection + * (repos to add) and repository_unselection (repos to remove) per app. + * @private + */ + async _buildAppChangesFromDelta (appGithub, enterpriseSlug, changedSubOrgs = [], changedRepos = [], baseRef) { + const AppOctokitClient = require('./appOctokitClient') + const repoSelector = new RepoSelector(this.github, this.repo.owner, this.log) + const appChangeMap = new Map() // app_slug → { installation_id, repository_selection, repository_unselection } + + // Get installation map (app_slug → installation_id) + const installationMap = new Map() + if (appGithub && enterpriseSlug) { + try { + const enterpriseClient = new AppOctokitClient({ github: appGithub, enterpriseSlug, log: this.log }) + const orgInstallations = await enterpriseClient.listOrgInstallations(this.repo.owner) + for (const inst of orgInstallations) { + installationMap.set(inst.app_slug, inst.id) + } + } catch (e) { + const msg = `Failed to list org installations for delta: ${e.message}` + this.log.error(msg) + this.errors.push({ owner: this.repo.owner, repo: this.repo.repo, msg, plugin: 'app_installations' }) + if (this.nop) this.appendToResults([new NopCommand('app_installations', this.repo, null, msg, 'ERROR')]) + return [] + } + } + + // Apps configured as "all" at the org level take precedence — they must + // never have repos unselected by suborg/repo deltas, and adding repos is + // redundant since the app already targets all repos. + const orgAllApps = new Set() + const orgAppInstallations = this.config && this.config.app_installations + if (Array.isArray(orgAppInstallations)) { + for (const appConfig of orgAppInstallations) { + // Any org-level app_installations entry always implies 'all'. + if (appConfig && appConfig.app_slug) { + orgAllApps.add(appConfig.app_slug) + } + } + } + + // Helper to ensure an entry exists in the change map + const ensureEntry = (slug) => { + // Org-level "all" apps are fully managed by full sync; deltas must not + // add or remove repos for them (org "all" takes precedence). + if (orgAllApps.has(slug)) return null + if (!appChangeMap.has(slug)) { + const installationId = installationMap.get(slug) + if (!installationId) { + this._reportUnknownApp(slug, 'suborg/repo') + return null + } + appChangeMap.set(slug, { + app_slug: slug, + installation_id: installationId, + repository_selection: new Set(), + repository_unselection: new Set() + }) + } + return appChangeMap.get(slug) + } + + // Helper to resolve repos for a suborg config's targeting criteria + const resolveSuborgRepos = async (config) => { + if (!config) return new Set() + const criteria = {} + if (config.suborgrepos) criteria.names = config.suborgrepos + if (config.suborgteams) criteria.teams = config.suborgteams + if (config.suborgproperties) criteria.custom_properties = config.suborgproperties + try { + return await repoSelector.resolve(criteria) + } catch (e) { + this.log.debug(`Error resolving suborg repos: ${e.message}`) + return new Set() + } + } + + // Process changed suborg configs + for (const suborg of changedSubOrgs) { + // Resolve the CURRENT suborg config. this.subOrgConfigs is unreliable in + // the delta path: getSubOrgConfigs keys it by the suborg file name (with + // extension) and by each targeted repo — never by the bare suborg name + // (suborg.repo) — and syncSelectedRepos filters it down to a single + // suborg's targeted repos. When the lookup misses, load the config + // authoritatively from this.ref (mirroring the previous-version load) so + // apps are not misclassified as removed and emit incorrect unselections. + let currentConfig = this.subOrgConfigs && this.subOrgConfigs[suborg.repo] + if (!currentConfig && suborg.path) { + try { + currentConfig = await this.loadYamlFromRef(suborg.path, this.ref) + } catch (e) { + this.log.debug(`Could not load current suborg config for '${suborg.repo || suborg.name}': ${e.message}`) + } + } + const currentApps = (currentConfig && currentConfig.app_installations) || [] + const currentAppSlugs = new Set(currentApps.map(a => a.app_slug).filter(Boolean)) + + // Load previous version of this suborg config + let previousConfig = null + let previousApps = [] + if (baseRef && suborg.path) { + try { + previousConfig = await this.loadYamlFromRef(suborg.path, baseRef) + previousApps = (previousConfig && previousConfig.app_installations) || [] + } catch (e) { + this.log.debug(`Could not load previous suborg config for '${suborg.repo || suborg.name}': ${e.message}`) + } + } + const previousAppSlugs = new Set(previousApps.map(a => a.app_slug).filter(Boolean)) + + // Resolve repos for current and previous targeting criteria + const currentRepos = await resolveSuborgRepos(currentConfig) + const previousRepos = await resolveSuborgRepos(previousConfig) + + // App newly added to this suborg: select all currently targeted repos + for (const slug of currentAppSlugs) { + if (previousAppSlugs.has(slug)) continue + const entry = ensureEntry(slug) + if (!entry) continue + for (const repo of currentRepos) { + entry.repository_selection.add(repo) + } + } + + // App removed from this suborg: unselect all previously targeted repos + for (const slug of previousAppSlugs) { + if (currentAppSlugs.has(slug)) continue + const entry = ensureEntry(slug) + if (!entry) continue + for (const repo of previousRepos) { + entry.repository_unselection.add(repo) + } + } + + // App present in both: only act on the targeting diff. If the targeting + // is unchanged, skip entirely to avoid redundant churn. + const addedRepos = [...currentRepos].filter(r => !previousRepos.has(r)) + const removedRepos = [...previousRepos].filter(r => !currentRepos.has(r)) + if (addedRepos.length > 0 || removedRepos.length > 0) { + for (const slug of currentAppSlugs) { + if (!previousAppSlugs.has(slug)) continue // handled as "newly added" above + const entry = ensureEntry(slug) + if (!entry) continue + for (const repo of addedRepos) entry.repository_selection.add(repo) + for (const repo of removedRepos) entry.repository_unselection.add(repo) + } + } + } + + // Process changed repo configs + for (const repo of changedRepos) { + const repoFilePath = path.posix.join(CONFIG_PATH, 'repos', `${repo.repo}.yml`) + + // Resolve the CURRENT repo config. During syncSelectedRepos this.repoConfigs + // is loaded one repo at a time and typically retains only the last + // processed repo, so it cannot be relied on for every changed repo. When + // the entry is missing, load it authoritatively from this.ref — otherwise + // other changed repos would look empty and be treated as "app removed", + // emitting incorrect unselections. + let repoConfig = this.repoConfigs && + (this.repoConfigs[`${repo.repo}.yml`] || this.repoConfigs[`${repo.repo}.yaml`]) + if (!repoConfig) { + try { + repoConfig = await this.loadYamlFromRef(repoFilePath, this.ref) + } catch (e) { + this.log.debug(`Could not load current repo config for '${repo.repo}': ${e.message}`) + } + } + const currentApps = (repoConfig && repoConfig.app_installations) || [] + const currentAppSlugs = new Set(currentApps.map(a => a.app_slug).filter(Boolean)) + + // Load previous version of this repo config + let previousApps = [] + if (baseRef) { + try { + const previousData = await this.loadYamlFromRef(repoFilePath, baseRef) + previousApps = (previousData && previousData.app_installations) || [] + } catch (e) { + this.log.debug(`Could not load previous repo config for '${repo.repo}': ${e.message}`) + } + } + const previousAppSlugs = new Set(previousApps.map(a => a.app_slug).filter(Boolean)) + + // App newly added to this repo config: select this repo. If the app was + // already present in the previous version, its selection is unchanged — + // skip to avoid redundant churn. + for (const slug of currentAppSlugs) { + if (previousAppSlugs.has(slug)) continue + const entry = ensureEntry(slug) + if (!entry) continue + entry.repository_selection.add(repo.repo) + } + + // App removed from this repo config: unselect this repo + for (const slug of previousAppSlugs) { + if (currentAppSlugs.has(slug)) continue + const entry = ensureEntry(slug) + if (!entry) continue + entry.repository_unselection.add(repo.repo) + } + } + + // Convert Sets to arrays and remove repos that appear in both selection and unselection + // (selection wins — if a repo is being added by one config and removed by another, keep it) + const results = [] + for (const change of appChangeMap.values()) { + for (const repo of change.repository_selection) { + change.repository_unselection.delete(repo) + } + results.push({ + ...change, + repository_selection: [...change.repository_selection], + repository_unselection: [...change.repository_unselection] }) } + return results + } + + /** + * Compute the full desired state for all managed apps by merging + * org + suborg + repo level app_installations configs. + * Used only in full sync mode (cron/manual). + * @private + */ + async _computeFullAppDesiredState (orgAppInstallations, appGithub, enterpriseSlug) { + const AppOctokitClient = require('./appOctokitClient') + const desiredState = {} + const repoSelector = new RepoSelector(this.github, this.repo.owner, this.log) + // Org-level app_installations may be absent entirely (apps declared only at + // the repo/suborg layer); normalise so the org loop below is safe. + if (!Array.isArray(orgAppInstallations)) orgAppInstallations = [] + + // Get all org installations to map app_slug → installation_id + let orgInstallations = [] + if (appGithub && enterpriseSlug) { + const enterpriseClient = new AppOctokitClient({ github: appGithub, enterpriseSlug, log: this.log }) + try { + orgInstallations = await enterpriseClient.listOrgInstallations(this.repo.owner) + } catch (e) { + const msg = `Failed to list org installations: ${e.message}` + this.log.error(msg) + this.errors.push({ owner: this.repo.owner, repo: this.repo.repo, msg, plugin: 'app_installations' }) + if (this.nop) this.appendToResults([new NopCommand('app_installations', this.repo, null, msg, 'ERROR')]) + return desiredState + } + } + + const installationMap = new Map() + const selectionMap = new Map() + for (const inst of orgInstallations) { + installationMap.set(inst.app_slug, inst.id) + selectionMap.set(inst.app_slug, inst.repository_selection) + } + + // Process org-level config. An org-level app_installations entry always + // implies access to ALL repos in the org (there is no per-repo selection + // at this layer). + for (const appConfig of orgAppInstallations) { + const slug = appConfig.app_slug + if (!slug) continue + + const installationId = installationMap.get(slug) + if (!installationId) { + this._reportUnknownApp(slug, 'org settings.yml') + continue + } + + desiredState[slug] = { installation_id: installationId, repos: 'all' } + } + + // Overlay suborg-level configs + if (this.subOrgConfigs) { + for (const [pattern, subOrgConfig] of Object.entries(this.subOrgConfigs)) { + if (!subOrgConfig || !subOrgConfig.app_installations) continue + + // Resolve repos for this suborg + const criteria = {} + if (subOrgConfig.suborgrepos) criteria.names = subOrgConfig.suborgrepos + if (subOrgConfig.suborgteams) criteria.teams = subOrgConfig.suborgteams + if (subOrgConfig.suborgproperties) criteria.custom_properties = subOrgConfig.suborgproperties + + let suborgRepos = new Set() + try { + suborgRepos = await repoSelector.resolve(criteria) + } catch (e) { + this.log.debug(`Error resolving suborg repos for pattern '${pattern}': ${e.message}`) + } + + for (const appConfig of subOrgConfig.app_installations) { + const slug = appConfig.app_slug + if (!slug) continue + if (!desiredState[slug]) { + const installationId = installationMap.get(slug) + if (!installationId) { this._reportUnknownApp(slug, 'suborg'); continue } + desiredState[slug] = { installation_id: installationId, repos: new Set() } + } + // Org "all" takes precedence — don't add specific repos + if (desiredState[slug].repos === 'all') continue + for (const repo of suborgRepos) { + desiredState[slug].repos.add(repo) + } + } + } + } + + // Overlay repo-level configs + if (this.repoConfigs) { + for (const [repoFileName, repoConfig] of Object.entries(this.repoConfigs)) { + if (!repoConfig || !repoConfig.app_installations) continue + const repoName = repoFileName.replace(/\.ya?ml$/, '') + + for (const appConfig of repoConfig.app_installations) { + const slug = appConfig.app_slug + if (!slug) continue + if (!desiredState[slug]) { + const installationId = installationMap.get(slug) + if (!installationId) { this._reportUnknownApp(slug, 'repo'); continue } + desiredState[slug] = { installation_id: installationId, repos: new Set() } + } + if (desiredState[slug].repos === 'all') continue + desiredState[slug].repos.add(repoName) + } + } + } + + // Attach each app's current (live) repository_selection so the plugin can + // decide whether to toggle 'all' ↔ 'selected' or add/remove individually. + for (const [slug, entry] of Object.entries(desiredState)) { + entry.current_selection = selectionMap.get(slug) + } + + return desiredState } async updateRepos (repo) { this.subOrgConfigs = this.subOrgConfigs || await this.getSubOrgConfigs() + // Snapshot the set of suborg `source` paths that match this repo *before* + // we apply any changes. We compare against the post-apply set below to + // decide whether to re-evaluate (and to break stable loops). + const preMatchedSuborgSources = this.reevaluateOnChange + ? this.getAllMatchingSubOrgSources(repo.repo) + : null // Keeping this as is instead of doing an object assign as that would cause `Cannot read properties of undefined (reading 'startsWith')` error // Copilot code review would recoommend using object assign but that would cause the error let repoConfig = this.config.repository @@ -334,14 +2053,34 @@ ${this.results.reduce((x, y) => { repoConfig = this.mergeDeep.mergeDeep({}, repoConfig, overrideRepoConfig) } if (repoConfig) { + // Per-repo disable_plugins stripMap (used to gate repository + archive + // plugins, which run per-repo outside the childPluginsList pipeline). + const repoStripMap = this.computeStripMap(repo.repo) + const repositoryDisabled = this.isPluginDisabledAnywhere(repoStripMap, 'repository') + const archiveDisabled = this.isPluginDisabledAnywhere(repoStripMap, 'archive') + + // Track actual change signals from the plugins, used by the suborg + // re-evaluation logic below to avoid an unnecessary live API round-trip + // when nothing relevant actually changed. + const changeSignals = { teamsChanged: false, propertiesChanged: false, renamed: false, created: false } try { this.log.debug(`found a matching repoconfig for this repo ${JSON.stringify(repoConfig)}`) const childPlugins = this.childPluginsList(repo) const RepoPlugin = Settings.PLUGINS.repository - const archivePlugin = new Archive(this.nop, this.github, repo, repoConfig, this.log) - const { shouldArchive, shouldUnarchive } = await archivePlugin.getState() + let archivePlugin = null + let shouldArchive = false + let shouldUnarchive = false + if (archiveDisabled) { + this.log.debug(`disable_plugins: skipping 'archive' plugin for ${repo.repo}`) + this.emitDisableSkip('archive', repo.repo) + } else { + archivePlugin = new Archive(this.nop, this.github, repo, repoConfig, this.log) + const state = await archivePlugin.getState() + shouldArchive = state.shouldArchive + shouldUnarchive = state.shouldUnarchive + } if (shouldUnarchive) { this.log.debug(`Unarchiving repo ${repo.repo}`) @@ -349,16 +2088,35 @@ ${this.results.reduce((x, y) => { this.appendToResults(unArchiveResults) } - const repoResults = await new RepoPlugin(this.nop, this.github, repo, repoConfig, this.installation_id, this.log, this.errors).sync() - this.appendToResults(repoResults) + if (repositoryDisabled) { + this.log.debug(`disable_plugins: skipping 'repository' plugin for ${repo.repo}`) + this.emitDisableSkip('repository', repo.repo) + } else { + const repoPluginInstance = new RepoPlugin(this.nop, this.github, repo, repoConfig, this.installation_id, this.log, this.errors) + const repoResults = await repoPluginInstance.sync() + this.appendToResults(repoResults) + if (repoPluginInstance.renamed) changeSignals.renamed = true + if (repoPluginInstance.created) changeSignals.created = true + } + const additiveSet = this.normalizeAdditivePlugins() + const childPluginInstances = childPlugins.map(([Plugin, config, section]) => { + const instance = new Plugin(this.nop, this.github, repo, config, this.log, this.errors) + instance.additive = additiveSet.has(section) + return [Plugin, instance] + }) const childResults = await Promise.all( - childPlugins.map(([Plugin, config]) => { - return new Plugin(this.nop, this.github, repo, config, this.log, this.errors).sync() - }) + childPluginInstances.map(([, instance]) => instance.sync()) ) this.appendToResults(childResults) + // Collect change signals from relevant child plugins. + for (const [Plugin, instance] of childPluginInstances) { + if (!instance.hasChanges) continue + if (Plugin === Settings.PLUGINS.teams) changeSignals.teamsChanged = true + if (Plugin === Settings.PLUGINS.custom_properties) changeSignals.propertiesChanged = true + } + if (shouldArchive) { this.log.debug(`Archiving repo ${repo.repo}`) const archiveResults = await archivePlugin.sync() @@ -374,11 +2132,23 @@ ${this.results.reduce((x, y) => { throw e } } + + // Suborg re-evaluation: if a repo-yml change actually applied teams or + // custom_properties (or this repo was just renamed/created), the repo + // may newly match or stop matching a suborg config + // (suborgteams/suborgproperties/suborgrepos). Refresh the suborg cache, + // compare matched-source sets; if the set changed, re-run updateRepos + // once for this repo. Bounded by + // MAX_REEVALUATION_DEPTH and a stable-set check to prevent loops. + await this.maybeReevaluateSuborg(repo, repoConfig, preMatchedSuborgSources, changeSignals) } else { this.log.debug(`Didnt find any a matching repoconfig for this repo ${JSON.stringify(repo)} in ${JSON.stringify(this.repoConfigs)}`) const childPlugins = this.childPluginsList(repo) - return Promise.all(childPlugins.map(([Plugin, config]) => { - return new Plugin(this.nop, this.github, repo, config, this.log, this.errors).sync().then(res => { + const additiveSet = this.normalizeAdditivePlugins() + return Promise.all(childPlugins.map(([Plugin, config, section]) => { + const instance = new Plugin(this.nop, this.github, repo, config, this.log, this.errors) + instance.additive = additiveSet.has(section) + return instance.sync().then(res => { this.appendToResults(res) }) })) @@ -393,6 +2163,18 @@ ${this.results.reduce((x, y) => { }) } + async updateChangedRepoConfigs (changedRepos = []) { + if (!Array.isArray(changedRepos) || changedRepos.length === 0) return + + const seen = new Set() + for (const repo of changedRepos) { + if (!repo || !repo.repo || seen.has(repo.repo)) continue + seen.add(repo.repo) + if (this.processedRepoNames.has(repo.repo)) continue + await this.checkAndProcessRepo(repo.owner || this.repo.owner, repo.repo) + } + } + getSubOrgConfig (repoName) { if (this.subOrgConfigs) { for (const pattern of Object.keys(this.subOrgConfigs)) { @@ -405,19 +2187,204 @@ ${this.results.reduce((x, y) => { return undefined } + // Read-only helper used for suborg re-evaluation stability checks. + // Returns the set of suborg `source` paths (i.e. the suborg config file path) + // that match the given repo name. Apply-time behavior is unchanged: + // `getSubOrgConfig` still returns the first match and + // `storeSubOrgConfigIfNoConflicts` still forbids multi-suborg overlap at + // config-load time -- so this set normally contains 0 or 1 entries. We + // expose it as a Set so callers can detect the transition from {} -> {pathA} + // when a repo newly matches a suborg after teams/properties are applied. + getAllMatchingSubOrgSources (repoName) { + const sources = new Set() + if (!this.subOrgConfigs) { + return sources + } + for (const pattern of Object.keys(this.subOrgConfigs)) { + const glob = new Glob(pattern) + if (glob.test(repoName)) { + const source = this.subOrgConfigs[pattern]?.source + if (source) { + sources.add(source) + } + } + } + return sources + } + + // Force a refresh of the cached suborg configs. Used by the re-eval loop + // because suborgteams / suborgproperties resolution calls live GitHub APIs + // and may now match the repo after teams/properties were applied in the + // first pass. + async reloadSubOrgConfigs () { + this.subOrgConfigs = await this.getSubOrgConfigs() + } + + // Decide whether applying this repo's config actually changed state that + // could affect suborg matching. If no relevant change happened, skip the + // re-eval API roundtrip entirely. + // + // Preferred path: use plugin-emitted change signals from the just-completed + // sync (teams plugin actually added/removed/updated, custom_properties + // plugin changed values, repository plugin renamed/created). These come + // from the Diffable base class (`plugin.hasChanges`) and the Repository + // plugin (`renamed`, `created`). + // + // Fallback (changeSignals omitted, e.g. unit tests calling the helper in + // isolation): inspect the per-repo yml top-level shape for teams / + // custom_properties / rename indicators. + shouldConsiderReevaluation (repo, repoConfig, changeSignals) { + if (changeSignals) { + return !!( + changeSignals.teamsChanged || + changeSignals.propertiesChanged || + changeSignals.renamed || + changeSignals.created + ) + } + const repoYml = this.repoConfigs && ( + this.repoConfigs[`${repo.repo}.yml`] || this.repoConfigs[`${repo.repo}.yaml`] + ) + if (repoYml) { + if (Array.isArray(repoYml.teams) && repoYml.teams.length > 0) return true + if (Array.isArray(repoYml.custom_properties) && repoYml.custom_properties.length > 0) return true + } + if (repo && repo.oldname && repo.oldname !== repo.repo) return true + if (repoConfig && repoConfig.oldname && repoConfig.oldname !== repoConfig.name) return true + return false + } + + // After applying changes to a repo, decide whether to re-run updateRepos + // because the applied changes may have changed whether the repo matches a + // suborg config. Loop prevention has two layers: + // 1. Hard cap: MAX_REEVALUATION_DEPTH (=1) re-evaluation passes per repo. + // 2. Stability check: stop if the set of matched suborg sources did not + // grow (no new suborg source appeared since the last pass). + async maybeReevaluateSuborg (repo, repoConfig, preMatchedSuborgSources, changeSignals) { + if (!this.reevaluateOnChange) return + if (!preMatchedSuborgSources) return + if (!this.shouldConsiderReevaluation(repo, repoConfig, changeSignals)) { + this.log.debug(`Suborg re-eval: skipping for ${repo.repo} (no relevant changes from teams/custom_properties/repository plugins)`) + return + } + + const depth = this.reevaluationDepth.get(repo.repo) || 0 + if (depth >= MAX_REEVALUATION_DEPTH) { + this.log.warn(`Suborg re-eval: max depth (${MAX_REEVALUATION_DEPTH}) reached for ${repo.repo}; stopping. Any further suborg matches will be picked up on the next sync.`) + return + } + + // Refresh suborg config cache; suborgteams/suborgproperties resolution + // hits live GitHub APIs and may now match this repo. + await this.reloadSubOrgConfigs() + + const newMatched = this.getAllMatchingSubOrgSources(repo.repo) + + // Stability check: if the source set did not change, we're done. A change + // can be either a newly matched suborg or a removed match after teams or + // custom_properties changed. + let hasChanged = preMatchedSuborgSources.size !== newMatched.size + if (!hasChanged) { + for (const source of newMatched) { + if (!preMatchedSuborgSources.has(source)) { + hasChanged = true + break + } + } + } + if (!hasChanged) { + this.log.debug(`Suborg re-eval: stable for ${repo.repo} (matched sources: ${JSON.stringify(Array.from(newMatched))}); stopping.`) + return + } + + this.reevaluatedRepos.set(repo.repo, new Set([...preMatchedSuborgSources, ...newMatched])) + this.reevaluationDepth.set(repo.repo, depth + 1) + this.log.debug(`Suborg re-eval: suborg sources changed for ${repo.repo} after apply; re-running updateRepos (depth=${depth + 1}).`) + + // Reload repo-level configs for this repo so the next pass picks up any + // state changes; then recurse. Depth cap above prevents infinite loops. + this.repoConfigs = await this.getRepoConfigs(repo) + await this.updateRepos(repo) + } + // Remove Org specific configs from the repo config returnRepoSpecificConfigs (config) { const newConfig = Object.assign({}, config) // clone delete newConfig.rulesets + delete newConfig.custom_repository_roles + delete newConfig.disable_plugins + delete newConfig.additive_plugins return newConfig } + // Shallow-clone a config object and strip metadata keys (`disable_plugins`, + // `additive_plugins`) that are policy controls, not plugin section config. + cloneAndStripDisableMeta (config) { + if (!config) return {} + const clone = Object.assign({}, config) + delete clone.disable_plugins + delete clone.additive_plugins + return clone + } + + // Parse and validate the `additive_plugins` list from the org-level config. + // Returns a Set of plugin names that should run in additive mode + // (remove() calls suppressed). Logs an error for unknown or non-Diffable + // plugin names and excludes them from the returned set. + normalizeAdditivePlugins () { + const raw = (this.config && this.config.additive_plugins) || [] + if (!Array.isArray(raw)) { + this.logError(`additive_plugins must be an array; got ${typeof raw}`) + return new Set() + } + const validPlugins = Settings.ADDITIVE_PLUGINS + const result = new Set() + for (const name of raw) { + if (typeof name !== 'string') { + this.logError(`additive_plugins: each entry must be a string plugin name; got ${JSON.stringify(name)}`) + continue + } + if (!validPlugins.has(name)) { + this.logError(`additive_plugins: unknown or non-Diffable plugin '${name}'. Valid: ${[...validPlugins].sort().join(', ')}`) + continue + } + result.add(name) + } + return result + } + childPluginsList (repo) { const repoName = repo.repo const subOrgOverrideConfig = this.getSubOrgConfig(repoName) this.log.debug(`suborg config for ${repoName} is ${JSON.stringify(subOrgOverrideConfig)}`) const repoOverrideConfig = this.getRepoOverrideConfig(repoName) - const overrideConfig = this.mergeDeep.mergeDeep({}, this.returnRepoSpecificConfigs(this.config), subOrgOverrideConfig, repoOverrideConfig) + + // Build clones of each layer and apply disable_plugins strips before the + // existing mergeDeep pipeline runs. The deployment layer's strips affect + // the OTHER three layers (per the matrix); the deployment config itself + // is not merged into per-repo plugin config today. + const stripMap = this.computeStripMap(repoName) + const sources = { + deployment: this.cloneAndStripDisableMeta((DeploymentConfig && DeploymentConfig.config) || {}), + org: this.returnRepoSpecificConfigs(this.config), + suborg: this.cloneAndStripDisableMeta(subOrgOverrideConfig), + repo: this.cloneAndStripDisableMeta(repoOverrideConfig) + } + this.applyStrips(stripMap, sources, repoName) + + const overrideConfig = this.mergeDeep.mergeDeep({}, sources.org, sources.suborg, sources.repo) + + // When processing repos removed from suborg targeting, inject empty arrays + // for plugin sections that were previously provided by the suborg. This + // ensures those plugins are instantiated and can detect/remove existing + // entries that are no longer desired. + if (this.removedFromSubOrgPluginSections && !subOrgOverrideConfig) { + for (const section of this.removedFromSubOrgPluginSections) { + if (!(section in overrideConfig)) { + overrideConfig[section] = [] + } + } + } this.log.debug(`consolidated config is ${JSON.stringify(overrideConfig)}`) @@ -435,9 +2402,19 @@ ${this.results.reduce((x, y) => { if (section !== 'repositories' && section !== 'repository') { // Ignore any config that is not a plugin if (section in Settings.PLUGINS) { + // app_installations is not a per-repo Diffable plugin; it operates at + // the org level on app installations and is reconciled separately by + // syncAppInstallations(). Skip it here so the per-repo pipeline does + // not try to call the (non-existent) sync() on it. + if (section === 'app_installations') continue this.log.debug(`Found section ${section} in the config. Creating plugin...`) const Plugin = Settings.PLUGINS[section] - childPlugins.push([Plugin, config]) + const pluginConfig = section === 'rulesets' + ? applyCentralizedBypassActors(config, this.config.centralized_ruleset_bypass_actors) + : config + // Include sectionName as 3rd element so callers can thread the + // additive_plugins flag without re-deriving the plugin key. + childPlugins.push([Plugin, pluginConfig, section]) } } } @@ -506,17 +2483,21 @@ ${this.results.reduce((x, y) => { log.debug('Fetching repositories') return github.paginate('GET /installation/repositories').then(repositories => { return Promise.all(repositories.map(repository => { - if (this.isRestricted(repository.name)) { - return null - } - const { owner, name } = repository - return this.updateRepos({ owner: owner.login, repo: name }) + return this.checkAndProcessRepo(owner.login, name) }) ) }) } + async checkAndProcessRepo (owner, name) { + this.processedRepoNames.add(name) + if (this.isRestricted(name)) { + return null + } + return this.updateRepos({ owner, repo: name }) + } + /** * Loads a file from GitHub * @@ -526,7 +2507,7 @@ ${this.results.reduce((x, y) => { async loadConfigMap (params) { try { this.log.debug(` In loadConfigMap ${JSON.stringify(params)}`) - const response = await this.github.repos.getContent(params).catch(e => { + const response = await this.github.rest.repos.getContent(params).catch(e => { this.log.debug(`Error getting settings ${JSON.stringify(params)} ${e}`) }) @@ -580,7 +2561,7 @@ ${this.results.reduce((x, y) => { // get /repos directory sha to use in the getTree api const repo = { owner: this.repo.owner, repo: env.ADMIN_REPO } const params = Object.assign(repo, { path: path.posix.join(CONFIG_PATH), ref: this.ref }) - const githubDirectoryContentResponse = await this.github.repos.getContent(params).catch(e => { + const githubDirectoryContentResponse = await this.github.rest.repos.getContent(params).catch(e => { this.log.debug(`Error getting settings ${JSON.stringify(params)} ${e}`) }) @@ -600,8 +2581,8 @@ ${this.results.reduce((x, y) => { // const endpoint = `/repos/${this.repo.owner}/${repo.repo}/git/trees/${repoDirInfo.sha}` // this.log.debug(`endpoint: ${endpoint}`) const treeParams = Object.assign(repo, { tree_sha: repoDirInfo.sha, recursive: 0 }) - const response = await this.github.git.getTree(treeParams).catch(e => { - this.log.debug(`Error getting settings ${JSON.stringify(this.github.git.getTree.endpoint(treeParams))} ${e}`) + const response = await this.github.rest.git.getTree(treeParams).catch(e => { + this.log.debug(`Error getting settings ${JSON.stringify(this.github.rest.git.getTree.endpoint(treeParams))} ${e}`) }) if (!response || !response.data) { @@ -824,7 +2805,7 @@ ${this.results.reduce((x, y) => { } } - const response = await this.github.repos.getContent(params).catch(e => { + const response = await this.github.rest.repos.getContent(params).catch(e => { if (e.status === 304) { this.log.debug(`Cache hit for file ${filePath}`) return { @@ -952,18 +2933,29 @@ ${this.results.reduce((x, y) => { } } -function prettify (obj) { - if (obj === null || obj === undefined) { - return '' - } - return JSON.stringify(obj, null, 2).replaceAll('\n', '
').replaceAll(' ', ' ') -} - Settings.FILE_NAME = path.posix.join(CONFIG_PATH, env.SETTINGS_FILE_PATH) Settings.FILE_PATH = path.posix.join(CONFIG_PATH, env.SETTINGS_FILE_PATH) Settings.SUB_ORG_PATTERN = new Glob(`${CONFIG_PATH}/suborgs/*.yml`) Settings.REPO_PATTERN = new Glob(`${CONFIG_PATH}/repos/*.yml`) +// Plugin names that support additive_plugins (all extend Diffable and have +// a meaningful remove() concept). Non-Diffable plugins (repository, archive, +// branches, validator) are intentionally excluded — listing them in +// additive_plugins will produce a validation error. +Settings.ADDITIVE_PLUGINS = new Set([ + 'labels', + 'collaborators', + 'teams', + 'milestones', + 'autolinks', + 'environments', + 'custom_properties', + 'variables', + 'rulesets', + 'custom_repository_roles', + 'app_installations' +]) + Settings.PLUGINS = { repository: require('./plugins/repository'), labels: require('./plugins/labels'), @@ -976,7 +2968,13 @@ Settings.PLUGINS = { rulesets: require('./plugins/rulesets'), environments: require('./plugins/environments'), custom_properties: require('./plugins/custom_properties.js'), - variables: require('./plugins/variables') + custom_repository_roles: require('./plugins/custom_repository_roles'), + variables: require('./plugins/variables'), + app_installations: require('./plugins/appInstallations') } module.exports = Settings +module.exports.isEmptyChange = isEmptyChange +module.exports.isDeepEmpty = isDeepEmpty +module.exports.getChangedEntryNames = getChangedEntryNames +module.exports.filterActionByChangedNames = filterActionByChangedNames diff --git a/lib/settingsGenerator.js b/lib/settingsGenerator.js new file mode 100644 index 000000000..e2780d64e --- /dev/null +++ b/lib/settingsGenerator.js @@ -0,0 +1,498 @@ +/* eslint-disable camelcase */ +const yaml = require('js-yaml') +const Settings = require('./settings') +const MergeDeep = require('./mergeDeep') +const env = require('./env') + +/** + * SettingsGenerator + * + * The reverse of safe-settings: read the *current* configuration of a repo / + * org / collection-of-repos from the GitHub API and emit safe-settings YAML + * (`repos/.yml`, `settings.yml`, `suborgs/.yml`). + * + * The heavy lifting (knowing which API to call to read current state) already + * lives in each plugin's `find()` method, so wherever possible we instantiate + * the existing plugin in nop mode with empty entries and reuse its `find()`. + * The raw API shape is then reduced to the configurable subset that the + * safe-settings schema understands. + */ + +// Keys that are pure API noise and should never appear in generated config. +const NOISE_KEYS = new Set([ + 'id', 'node_id', 'url', 'html_url', 'repository_url', 'labels_url', 'events_url', + 'created_at', 'updated_at', 'pushed_at', 'creator', '_links', 'current_user_can_bypass' +]) + +function makeLogger () { + const noop = () => {} + const logger = { debug: noop, info: noop, warn: noop, error: noop, trace: noop } + logger.child = () => logger + return logger +} + +/** + * Recursively strip API-only noise keys from an arbitrary value. + * Used for sections (rulesets, environments) whose API shape is large and + * not worth hand-mapping field by field. + */ +function stripNoise (value) { + if (Array.isArray(value)) { + return value.map(stripNoise) + } + if (value && typeof value === 'object') { + const out = {} + for (const [k, v] of Object.entries(value)) { + if (NOISE_KEYS.has(k)) continue + if (v === null || v === undefined) continue + out[k] = stripNoise(v) + } + return out + } + return value +} + +/** Remove sections whose value is empty (undefined, [], {} ). */ +function pruneEmpty (config) { + const out = {} + for (const [section, value] of Object.entries(config)) { + if (value === undefined || value === null) continue + if (Array.isArray(value) && value.length === 0) continue + if (!Array.isArray(value) && typeof value === 'object' && Object.keys(value).length === 0) continue + out[section] = value + } + return out +} + +class SettingsGenerator { + /** + * @param {object} github An authenticated octokit instance. + * @param {string} owner The org / owner login. + * @param {object} [opts] + * @param {object} [opts.log] Logger; defaults to a silent logger. + */ + constructor (github, owner, opts = {}) { + this.github = github + this.owner = owner + this.log = opts.log || makeLogger() + this.errors = [] + } + + /** + * Instantiate a Diffable plugin in nop mode with empty entries and return + * its `find()` result (the current state read from GitHub). + * + * @param {string} section Plugin/section name (key of Settings.PLUGINS). + * @param {object} repo { owner, repo } + * @param {string} [scope] Optional scope passed to plugins that accept it + * (rulesets uses 'org' | 'repo'). + */ + async findExisting (section, repo, scope) { + const Plugin = Settings.PLUGINS[section] + if (!Plugin) throw new Error(`Unknown plugin section: ${section}`) + const instance = new Plugin(true, this.github, repo, [], this.log, this.errors, scope) + return instance.find() + } + + // --- Section extractors ------------------------------------------------- + + async repository (repo) { + const { data } = await this.github.rest.repos.get(repo) + const fields = [ + 'name', 'description', 'homepage', 'private', 'visibility', + 'has_issues', 'has_projects', 'has_wiki', 'has_downloads', 'is_template', + 'default_branch', 'allow_squash_merge', 'allow_merge_commit', + 'allow_rebase_merge', 'allow_auto_merge', 'delete_branch_on_merge', + 'allow_update_branch', 'squash_merge_commit_title', 'squash_merge_commit_message', + 'merge_commit_title', 'merge_commit_message', 'web_commit_signoff_required', + 'archived' + ] + const out = {} + for (const f of fields) { + if (data[f] !== undefined && data[f] !== null) out[f] = data[f] + } + if (Array.isArray(data.topics) && data.topics.length > 0) out.topics = data.topics + return out + } + + async labels (repo) { + const existing = await this.findExisting('labels', repo) + return (existing || []).map(({ name, color, description }) => ({ + name, + color: color ? String(color) : undefined, + description: description || undefined + })) + } + + async collaborators (repo) { + const existing = await this.findExisting('collaborators', repo) + return (existing || []) + .filter(c => c && c.username) + .map(({ username, permission }) => ({ username, permission })) + } + + async teams (repo) { + const existing = await this.findExisting('teams', repo) + return (existing || []).map(t => ({ + name: t.slug || t.name, + permission: t.permission + })) + } + + async milestones (repo) { + const existing = await this.findExisting('milestones', repo) + return (existing || []).map(({ title, description, state }) => ({ + title, + description: description || undefined, + state: state || undefined + })) + } + + async autolinks (repo) { + const existing = await this.findExisting('autolinks', repo) + return (existing || []).map(({ key_prefix, url_template, is_alphanumeric }) => ({ + key_prefix, + url_template, + is_alphanumeric + })) + } + + async custom_properties (repo) { + const existing = await this.findExisting('custom_properties', repo) + return (existing || []).filter(p => p && p.value !== null && p.value !== undefined) + } + + async variables (repo) { + const existing = await this.findExisting('variables', repo) + return (existing || []).map(({ name, value }) => ({ name, value })) + } + + async environments (repo) { + const existing = await this.findExisting('environments', repo) + return stripNoise(existing || []) + } + + async rulesets (repo, scope = 'repo') { + const existing = await this.findExisting('rulesets', repo, scope) + return (existing || []).map(rs => { + const { source, source_type, ...rest } = stripNoise(rs) + return rest + }) + } + + async custom_repository_roles (repo) { + const existing = await this.findExisting('custom_repository_roles', repo) + return (existing || []).map(({ id, ...rest }) => rest) + } + + async branches (repo) { + let branchList + try { + branchList = await this.github.paginate(this.github.rest.repos.listBranches, { + owner: repo.owner, + repo: repo.repo, + protected: true, + per_page: 100 + }) + } catch (e) { + this.log.debug(`Could not list protected branches for ${repo.repo}: ${e.message}`) + return [] + } + + const result = [] + for (const b of branchList || []) { + try { + const { data } = await this.github.rest.repos.getBranchProtection({ + owner: repo.owner, + repo: repo.repo, + branch: b.name + }) + result.push({ name: b.name, protection: this.reformatBranchProtection(data) }) + } catch (e) { + this.log.debug(`Could not read branch protection for ${repo.repo}#${b.name}: ${e.message}`) + } + } + return result + } + + /** + * Convert the GitHub branch-protection API response into the flatter shape + * used by safe-settings config (boolean toggles instead of `{ enabled }`). + * Mirrors Branches.reformatAndReturnBranchProtection. + */ + reformatBranchProtection (protection) { + if (!protection) return protection + const p = stripNoise(protection) + const flatten = key => { + if (p[key] && typeof p[key] === 'object' && 'enabled' in p[key]) { + p[key] = p[key].enabled + } + } + flatten('required_conversation_resolution') + flatten('allow_deletions') + flatten('required_linear_history') + flatten('enforce_admins') + flatten('required_signatures') + flatten('allow_force_pushes') + flatten('block_creations') + flatten('lock_branch') + return p + } + + // --- Scope builders ----------------------------------------------------- + + /** + * Build the full repo-level config object for a single repository. + * @param {string} repoName + * @returns {Promise} pruned config (empty sections removed) + */ + async buildRepoConfig (repoName) { + const repo = { owner: this.owner, repo: repoName } + const sections = [ + 'repository', 'labels', 'collaborators', 'teams', 'milestones', + 'branches', 'autolinks', 'custom_properties', 'variables', + 'environments' + ] + const config = {} + for (const section of sections) { + try { + config[section] = await this[section](repo) + } catch (e) { + this.log.warn(`Failed to extract ${section} for ${repoName}: ${e.message}`) + } + } + try { + config.rulesets = await this.rulesets(repo, 'repo') + } catch (e) { + this.log.warn(`Failed to extract rulesets for ${repoName}: ${e.message}`) + } + return pruneEmpty(config) + } + + /** + * Build the org-level (settings.yml) config. At org scope we can only read + * org-level rulesets and custom repository roles. + * @returns {Promise} + */ + async buildOrgConfig () { + const repo = { owner: this.owner, repo: env.ADMIN_REPO } + const config = {} + try { + config.rulesets = await this.rulesets(repo, 'org') + } catch (e) { + this.log.warn(`Failed to extract org rulesets: ${e.message}`) + } + try { + config.custom_repository_roles = await this.custom_repository_roles(repo) + } catch (e) { + this.log.warn(`Failed to extract custom repository roles: ${e.message}`) + } + return pruneEmpty(config) + } + + /** + * Build a suborg config for all repos that carry a custom property value. + * Settings common to ALL matching repos are kept (intersection). + * @param {string} propertyName + * @param {string|boolean} propertyValue + * @returns {Promise} + */ + async buildSubOrgConfig (propertyName, propertyValue) { + const repos = await this.findReposByProperty(propertyName, propertyValue) + if (repos.length === 0) { + return { suborgproperties: [{ [propertyName]: propertyValue }] } + } + + const configs = [] + for (const repoName of repos) { + configs.push(await this.buildRepoConfig(repoName)) + } + + const common = intersectConfigs(configs) + return Object.assign( + { suborgproperties: [{ [propertyName]: propertyValue }] }, + pruneEmpty(common) + ) + } + + /** + * High level entry point. Resolve the target file path and config content + * for a given source descriptor. + * + * @param {object} source + * @param {'repo'|'org'|'custom-property'} source.sourceType + * @param {string} source.sourceValue For 'repo' the repo name; for 'org' the + * org login; for 'custom-property' a `name=value` pair (or just the value + * if `propertyName` is supplied separately). + * @param {string} [source.propertyName] Custom property name (alternative to + * encoding it in sourceValue). + * @returns {Promise<{ filePath: string, config: object, yaml: string }>} + */ + async generate ({ sourceType, sourceValue, propertyName } = {}) { + let config + let filePath + const base = env.CONFIG_PATH + + switch (sourceType) { + case 'repo': { + config = await this.buildRepoConfig(sourceValue) + filePath = `${base}/repos/${sourceValue}.yml` + break + } + case 'org': { + config = await this.buildOrgConfig() + filePath = `${base}/${env.SETTINGS_FILE_PATH}` + break + } + case 'custom-property': + case 'custom-property-name': { + const { name, value } = parsePropertyValue(sourceValue, propertyName) + config = await this.buildSubOrgConfig(name, value) + filePath = `${base}/suborgs/${name}_${value}.yml` + break + } + default: + throw new Error(`Unsupported source type: ${sourceType}`) + } + + return { filePath, config, yaml: toYaml(config) } + } + + /** + * Discover repository names that have a given custom property value. + * Mirrors Settings.getRepositoriesByProperty. + * @returns {Promise} + */ + async findReposByProperty (propertyName, propertyValue) { + const query = `props.${propertyName}:${propertyValue}` + const encodedQuery = encodeURIComponent(query) + const options = this.github.request.endpoint( + `/orgs/${this.owner}/properties/values?repository_query=${encodedQuery}` + ) + const results = await this.github.paginate(options) + return (results || []) + .map(r => r.repository_name) + .filter(Boolean) + } +} + +// --- Intersection helpers ------------------------------------------------- +const NAME_FIELDS = (MergeDeep.NAME_FIELDS || []) + .concat(['title']) + +function identityOf (item) { + if (!item || typeof item !== 'object') return undefined + const prop = NAME_FIELDS.find(p => Object.prototype.hasOwnProperty.call(item, p)) + return prop ? `${prop}:${item[prop]}` : undefined +} + +function deepEqual (a, b) { + if (a === b) return true + if (typeof a !== typeof b) return false + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) return false + return a.every((x, i) => deepEqual(x, b[i])) + } + if (a && b && typeof a === 'object') { + const ak = Object.keys(a) + const bk = Object.keys(b) + if (ak.length !== bk.length) return false + return ak.every(k => deepEqual(a[k], b[k])) + } + return false +} + +/** + * Reduce a list of config objects to the parts that are identical across ALL + * of them. + * - scalars: kept only if equal everywhere + * - arrays: items kept only if an item with the same identity (NAME_FIELDS) + * AND deep-equal value is present in every config + * - objects: recursively intersected + * @param {object[]} configs + * @returns {object} + */ +function intersectConfigs (configs) { + if (!configs || configs.length === 0) return {} + if (configs.length === 1) return configs[0] + + const result = {} + // Only consider sections present in every config. + const commonSections = Object.keys(configs[0]).filter(section => + configs.every(c => Object.prototype.hasOwnProperty.call(c, section)) + ) + + for (const section of commonSections) { + const values = configs.map(c => c[section]) + result[section] = intersectValues(values) + } + return result +} + +function intersectValues (values) { + const [first] = values + + if (Array.isArray(first)) { + if (!values.every(Array.isArray)) return undefined + const kept = [] + for (const item of first) { + const id = identityOf(item) + const presentEverywhere = values.every(arr => + arr.some(other => (id !== undefined + ? identityOf(other) === id && deepEqual(other, item) + : deepEqual(other, item))) + ) + if (presentEverywhere) kept.push(item) + } + return kept + } + + if (first && typeof first === 'object') { + if (!values.every(v => v && typeof v === 'object' && !Array.isArray(v))) return undefined + const out = {} + const commonKeys = Object.keys(first).filter(k => + values.every(v => Object.prototype.hasOwnProperty.call(v, k)) + ) + for (const k of commonKeys) { + const intersected = intersectValues(values.map(v => v[k])) + if (intersected !== undefined) out[k] = intersected + } + return out + } + + // scalar + return values.every(v => deepEqual(v, first)) ? first : undefined +} + +/** Serialize a config object to YAML. */ +function toYaml (config) { + return yaml.dump(config, { lineWidth: -1, noRefs: true }) +} + +/** + * Parse a custom-property source value. Accepts either a `name=value` pair, a + * `name:value` pair, or just a value when `propertyName` is supplied. + * @returns {{ name: string, value: string }} + */ +function parsePropertyValue (sourceValue, propertyName) { + if (propertyName) { + return { name: propertyName, value: sourceValue } + } + const match = /^([^=:]+)[=:](.+)$/.exec(String(sourceValue || '')) + if (!match) { + throw new Error( + `custom-property source requires a "name=value" pair (got "${sourceValue}")` + ) + } + return { name: match[1].trim(), value: match[2].trim() } +} + +module.exports = SettingsGenerator +module.exports.SettingsGenerator = SettingsGenerator +module.exports.intersectConfigs = intersectConfigs +module.exports.intersectValues = intersectValues +module.exports.deepEqual = deepEqual +module.exports.stripNoise = stripNoise +module.exports.pruneEmpty = pruneEmpty +module.exports.toYaml = toYaml +module.exports.parsePropertyValue = parsePropertyValue diff --git a/package-lock.json b/package-lock.json index aba003965..f9bafb209 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,17 +9,17 @@ "version": "0.1.0-rc.26", "license": "ISC", "dependencies": { - "@apidevtools/json-schema-ref-parser": "^12.0.2", + "@apidevtools/json-schema-ref-parser": "^14.2.1", "@probot/adapter-aws-lambda-serverless": "^4.0.3", "deepmerge": "^4.3.1", "eta": "^3.5.0", "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "minimatch": "^10.0.1", - "node-cron": "^3.0.2", + "lodash": "^4.18.1", + "minimatch": "^10.2.1", + "node-cron": "^4.2.1", "octokit": "^5.0.2", - "probot": "^13.4.4", - "proxy-from-env": "^1.1.0", + "probot": "^14.3.2", + "proxy-from-env": "^2.1.0", "undici": "^7.7.0" }, "devDependencies": { @@ -39,69 +39,39 @@ "nock": "^14.0.1", "nodemon": "^3.1.9", "npm-run-all": "^4.1.5", - "smee-client": "^4.3.1", + "smee-client": "^5.0.0", "standard": "^17.1.2" }, "engines": { - "node": ">= 16.0.0" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" + "node": ">= 22.0.0" } }, "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-12.0.2.tgz", - "integrity": "sha512-SoZWqQz4YMKdw4kEMfG5w6QAy+rntjsoAT1FtvZAnVEnCR4uy9YSuDBNoVAFHgzSz0dJbISLLCSrGR2Zd7bcvA==", + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.2.1.tgz", + "integrity": "sha512-HmdFw9CDYqM6B25pqGBpNeLCKvGPlIx1EbLrVL0zPvj50CJQUHyBNBw45Muk0kEIkogo1VZvOKHajdMuAzSxRg==", + "license": "MIT", "dependencies": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" }, "engines": { - "node": ">= 16" + "node": ">= 20" }, "funding": { "url": "https://github.com/sponsors/philsturgeon" + }, + "peerDependencies": { + "@types/json-schema": "^7.0.15" } }, - "node_modules/@asamuzakjp/css-color": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.1.1.tgz", - "integrity": "sha512-hpRD68SV2OMcZCsrbdkccTw5FXjNDLo5OuqSHyHZfwweGsDWZwDJ2+gONyNAbazZclobMirACLw0lk8WVxIqxA==", - "dev": true, - "dependencies": { - "@csstools/css-calc": "^2.1.2", - "@csstools/css-color-parser": "^3.0.8", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "lru-cache": "^10.4.3" - } - }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true - }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -110,30 +80,32 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", - "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", - "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.24.5", - "@babel/helpers": "^7.24.5", - "@babel/parser": "^7.24.5", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.5", - "@babel/types": "^7.24.5", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -153,34 +125,38 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/generator": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", - "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.24.5", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", - "browserslist": "^4.22.2", + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -193,67 +169,45 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", - "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.24.0" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", - "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.24.3", - "@babel/helper-simple-access": "^7.24.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/helper-validator-identifier": "^7.24.5" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -263,34 +217,11 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", - "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", - "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", - "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, - "dependencies": { - "@babel/types": "^7.24.5" - }, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -306,9 +237,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -316,36 +247,37 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", - "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", - "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -359,6 +291,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -371,6 +304,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -383,6 +317,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, @@ -390,11 +325,44 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-import-meta": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -407,6 +375,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -415,12 +384,13 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz", - "integrity": "sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -434,6 +404,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -446,6 +417,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -458,6 +430,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -470,6 +443,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -482,6 +456,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -494,6 +469,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -501,11 +477,28 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-top-level-await": { "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -517,12 +510,13 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.1.tgz", - "integrity": "sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -532,59 +526,48 @@ } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", - "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/parser": "^7.24.5", - "@babel/types": "^7.24.5", - "debug": "^4.3.1", - "globals": "^11.1.0" + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", - "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -594,156 +577,53 @@ "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, - "node_modules/@csstools/color-helpers": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", - "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@csstools/css-calc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.2.tgz", - "integrity": "sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.8.tgz", - "integrity": "sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "dependencies": { - "@csstools/color-helpers": "^5.0.2", - "@csstools/css-calc": "^2.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", - "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.3" - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", - "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "engines": { - "node": ">=18" - } + "license": "MIT" }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, + "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", + "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", "dev": true, + "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.3", "strip-json-comments": "^3.1.1" }, "engines": { @@ -753,10 +633,17 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/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/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -764,79 +651,38 @@ "concat-map": "0.0.1" } }, - "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "node_modules/@eslint/eslintrc/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": "BSD-2-Clause", + "license": "ISC", "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "brace-expansion": "^1.1.7" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": "*" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -844,10 +690,17 @@ "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/config-array/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/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -856,9 +709,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "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": { @@ -873,6 +726,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=12.22" }, @@ -885,12 +739,14 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "dev": true + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@ioredis/commands": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", - "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", "license": "MIT" }, "node_modules/@istanbuljs/load-nyc-config": { @@ -898,6 +754,7 @@ "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, + "license": "ISC", "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", @@ -914,6 +771,7 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, + "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" } @@ -923,6 +781,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -932,9 +791,9 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", "dev": true, "dependencies": { "argparse": "^1.0.7", @@ -949,6 +808,7 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -961,6 +821,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, + "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -976,6 +837,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -988,6 +850,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -997,6 +860,7 @@ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1006,6 +870,7 @@ "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -1023,6 +888,7 @@ "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" }, @@ -1038,6 +904,7 @@ "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" @@ -1054,6 +921,7 @@ "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" }, @@ -1065,13 +933,15 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/@jest/console/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" } @@ -1081,6 +951,7 @@ "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" }, @@ -1093,6 +964,7 @@ "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", "dev": true, + "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", "@jest/reporters": "^29.7.0", @@ -1140,6 +1012,7 @@ "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" }, @@ -1155,6 +1028,7 @@ "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" @@ -1171,6 +1045,7 @@ "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" }, @@ -1182,13 +1057,15 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/@jest/core/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" } @@ -1198,6 +1075,7 @@ "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" }, @@ -1210,6 +1088,7 @@ "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", @@ -1225,6 +1104,7 @@ "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", "dev": true, + "license": "MIT", "dependencies": { "expect": "^29.7.0", "jest-snapshot": "^29.7.0" @@ -1238,6 +1118,7 @@ "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", "dev": true, + "license": "MIT", "dependencies": { "jest-get-type": "^29.6.3" }, @@ -1250,6 +1131,7 @@ "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", @@ -1267,6 +1149,7 @@ "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -1282,6 +1165,7 @@ "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", "dev": true, + "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@jest/console": "^29.7.0", @@ -1325,6 +1209,7 @@ "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" }, @@ -1340,6 +1225,7 @@ "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" @@ -1356,6 +1242,7 @@ "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" }, @@ -1367,13 +1254,15 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/@jest/reporters/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" } @@ -1383,6 +1272,7 @@ "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" }, @@ -1395,6 +1285,7 @@ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, + "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" }, @@ -1407,6 +1298,7 @@ "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", "callsites": "^3.0.0", @@ -1421,6 +1313,7 @@ "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", "dev": true, + "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", "@jest/types": "^29.6.3", @@ -1436,6 +1329,7 @@ "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/test-result": "^29.7.0", "graceful-fs": "^4.2.9", @@ -1451,6 +1345,7 @@ "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", @@ -1477,6 +1372,7 @@ "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" }, @@ -1492,6 +1388,7 @@ "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" @@ -1508,6 +1405,7 @@ "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" }, @@ -1519,13 +1417,15 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/@jest/transform/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" } @@ -1535,6 +1435,7 @@ "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" }, @@ -1547,6 +1448,7 @@ "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", @@ -1564,6 +1466,7 @@ "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" }, @@ -1579,6 +1482,7 @@ "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" @@ -1595,6 +1499,7 @@ "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" }, @@ -1606,13 +1511,15 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/@jest/types/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" } @@ -1622,6 +1529,7 @@ "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" }, @@ -1630,17 +1538,25 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -1648,52 +1564,40 @@ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "license": "MIT" - }, "node_modules/@mswjs/interceptors": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.38.0.tgz", - "integrity": "sha512-nPHVM+LUl4V1kXPXuTcNN5OMD//ltCQ0lccuEagvidJdpbig3hP3W6/ctWHx6mee7vZIWE0L+Mqj3vx0ASlm/w==", + "version": "0.41.3", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.3.tgz", + "integrity": "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==", "dev": true, + "license": "MIT", "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", - "jsdom": "^26.0.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" }, @@ -1706,6 +1610,7 @@ "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" @@ -1719,6 +1624,7 @@ "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" } @@ -1728,6 +1634,7 @@ "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" @@ -1737,32 +1644,34 @@ } }, "node_modules/@octokit/app": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@octokit/app/-/app-16.0.1.tgz", - "integrity": "sha512-kgTeTsWmpUX+s3Fs4EK4w1K+jWCDB6ClxLSWUWTyhlw7+L3jHtuXDR4QtABu2GsmCMdk67xRhruiXotS3ay3Yw==", - "dependencies": { - "@octokit/auth-app": "^8.0.1", - "@octokit/auth-unauthenticated": "^7.0.1", - "@octokit/core": "^7.0.2", - "@octokit/oauth-app": "^8.0.1", - "@octokit/plugin-paginate-rest": "^13.0.0", - "@octokit/types": "^14.0.0", + "version": "16.1.2", + "resolved": "https://registry.npmjs.org/@octokit/app/-/app-16.1.2.tgz", + "integrity": "sha512-8j7sEpUYVj18dxvh0KWj6W/l6uAiVRBl1JBDVRqH1VHKAO/G5eRVl4yEoYACjakWers1DjUkcCHyJNQK47JqyQ==", + "license": "MIT", + "dependencies": { + "@octokit/auth-app": "^8.1.2", + "@octokit/auth-unauthenticated": "^7.0.3", + "@octokit/core": "^7.0.6", + "@octokit/oauth-app": "^8.0.3", + "@octokit/plugin-paginate-rest": "^14.0.0", + "@octokit/types": "^16.0.0", "@octokit/webhooks": "^14.0.0" }, "engines": { "node": ">= 20" } }, - "node_modules/@octokit/app/node_modules/@octokit/auth-app": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-8.0.1.tgz", - "integrity": "sha512-P2J5pB3pjiGwtJX4WqJVYCtNkcZ+j5T2Wm14aJAEIC3WJOrv12jvBley3G1U/XI8q9o1A7QMG54LiFED2BiFlg==", + "node_modules/@octokit/auth-app": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-8.2.0.tgz", + "integrity": "sha512-vVjdtQQwomrZ4V46B9LaCsxsySxGoHsyw6IYBov/TqJVROrlYdyNgw5q6tQbB7KZt53v1l1W53RiqTvpzL907g==", + "license": "MIT", "dependencies": { - "@octokit/auth-oauth-app": "^9.0.1", - "@octokit/auth-oauth-user": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0", + "@octokit/auth-oauth-app": "^9.0.3", + "@octokit/auth-oauth-user": "^6.0.2", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", "toad-cache": "^3.7.0", "universal-github-app-jwt": "^2.2.0", "universal-user-agent": "^7.0.0" @@ -1771,80 +1680,86 @@ "node": ">= 20" } }, - "node_modules/@octokit/app/node_modules/@octokit/auth-oauth-app": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-9.0.1.tgz", - "integrity": "sha512-TthWzYxuHKLAbmxdFZwFlmwVyvynpyPmjwc+2/cI3cvbT7mHtsAW9b1LvQaNnAuWL+pFnqtxdmrU8QpF633i1g==", + "node_modules/@octokit/auth-oauth-app": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-9.0.3.tgz", + "integrity": "sha512-+yoFQquaF8OxJSxTb7rnytBIC2ZLbLqA/yb71I4ZXT9+Slw4TziV9j/kyGhUFRRTF2+7WlnIWsePZCWHs+OGjg==", + "license": "MIT", "dependencies": { - "@octokit/auth-oauth-device": "^8.0.1", - "@octokit/auth-oauth-user": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", + "@octokit/auth-oauth-device": "^8.0.3", + "@octokit/auth-oauth-user": "^6.0.2", + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" }, "engines": { "node": ">= 20" } }, - "node_modules/@octokit/app/node_modules/@octokit/auth-oauth-device": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-8.0.1.tgz", - "integrity": "sha512-TOqId/+am5yk9zor0RGibmlqn4V0h8vzjxlw/wYr3qzkQxl8aBPur384D1EyHtqvfz0syeXji4OUvKkHvxk/Gw==", + "node_modules/@octokit/auth-oauth-device": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-8.0.3.tgz", + "integrity": "sha512-zh2W0mKKMh/VWZhSqlaCzY7qFyrgd9oTWmTmHaXnHNeQRCZr/CXy2jCgHo4e4dJVTiuxP5dLa0YM5p5QVhJHbw==", + "license": "MIT", "dependencies": { - "@octokit/oauth-methods": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", + "@octokit/oauth-methods": "^6.0.2", + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" }, "engines": { "node": ">= 20" } }, - "node_modules/@octokit/app/node_modules/@octokit/auth-oauth-user": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-6.0.0.tgz", - "integrity": "sha512-GV9IW134PHsLhtUad21WIeP9mlJ+QNpFd6V9vuPWmaiN25HEJeEQUcS4y5oRuqCm9iWDLtfIs+9K8uczBXKr6A==", + "node_modules/@octokit/auth-oauth-user": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-6.0.2.tgz", + "integrity": "sha512-qLoPPc6E6GJoz3XeDG/pnDhJpTkODTGG4kY0/Py154i/I003O9NazkrwJwRuzgCalhzyIeWQ+6MDvkUmKXjg/A==", + "license": "MIT", "dependencies": { - "@octokit/auth-oauth-device": "^8.0.1", - "@octokit/oauth-methods": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", + "@octokit/auth-oauth-device": "^8.0.3", + "@octokit/oauth-methods": "^6.0.2", + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" }, "engines": { "node": ">= 20" } }, - "node_modules/@octokit/app/node_modules/@octokit/auth-token": { + "node_modules/@octokit/auth-token": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "license": "MIT", "engines": { "node": ">= 20" } }, - "node_modules/@octokit/app/node_modules/@octokit/auth-unauthenticated": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-7.0.1.tgz", - "integrity": "sha512-qVq1vdjLLZdE8kH2vDycNNjuJRCD1q2oet1nA/GXWaYlpDxlR7rdVhX/K/oszXslXiQIiqrQf+rdhDlA99JdTQ==", + "node_modules/@octokit/auth-unauthenticated": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-7.0.3.tgz", + "integrity": "sha512-8Jb1mtUdmBHL7lGmop9mU9ArMRUTRhg8vp0T1VtZ4yd9vEm3zcLwmjQkhNEduKawOOORie61xhtYIhTDN+ZQ3g==", + "license": "MIT", "dependencies": { - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0" + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0" }, "engines": { "node": ">= 20" } }, - "node_modules/@octokit/app/node_modules/@octokit/core": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.2.tgz", - "integrity": "sha512-ODsoD39Lq6vR6aBgvjTnA3nZGliknKboc9Gtxr7E4WDNqY24MxANKcuDQSF0jzapvGb3KWOEDrKfve4HoWGK+g==", + "node_modules/@octokit/core": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", + "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", + "license": "MIT", "dependencies": { "@octokit/auth-token": "^6.0.0", - "@octokit/graphql": "^9.0.1", - "@octokit/request": "^10.0.2", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0", + "@octokit/graphql": "^9.0.3", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" }, @@ -1852,65 +1767,106 @@ "node": ">= 20" } }, - "node_modules/@octokit/app/node_modules/@octokit/endpoint": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", - "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", + "node_modules/@octokit/endpoint": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz", + "integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==", + "license": "MIT", "dependencies": { - "@octokit/types": "^14.0.0", + "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" }, "engines": { "node": ">= 20" } }, - "node_modules/@octokit/app/node_modules/@octokit/graphql": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.1.tgz", - "integrity": "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==", + "node_modules/@octokit/graphql": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz", + "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/oauth-app": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-8.0.3.tgz", + "integrity": "sha512-jnAjvTsPepyUaMu9e69hYBuozEPgYqP4Z3UnpmvoIzHDpf8EXDGvTY1l1jK0RsZ194oRd+k6Hm13oRU8EoDFwg==", + "license": "MIT", "dependencies": { - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", + "@octokit/auth-oauth-app": "^9.0.2", + "@octokit/auth-oauth-user": "^6.0.1", + "@octokit/auth-unauthenticated": "^7.0.2", + "@octokit/core": "^7.0.5", + "@octokit/oauth-authorization-url": "^8.0.0", + "@octokit/oauth-methods": "^6.0.1", + "@types/aws-lambda": "^8.10.83", "universal-user-agent": "^7.0.0" }, "engines": { "node": ">= 20" } }, - "node_modules/@octokit/app/node_modules/@octokit/oauth-authorization-url": { + "node_modules/@octokit/oauth-authorization-url": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-8.0.0.tgz", "integrity": "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==", + "license": "MIT", "engines": { "node": ">= 20" } }, - "node_modules/@octokit/app/node_modules/@octokit/oauth-methods": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-6.0.0.tgz", - "integrity": "sha512-Q8nFIagNLIZgM2odAraelMcDssapc+lF+y3OlcIPxyAU+knefO8KmozGqfnma1xegRDP4z5M73ABsamn72bOcA==", + "node_modules/@octokit/oauth-methods": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-6.0.2.tgz", + "integrity": "sha512-HiNOO3MqLxlt5Da5bZbLV8Zarnphi4y9XehrbaFMkcoJ+FL7sMxH/UlUsCVxpddVu4qvNDrBdaTVE2o4ITK8ng==", + "license": "MIT", "dependencies": { "@octokit/oauth-authorization-url": "^8.0.0", - "@octokit/request": "^10.0.2", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0" + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0" }, "engines": { "node": ">= 20" } }, - "node_modules/@octokit/app/node_modules/@octokit/openapi-types": { - "version": "25.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", - "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==" + "node_modules/@octokit/openapi-types": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", + "license": "MIT" + }, + "node_modules/@octokit/openapi-webhooks-types": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-webhooks-types/-/openapi-webhooks-types-12.1.0.tgz", + "integrity": "sha512-WiuzhOsiOvb7W3Pvmhf8d2C6qaLHXrWiLBP4nJ/4kydu+wpagV5Fkz9RfQwV2afYzv3PB+3xYgp4mAdNGjDprA==", + "license": "MIT" }, - "node_modules/@octokit/app/node_modules/@octokit/plugin-paginate-rest": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.0.0.tgz", - "integrity": "sha512-nPXM3wgil9ONnAINcm8cN+nwso4QhNB13PtnlRFkYFHCUIogcH9DHak/StQYcwkkjuc7pUluLG1AWZNscgvH7Q==", + "node_modules/@octokit/plugin-enterprise-compatibility": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-enterprise-compatibility/-/plugin-enterprise-compatibility-6.0.3.tgz", + "integrity": "sha512-CmLJklw7eOAFhKpK0L2lCdHzCJXXnsq+zPLvQ6ys2qEB2pxdkPo6meqj73FxbNScaYz0OOPIpSZfJqx/vovLEQ==", + "license": "MIT", "dependencies": { - "@octokit/types": "^14.0.0" + "@octokit/request-error": "^7.1.0", + "@octokit/types": "^16.0.0" }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/plugin-paginate-graphql": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-6.0.0.tgz", + "integrity": "sha512-crfpnIoFiBtRkvPqOyLOsw12XsveYuY2ieP6uYDosoUegBJpSVxGwut9sxUgFFcll3VTOTqpUf8yGd8x1OmAkQ==", + "license": "MIT", "engines": { "node": ">= 20" }, @@ -1918,816 +1874,713 @@ "@octokit/core": ">=6" } }, - "node_modules/@octokit/app/node_modules/@octokit/request": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.2.tgz", - "integrity": "sha512-iYj4SJG/2bbhh+iIpFmG5u49DtJ4lipQ+aPakjL9OKpsGY93wM8w06gvFbEQxcMsZcCvk5th5KkIm2m8o14aWA==", + "node_modules/@octokit/plugin-paginate-rest": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz", + "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==", + "license": "MIT", "dependencies": { - "@octokit/endpoint": "^11.0.0", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0", - "fast-content-type-parse": "^3.0.0", - "universal-user-agent": "^7.0.2" + "@octokit/types": "^16.0.0" }, "engines": { "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" } }, - "node_modules/@octokit/app/node_modules/@octokit/request-error": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", - "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", + "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", + "license": "MIT", "dependencies": { - "@octokit/types": "^14.0.0" + "@octokit/types": "^16.0.0" }, "engines": { "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" } }, - "node_modules/@octokit/app/node_modules/@octokit/types": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz", - "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==", + "node_modules/@octokit/plugin-retry": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-8.1.0.tgz", + "integrity": "sha512-O1FZgXeiGb2sowEr/hYTr6YunGdSAFWnr2fyW39Ah85H8O33ELASQxcvOFF5LE6Tjekcyu2ms4qAzJVhSaJxTw==", + "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^25.0.0" + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=7" } }, - "node_modules/@octokit/app/node_modules/@octokit/webhooks": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-14.0.0.tgz", - "integrity": "sha512-IZV4vg/s1pqIpCs86a0tp5FQ/O94DUaqksMdNrXFSaE037TXsB+fIhr8OVig09oEx3WazVgE6B2U+u7/Fvdlsw==", + "node_modules/@octokit/plugin-throttling": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-11.0.3.tgz", + "integrity": "sha512-34eE0RkFCKycLl2D2kq7W+LovheM/ex3AwZCYN8udpi6bxsyjZidb2McXs69hZhLmJlDqTSP8cH+jSRpiaijBg==", + "license": "MIT", "dependencies": { - "@octokit/openapi-webhooks-types": "11.0.0", - "@octokit/request-error": "^7.0.0", - "@octokit/webhooks-methods": "^6.0.0" + "@octokit/types": "^16.0.0", + "bottleneck": "^2.15.3" }, "engines": { "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": "^7.0.0" } }, - "node_modules/@octokit/app/node_modules/@octokit/webhooks-methods": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-6.0.0.tgz", - "integrity": "sha512-MFlzzoDJVw/GcbfzVC1RLR36QqkTLUf79vLVO3D+xn7r0QgxnFoLZgtrzxiQErAjFUOdH6fas2KeQJ1yr/qaXQ==", + "node_modules/@octokit/request": { + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz", + "integrity": "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.3", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "fast-content-type-parse": "^3.0.0", + "json-with-bigint": "^3.5.3", + "universal-user-agent": "^7.0.2" + }, "engines": { "node": ">= 20" } }, - "node_modules/@octokit/app/node_modules/before-after-hook": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", - "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==" - }, - "node_modules/@octokit/app/node_modules/universal-github-app-jwt": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.2.tgz", - "integrity": "sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==" - }, - "node_modules/@octokit/app/node_modules/universal-user-agent": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", - "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==" - }, - "node_modules/@octokit/auth-app": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-6.1.3.tgz", - "integrity": "sha512-dcaiteA6Y/beAlDLZOPNReN3FGHu+pARD6OHfh3T9f3EO09++ec+5wt3KtGGSSs2Mp5tI8fQwdMOEnrzBLfgUA==", + "node_modules/@octokit/request-error": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", + "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", "license": "MIT", "dependencies": { - "@octokit/auth-oauth-app": "^7.1.0", - "@octokit/auth-oauth-user": "^4.1.0", - "@octokit/request": "^8.3.1", - "@octokit/request-error": "^5.1.0", - "@octokit/types": "^13.1.0", - "deprecation": "^2.3.1", - "lru-cache": "npm:@wolfy1339/lru-cache@^11.0.2-patch.1", - "universal-github-app-jwt": "^1.1.2", - "universal-user-agent": "^6.0.0" + "@octokit/types": "^16.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, - "node_modules/@octokit/auth-app/node_modules/@octokit/openapi-types": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", - "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", - "license": "MIT" - }, - "node_modules/@octokit/auth-app/node_modules/@octokit/types": { - "version": "13.8.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", - "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", + "node_modules/@octokit/types": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^23.0.1" + "@octokit/openapi-types": "^27.0.0" } }, - "node_modules/@octokit/auth-app/node_modules/lru-cache": { - "name": "@wolfy1339/lru-cache", - "version": "11.0.2-patch.1", - "resolved": "https://registry.npmjs.org/@wolfy1339/lru-cache/-/lru-cache-11.0.2-patch.1.tgz", - "integrity": "sha512-BgYZfL2ADCXKOw2wJtkM3slhHotawWkgIRRxq4wEybnZQPjvAp71SPX35xepMykTw8gXlzWcWPTY31hlbnRsDA==", - "license": "ISC", - "engines": { - "node": "18 >=18.20 || 20 || >=22" - } - }, - "node_modules/@octokit/auth-oauth-app": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-7.1.0.tgz", - "integrity": "sha512-w+SyJN/b0l/HEb4EOPRudo7uUOSW51jcK1jwLa+4r7PA8FPFpoxEnHBHMITqCsc/3Vo2qqFjgQfz/xUUvsSQnA==", + "node_modules/@octokit/webhooks": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-14.2.0.tgz", + "integrity": "sha512-da6KbdNCV5sr1/txD896V+6W0iamFWrvVl8cHkBSPT+YlvmT3DwXa4jxZnQc+gnuTEqSWbBeoSZYTayXH9wXcw==", "license": "MIT", "dependencies": { - "@octokit/auth-oauth-device": "^6.1.0", - "@octokit/auth-oauth-user": "^4.1.0", - "@octokit/request": "^8.3.1", - "@octokit/types": "^13.0.0", - "@types/btoa-lite": "^1.0.0", - "btoa-lite": "^1.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/openapi-webhooks-types": "12.1.0", + "@octokit/request-error": "^7.0.0", + "@octokit/webhooks-methods": "^6.0.0" }, "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/openapi-types": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", - "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", - "license": "MIT" - }, - "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/types": { - "version": "13.8.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", - "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^23.0.1" + "node": ">= 20" } }, - "node_modules/@octokit/auth-oauth-device": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-6.1.0.tgz", - "integrity": "sha512-FNQ7cb8kASufd6Ej4gnJ3f1QB5vJitkoV1O0/g6e6lUsQ7+VsSNRHRmFScN2tV4IgKA12frrr/cegUs0t+0/Lw==", + "node_modules/@octokit/webhooks-methods": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-6.0.0.tgz", + "integrity": "sha512-MFlzzoDJVw/GcbfzVC1RLR36QqkTLUf79vLVO3D+xn7r0QgxnFoLZgtrzxiQErAjFUOdH6fas2KeQJ1yr/qaXQ==", "license": "MIT", - "dependencies": { - "@octokit/oauth-methods": "^4.1.0", - "@octokit/request": "^8.3.1", - "@octokit/types": "^13.0.0", - "universal-user-agent": "^6.0.0" - }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, - "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/openapi-types": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", - "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "node_modules/@octokit/webhooks-types": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.6.1.tgz", + "integrity": "sha512-S8u2cJzklBC0FgTwWVLaM8tMrDuDMVE4xiTK4EYXM9GntyvrdbSoxqDQa+Fh57CCNApyIpyeqPhhFEmHPfrXgw==", "license": "MIT" }, - "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/types": { - "version": "13.8.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", - "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^23.0.1" - } + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" }, - "node_modules/@octokit/auth-oauth-user": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-4.1.0.tgz", - "integrity": "sha512-FrEp8mtFuS/BrJyjpur+4GARteUCrPeR/tZJzD8YourzoVhRics7u7we/aDcKv+yywRNwNi/P4fRi631rG/OyQ==", + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, "license": "MIT", "dependencies": { - "@octokit/auth-oauth-device": "^6.1.0", - "@octokit/oauth-methods": "^4.1.0", - "@octokit/request": "^8.3.1", - "@octokit/types": "^13.0.0", - "btoa-lite": "^1.0.0", - "universal-user-agent": "^6.0.0" - }, - "engines": { - "node": ">= 18" + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" } }, - "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/openapi-types": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", - "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, "license": "MIT" }, - "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/types": { - "version": "13.8.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", - "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^23.0.1" - } - }, - "node_modules/@octokit/auth-token": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", - "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", "engines": { - "node": ">= 18" + "node": ">=8.0.0" } }, - "node_modules/@octokit/auth-unauthenticated": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-5.0.1.tgz", - "integrity": "sha512-oxeWzmBFxWd+XolxKTc4zr+h3mt+yofn4r7OfoIkR/Cj/o70eEGmPsFbueyJE2iBAGpjgTnEOKM3pnuEGVmiqg==", - "license": "MIT", + "node_modules/@opentelemetry/api-logs": { + "version": "0.57.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.57.2.tgz", + "integrity": "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==", + "license": "Apache-2.0", "dependencies": { - "@octokit/request-error": "^5.0.0", - "@octokit/types": "^12.0.0" + "@opentelemetry/api": "^1.3.0" }, "engines": { - "node": ">= 18" + "node": ">=14" } }, - "node_modules/@octokit/core": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.0.tgz", - "integrity": "sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==", - "dependencies": { - "@octokit/auth-token": "^4.0.0", - "@octokit/graphql": "^7.1.0", - "@octokit/request": "^8.3.1", - "@octokit/request-error": "^5.1.0", - "@octokit/types": "^13.0.0", - "before-after-hook": "^2.2.0", - "universal-user-agent": "^6.0.0" - }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz", + "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==", + "license": "Apache-2.0", "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/core/node_modules/@octokit/openapi-types": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", - "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" - }, - "node_modules/@octokit/core/node_modules/@octokit/types": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", - "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", - "dependencies": { - "@octokit/openapi-types": "^22.2.0" + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@octokit/endpoint": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", - "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "node_modules/@opentelemetry/core": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz", + "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==", + "license": "Apache-2.0", "dependencies": { - "@octokit/types": "^13.1.0", - "universal-user-agent": "^6.0.0" + "@opentelemetry/semantic-conventions": "1.28.0" }, "engines": { - "node": ">= 18" + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", - "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==" - }, - "node_modules/@octokit/endpoint/node_modules/@octokit/types": { - "version": "13.8.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", - "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", - "dependencies": { - "@octokit/openapi-types": "^23.0.1" + "node_modules/@opentelemetry/core/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" } }, - "node_modules/@octokit/graphql": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.0.tgz", - "integrity": "sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ==", + "node_modules/@opentelemetry/instrumentation": { + "version": "0.57.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz", + "integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==", + "license": "Apache-2.0", "dependencies": { - "@octokit/request": "^8.3.0", - "@octokit/types": "^13.0.0", - "universal-user-agent": "^6.0.0" + "@opentelemetry/api-logs": "0.57.2", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" }, "engines": { - "node": ">= 18" + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", - "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" - }, - "node_modules/@octokit/graphql/node_modules/@octokit/types": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", - "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", + "node_modules/@opentelemetry/instrumentation-amqplib": { + "version": "0.46.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.46.1.tgz", + "integrity": "sha512-AyXVnlCf/xV3K/rNumzKxZqsULyITJH6OVLiW6730JPRqWA7Zc9bvYoVNpN6iOpTU8CasH34SU/ksVJmObFibQ==", + "license": "Apache-2.0", "dependencies": { - "@octokit/openapi-types": "^22.2.0" + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@octokit/oauth-app": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-8.0.1.tgz", - "integrity": "sha512-QnhMYEQpnYbEPn9cae+wXL2LuPMFglmfeuDJXXsyxIXdoORwkLK8y0cHhd/5du9MbO/zdG/BXixzB7EEwU63eQ==", + "node_modules/@opentelemetry/instrumentation-connect": { + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.43.0.tgz", + "integrity": "sha512-Q57JGpH6T4dkYHo9tKXONgLtxzsh1ZEW5M9A/OwKrZFyEpLqWgjhcZ3hIuVvDlhb426iDF1f9FPToV/mi5rpeA==", + "license": "Apache-2.0", "dependencies": { - "@octokit/auth-oauth-app": "^9.0.1", - "@octokit/auth-oauth-user": "^6.0.0", - "@octokit/auth-unauthenticated": "^7.0.1", - "@octokit/core": "^7.0.2", - "@octokit/oauth-authorization-url": "^8.0.0", - "@octokit/oauth-methods": "^6.0.0", - "@types/aws-lambda": "^8.10.83", - "universal-user-agent": "^7.0.0" + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/connect": "3.4.36" }, "engines": { - "node": ">= 20" + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@octokit/oauth-app/node_modules/@octokit/auth-oauth-app": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-9.0.1.tgz", - "integrity": "sha512-TthWzYxuHKLAbmxdFZwFlmwVyvynpyPmjwc+2/cI3cvbT7mHtsAW9b1LvQaNnAuWL+pFnqtxdmrU8QpF633i1g==", + "node_modules/@opentelemetry/instrumentation-connect/node_modules/@types/connect": { + "version": "3.4.36", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz", + "integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==", + "license": "MIT", "dependencies": { - "@octokit/auth-oauth-device": "^8.0.1", - "@octokit/auth-oauth-user": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" + "@types/node": "*" } }, - "node_modules/@octokit/oauth-app/node_modules/@octokit/auth-oauth-device": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-8.0.1.tgz", - "integrity": "sha512-TOqId/+am5yk9zor0RGibmlqn4V0h8vzjxlw/wYr3qzkQxl8aBPur384D1EyHtqvfz0syeXji4OUvKkHvxk/Gw==", + "node_modules/@opentelemetry/instrumentation-dataloader": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.16.0.tgz", + "integrity": "sha512-88+qCHZC02up8PwKHk0UQKLLqGGURzS3hFQBZC7PnGwReuoKjHXS1o29H58S+QkXJpkTr2GACbx8j6mUoGjNPA==", + "license": "Apache-2.0", "dependencies": { - "@octokit/oauth-methods": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.0" + "@opentelemetry/instrumentation": "^0.57.0" }, "engines": { - "node": ">= 20" + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@octokit/oauth-app/node_modules/@octokit/auth-oauth-user": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-6.0.0.tgz", - "integrity": "sha512-GV9IW134PHsLhtUad21WIeP9mlJ+QNpFd6V9vuPWmaiN25HEJeEQUcS4y5oRuqCm9iWDLtfIs+9K8uczBXKr6A==", + "node_modules/@opentelemetry/instrumentation-express": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.47.0.tgz", + "integrity": "sha512-XFWVx6k0XlU8lu6cBlCa29ONtVt6ADEjmxtyAyeF2+rifk8uBJbk1La0yIVfI0DoKURGbaEDTNelaXG9l/lNNQ==", + "license": "Apache-2.0", "dependencies": { - "@octokit/auth-oauth-device": "^8.0.1", - "@octokit/oauth-methods": "^6.0.0", - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.0" + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { - "node": ">= 20" + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@octokit/oauth-app/node_modules/@octokit/auth-token": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", - "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/oauth-app/node_modules/@octokit/auth-unauthenticated": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-7.0.1.tgz", - "integrity": "sha512-qVq1vdjLLZdE8kH2vDycNNjuJRCD1q2oet1nA/GXWaYlpDxlR7rdVhX/K/oszXslXiQIiqrQf+rdhDlA99JdTQ==", + "node_modules/@opentelemetry/instrumentation-fastify": { + "version": "0.44.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.44.1.tgz", + "integrity": "sha512-RoVeMGKcNttNfXMSl6W4fsYoCAYP1vi6ZAWIGhBY+o7R9Y0afA7f9JJL0j8LHbyb0P0QhSYk+6O56OwI2k4iRQ==", + "license": "Apache-2.0", "dependencies": { - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0" + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { - "node": ">= 20" + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@octokit/oauth-app/node_modules/@octokit/core": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.2.tgz", - "integrity": "sha512-ODsoD39Lq6vR6aBgvjTnA3nZGliknKboc9Gtxr7E4WDNqY24MxANKcuDQSF0jzapvGb3KWOEDrKfve4HoWGK+g==", + "node_modules/@opentelemetry/instrumentation-fs": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.19.0.tgz", + "integrity": "sha512-JGwmHhBkRT2G/BYNV1aGI+bBjJu4fJUD/5/Jat0EWZa2ftrLV3YE8z84Fiij/wK32oMZ88eS8DI4ecLGZhpqsQ==", + "license": "Apache-2.0", "dependencies": { - "@octokit/auth-token": "^6.0.0", - "@octokit/graphql": "^9.0.1", - "@octokit/request": "^10.0.2", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0", - "before-after-hook": "^4.0.0", - "universal-user-agent": "^7.0.0" + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.0" }, "engines": { - "node": ">= 20" + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@octokit/oauth-app/node_modules/@octokit/endpoint": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", - "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", + "node_modules/@opentelemetry/instrumentation-generic-pool": { + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.43.0.tgz", + "integrity": "sha512-at8GceTtNxD1NfFKGAuwtqM41ot/TpcLh+YsGe4dhf7gvv1HW/ZWdq6nfRtS6UjIvZJOokViqLPJ3GVtZItAnQ==", + "license": "Apache-2.0", "dependencies": { - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.2" + "@opentelemetry/instrumentation": "^0.57.0" }, "engines": { - "node": ">= 20" + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@octokit/oauth-app/node_modules/@octokit/graphql": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.1.tgz", - "integrity": "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==", + "node_modules/@opentelemetry/instrumentation-graphql": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.47.0.tgz", + "integrity": "sha512-Cc8SMf+nLqp0fi8oAnooNEfwZWFnzMiBHCGmDFYqmgjPylyLmi83b+NiTns/rKGwlErpW0AGPt0sMpkbNlzn8w==", + "license": "Apache-2.0", "dependencies": { - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.0" + "@opentelemetry/instrumentation": "^0.57.0" }, "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/oauth-app/node_modules/@octokit/oauth-authorization-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-8.0.0.tgz", - "integrity": "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==", - "engines": { - "node": ">= 20" + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@octokit/oauth-app/node_modules/@octokit/oauth-methods": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-6.0.0.tgz", - "integrity": "sha512-Q8nFIagNLIZgM2odAraelMcDssapc+lF+y3OlcIPxyAU+knefO8KmozGqfnma1xegRDP4z5M73ABsamn72bOcA==", + "node_modules/@opentelemetry/instrumentation-hapi": { + "version": "0.45.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.45.1.tgz", + "integrity": "sha512-VH6mU3YqAKTePPfUPwfq4/xr049774qWtfTuJqVHoVspCLiT3bW+fCQ1toZxt6cxRPYASoYaBsMA3CWo8B8rcw==", + "license": "Apache-2.0", "dependencies": { - "@octokit/oauth-authorization-url": "^8.0.0", - "@octokit/request": "^10.0.2", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0" + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { - "node": ">= 20" + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@octokit/oauth-app/node_modules/@octokit/openapi-types": { - "version": "25.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz", - "integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==" - }, - "node_modules/@octokit/oauth-app/node_modules/@octokit/request": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.2.tgz", - "integrity": "sha512-iYj4SJG/2bbhh+iIpFmG5u49DtJ4lipQ+aPakjL9OKpsGY93wM8w06gvFbEQxcMsZcCvk5th5KkIm2m8o14aWA==", + "node_modules/@opentelemetry/instrumentation-http": { + "version": "0.57.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.57.1.tgz", + "integrity": "sha512-ThLmzAQDs7b/tdKI3BV2+yawuF09jF111OFsovqT1Qj3D8vjwKBwhi/rDE5xethwn4tSXtZcJ9hBsVAlWFQZ7g==", + "license": "Apache-2.0", "dependencies": { - "@octokit/endpoint": "^11.0.0", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0", - "fast-content-type-parse": "^3.0.0", - "universal-user-agent": "^7.0.2" + "@opentelemetry/core": "1.30.1", + "@opentelemetry/instrumentation": "0.57.1", + "@opentelemetry/semantic-conventions": "1.28.0", + "forwarded-parse": "2.1.2", + "semver": "^7.5.2" }, "engines": { - "node": ">= 20" + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@octokit/oauth-app/node_modules/@octokit/request-error": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", - "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", + "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/api-logs": { + "version": "0.57.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.57.1.tgz", + "integrity": "sha512-I4PHczeujhQAQv6ZBzqHYEUiggZL4IdSMixtVD3EYqbdrjujE7kRfI5QohjlPoJm8BvenoW5YaTMWRrbpot6tg==", + "license": "Apache-2.0", "dependencies": { - "@octokit/types": "^14.0.0" + "@opentelemetry/api": "^1.3.0" }, "engines": { - "node": ">= 20" + "node": ">=14" } }, - "node_modules/@octokit/oauth-app/node_modules/@octokit/types": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz", - "integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==", + "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/instrumentation": { + "version": "0.57.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.1.tgz", + "integrity": "sha512-SgHEKXoVxOjc20ZYusPG3Fh+RLIZTSa4x8QtD3NfgAUDyqdFFS9W1F2ZVbZkqDCdyMcQG02Ok4duUGLHJXHgbA==", + "license": "Apache-2.0", "dependencies": { - "@octokit/openapi-types": "^25.0.0" + "@opentelemetry/api-logs": "0.57.1", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@octokit/oauth-app/node_modules/before-after-hook": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", - "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==" - }, - "node_modules/@octokit/oauth-app/node_modules/universal-user-agent": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", - "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==" - }, - "node_modules/@octokit/oauth-authorization-url": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-6.0.2.tgz", - "integrity": "sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==", - "license": "MIT", + "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", "engines": { - "node": ">= 18" + "node": ">=14" } }, - "node_modules/@octokit/oauth-methods": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-4.1.0.tgz", - "integrity": "sha512-4tuKnCRecJ6CG6gr0XcEXdZtkTDbfbnD5oaHBmLERTjTMZNi2CbfEHZxPU41xXLDG4DfKf+sonu00zvKI9NSbw==", - "license": "MIT", + "node_modules/@opentelemetry/instrumentation-ioredis": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.47.0.tgz", + "integrity": "sha512-4HqP9IBC8e7pW9p90P3q4ox0XlbLGme65YTrA3UTLvqvo4Z6b0puqZQP203YFu8m9rE/luLfaG7/xrwwqMUpJw==", + "license": "Apache-2.0", "dependencies": { - "@octokit/oauth-authorization-url": "^6.0.2", - "@octokit/request": "^8.3.1", - "@octokit/request-error": "^5.1.0", - "@octokit/types": "^13.0.0", - "btoa-lite": "^1.0.0" + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/redis-common": "^0.36.2", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/oauth-methods/node_modules/@octokit/openapi-types": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", - "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", - "license": "MIT" - }, - "node_modules/@octokit/oauth-methods/node_modules/@octokit/types": { - "version": "13.8.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", - "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^23.0.1" + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@octokit/openapi-types": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", - "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==" - }, - "node_modules/@octokit/openapi-webhooks-types": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-webhooks-types/-/openapi-webhooks-types-11.0.0.tgz", - "integrity": "sha512-ZBzCFj98v3SuRM7oBas6BHZMJRadlnDoeFfvm1olVxZnYeU6Vh97FhPxyS5aLh5pN51GYv2I51l/hVUAVkGBlA==" - }, - "node_modules/@octokit/plugin-enterprise-compatibility": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-enterprise-compatibility/-/plugin-enterprise-compatibility-4.1.0.tgz", - "integrity": "sha512-a8QehVu9Iy4k+m2XgG2rrF4m9vhlRIaefOMr0yJzgQCt4KpiTj5mZVrzSwagyOovkJdD0yDolQazBQZqPWTFSQ==", - "license": "MIT", + "node_modules/@opentelemetry/instrumentation-kafkajs": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.7.0.tgz", + "integrity": "sha512-LB+3xiNzc034zHfCtgs4ITWhq6Xvdo8bsq7amR058jZlf2aXXDrN9SV4si4z2ya9QX4tz6r4eZJwDkXOp14/AQ==", + "license": "Apache-2.0", "dependencies": { - "@octokit/request-error": "^5.0.0", - "@octokit/types": "^12.0.0" + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { - "node": ">= 18" + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@octokit/plugin-paginate-rest": { - "version": "11.4.4-cjs.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.4-cjs.2.tgz", - "integrity": "sha512-2dK6z8fhs8lla5PaOTgqfCGBxgAv/le+EhPs27KklPhm1bKObpu6lXzwfUEQ16ajXzqNrKMujsFyo9K2eaoISw==", - "license": "MIT", + "node_modules/@opentelemetry/instrumentation-knex": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.44.0.tgz", + "integrity": "sha512-SlT0+bLA0Lg3VthGje+bSZatlGHw/vwgQywx0R/5u9QC59FddTQSPJeWNw29M6f8ScORMeUOOTwihlQAn4GkJQ==", + "license": "Apache-2.0", "dependencies": { - "@octokit/types": "^13.7.0" + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { - "node": ">= 18" + "node": ">=14" }, "peerDependencies": { - "@octokit/core": "5" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", - "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", - "license": "MIT" - }, - "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { - "version": "13.8.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", - "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", - "license": "MIT", + "node_modules/@opentelemetry/instrumentation-koa": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.47.0.tgz", + "integrity": "sha512-HFdvqf2+w8sWOuwtEXayGzdZ2vWpCKEQv5F7+2DSA74Te/Cv4rvb2E5So5/lh+ok4/RAIPuvCbCb/SHQFzMmbw==", + "license": "Apache-2.0", "dependencies": { - "@octokit/openapi-types": "^23.0.1" + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "13.3.2-cjs.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.3.2-cjs.1.tgz", - "integrity": "sha512-VUjIjOOvF2oELQmiFpWA1aOPdawpyaCUqcEBc/UOUnj3Xp6DJGrJ1+bjUIIDzdHjnFNO6q57ODMfdEZnoBkCwQ==", - "license": "MIT", + "node_modules/@opentelemetry/instrumentation-lru-memoizer": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.44.0.tgz", + "integrity": "sha512-Tn7emHAlvYDFik3vGU0mdwvWJDwtITtkJ+5eT2cUquct6nIs+H8M47sqMJkCpyPe5QIBJoTOHxmc6mj9lz6zDw==", + "license": "Apache-2.0", "dependencies": { - "@octokit/types": "^13.8.0" + "@opentelemetry/instrumentation": "^0.57.0" }, "engines": { - "node": ">= 18" + "node": ">=14" }, "peerDependencies": { - "@octokit/core": "^5" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", - "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", - "license": "MIT" - }, - "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { - "version": "13.8.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", - "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^23.0.1" - } - }, - "node_modules/@octokit/plugin-retry": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz", - "integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==", + "node_modules/@opentelemetry/instrumentation-mongodb": { + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.51.0.tgz", + "integrity": "sha512-cMKASxCX4aFxesoj3WK8uoQ0YUrRvnfxaO72QWI2xLu5ZtgX/QvdGBlU3Ehdond5eb74c2s1cqRQUIptBnKz1g==", + "license": "Apache-2.0", "dependencies": { - "@octokit/request-error": "^5.0.0", - "@octokit/types": "^12.0.0", - "bottleneck": "^2.15.3" + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { - "node": ">= 18" + "node": ">=14" }, "peerDependencies": { - "@octokit/core": ">=5" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@octokit/plugin-throttling": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-8.2.0.tgz", - "integrity": "sha512-nOpWtLayKFpgqmgD0y3GqXafMFuKcA4tRPZIfu7BArd2lEZeb1988nhWhwx4aZWmjDmUfdgVf7W+Tt4AmvRmMQ==", + "node_modules/@opentelemetry/instrumentation-mongoose": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.46.0.tgz", + "integrity": "sha512-mtVv6UeaaSaWTeZtLo4cx4P5/ING2obSqfWGItIFSunQBrYROfhuVe7wdIrFUs2RH1tn2YYpAJyMaRe/bnTTIQ==", + "license": "Apache-2.0", "dependencies": { - "@octokit/types": "^12.2.0", - "bottleneck": "^2.15.3" + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { - "node": ">= 18" + "node": ">=14" }, "peerDependencies": { - "@octokit/core": "^5.0.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@octokit/request": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", - "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "node_modules/@opentelemetry/instrumentation-mysql": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.45.0.tgz", + "integrity": "sha512-tWWyymgwYcTwZ4t8/rLDfPYbOTF3oYB8SxnYMtIQ1zEf5uDm90Ku3i6U/vhaMyfHNlIHvDhvJh+qx5Nc4Z3Acg==", + "license": "Apache-2.0", "dependencies": { - "@octokit/endpoint": "^9.0.6", - "@octokit/request-error": "^5.1.1", - "@octokit/types": "^13.1.0", - "universal-user-agent": "^6.0.0" + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/mysql": "2.15.26" }, "engines": { - "node": ">= 18" + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@octokit/request-error": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", - "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "node_modules/@opentelemetry/instrumentation-mysql2": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.45.0.tgz", + "integrity": "sha512-qLslv/EPuLj0IXFvcE3b0EqhWI8LKmrgRPIa4gUd8DllbBpqJAvLNJSv3cC6vWwovpbSI3bagNO/3Q2SuXv2xA==", + "license": "Apache-2.0", "dependencies": { - "@octokit/types": "^13.1.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@opentelemetry/sql-common": "^0.40.1" }, "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", - "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" - }, - "node_modules/@octokit/request-error/node_modules/@octokit/types": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", - "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", - "dependencies": { - "@octokit/openapi-types": "^22.2.0" - } - }, - "node_modules/@octokit/request/node_modules/@octokit/openapi-types": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", - "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" - }, - "node_modules/@octokit/request/node_modules/@octokit/types": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", - "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", - "dependencies": { - "@octokit/openapi-types": "^22.2.0" + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@octokit/types": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", - "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "node_modules/@opentelemetry/instrumentation-nestjs-core": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.44.0.tgz", + "integrity": "sha512-t16pQ7A4WYu1yyQJZhRKIfUNvl5PAaF2pEteLvgJb/BWdd1oNuU1rOYt4S825kMy+0q4ngiX281Ss9qiwHfxFQ==", + "license": "Apache-2.0", "dependencies": { - "@octokit/openapi-types": "^20.0.0" + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@octokit/webhooks": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-12.2.0.tgz", - "integrity": "sha512-CyuLJ0/P7bKZ+kIYw+fnkeVdhUzNuDKgNSI7pU/m7Nod0T7kP+s4s2f0pNmG9HL8/RZN1S0ZWTDll3VTMrFLAw==", + "node_modules/@opentelemetry/instrumentation-pg": { + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.50.0.tgz", + "integrity": "sha512-TtLxDdYZmBhFswm8UIsrDjh/HFBeDXd4BLmE8h2MxirNHewLJ0VS9UUddKKEverb5Sm2qFVjqRjcU+8Iw4FJ3w==", + "license": "Apache-2.0", "dependencies": { - "@octokit/request-error": "^5.0.0", - "@octokit/webhooks-methods": "^4.1.0", - "@octokit/webhooks-types": "7.4.0", - "aggregate-error": "^3.1.0" + "@opentelemetry/core": "^1.26.0", + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "1.27.0", + "@opentelemetry/sql-common": "^0.40.1", + "@types/pg": "8.6.1", + "@types/pg-pool": "2.0.6" }, "engines": { - "node": ">= 18" + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@octokit/webhooks-methods": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-4.1.0.tgz", - "integrity": "sha512-zoQyKw8h9STNPqtm28UGOYFE7O6D4Il8VJwhAtMHFt2C4L0VQT1qGKLeefUOqHNs1mNRYSadVv7x0z8U2yyeWQ==", + "node_modules/@opentelemetry/instrumentation-pg/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", + "license": "Apache-2.0", "engines": { - "node": ">= 18" + "node": ">=14" } }, - "node_modules/@octokit/webhooks-types": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.4.0.tgz", - "integrity": "sha512-FE2V+QZ2UYlh+9wWd5BPLNXG+J/XUD/PPq0ovS+nCcGX4+3qVbi3jYOmCTW48hg9SBBLtInx9+o7fFt4H5iP0Q==" - }, - "node_modules/@open-draft/deferred-promise": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", - "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", - "dev": true - }, - "node_modules/@open-draft/logger": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", - "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", - "dev": true, + "node_modules/@opentelemetry/instrumentation-redis-4": { + "version": "0.46.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.46.0.tgz", + "integrity": "sha512-aTUWbzbFMFeRODn3720TZO0tsh/49T8H3h8vVnVKJ+yE36AeW38Uj/8zykQ/9nO8Vrtjr5yKuX3uMiG/W8FKNw==", + "license": "Apache-2.0", "dependencies": { - "is-node-process": "^1.2.0", - "outvariant": "^1.4.0" + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/redis-common": "^0.36.2", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@open-draft/until": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", - "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", - "dev": true - }, - "node_modules/@opentelemetry/api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "node_modules/@opentelemetry/instrumentation-tedious": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.18.0.tgz", + "integrity": "sha512-9zhjDpUDOtD+coeADnYEJQ0IeLVCj7w/hqzIutdp5NqS1VqTAanaEfsEcSypyvYv5DX3YOsTUoF+nr2wDXPETA==", "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/tedious": "^4.0.14" + }, "engines": { - "node": ">=8.0.0" + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/api-logs": { - "version": "0.57.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.57.2.tgz", - "integrity": "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==", + "node_modules/@opentelemetry/instrumentation-undici": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.10.0.tgz", + "integrity": "sha512-vm+V255NGw9gaSsPD6CP0oGo8L55BffBc8KnxqsMuc6XiAD1L8SFNzsW0RHhxJFqy9CJaJh+YiJ5EHXuZ5rZBw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.0" }, "engines": { "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.7.0" } }, - "node_modules/@opentelemetry/context-async-hooks": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz", - "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==", + "node_modules/@opentelemetry/redis-common": { + "version": "0.36.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz", + "integrity": "sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==", "license": "Apache-2.0", "engines": { "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/core": { + "node_modules/@opentelemetry/resources": { "version": "1.30.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz", - "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz", + "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==", "license": "Apache-2.0", "dependencies": { + "@opentelemetry/core": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" }, "engines": { @@ -2737,7 +2590,7 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/core/node_modules/@opentelemetry/semantic-conventions": { + "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/semantic-conventions": { "version": "1.28.0", "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", @@ -2746,94 +2599,97 @@ "node": ">=14" } }, - "node_modules/@opentelemetry/instrumentation": { - "version": "0.57.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz", - "integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==", + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz", + "integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.57.2", - "@types/shimmer": "^1.2.0", - "import-in-the-middle": "^1.8.1", - "require-in-the-middle": "^7.1.1", - "semver": "^7.5.2", - "shimmer": "^1.2.1" + "@opentelemetry/core": "1.30.1", + "@opentelemetry/resources": "1.30.1", + "@opentelemetry/semantic-conventions": "1.28.0" }, "engines": { "node": ">=14" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/instrumentation-amqplib": { - "version": "0.46.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.46.1.tgz", - "integrity": "sha512-AyXVnlCf/xV3K/rNumzKxZqsULyITJH6OVLiW6730JPRqWA7Zc9bvYoVNpN6iOpTU8CasH34SU/ksVJmObFibQ==", + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, "engines": { "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-connect": { - "version": "0.43.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.43.0.tgz", - "integrity": "sha512-Q57JGpH6T4dkYHo9tKXONgLtxzsh1ZEW5M9A/OwKrZFyEpLqWgjhcZ3hIuVvDlhb426iDF1f9FPToV/mi5rpeA==", + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", + "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/sql-common": { + "version": "0.40.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.40.1.tgz", + "integrity": "sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.0", - "@opentelemetry/semantic-conventions": "^1.27.0", - "@types/connect": "3.4.36" + "@opentelemetry/core": "^1.1.0" }, "engines": { "node": ">=14" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": "^1.1.0" } }, - "node_modules/@opentelemetry/instrumentation-connect/node_modules/@types/connect": { - "version": "3.4.36", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz", - "integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==", - "license": "MIT", + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@prisma/instrumentation": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-5.22.0.tgz", + "integrity": "sha512-LxccF392NN37ISGxIurUljZSh1YWnphO34V5a0+T7FVQG2u9bhAXRTJpgmQ3483woVhkraQZFF7cbRrpbw/F4Q==", + "license": "Apache-2.0", "dependencies": { - "@types/node": "*" + "@opentelemetry/api": "^1.8", + "@opentelemetry/instrumentation": "^0.49 || ^0.50 || ^0.51 || ^0.52.0 || ^0.53.0", + "@opentelemetry/sdk-trace-base": "^1.22" } }, - "node_modules/@opentelemetry/instrumentation-dataloader": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.16.0.tgz", - "integrity": "sha512-88+qCHZC02up8PwKHk0UQKLLqGGURzS3hFQBZC7PnGwReuoKjHXS1o29H58S+QkXJpkTr2GACbx8j6mUoGjNPA==", + "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/api-logs": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz", + "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0" + "@opentelemetry/api": "^1.0.0" }, "engines": { "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-express": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.47.0.tgz", - "integrity": "sha512-XFWVx6k0XlU8lu6cBlCa29ONtVt6ADEjmxtyAyeF2+rifk8uBJbk1La0yIVfI0DoKURGbaEDTNelaXG9l/lNNQ==", + "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/instrumentation": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz", + "integrity": "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.0", - "@opentelemetry/semantic-conventions": "^1.27.0" + "@opentelemetry/api-logs": "0.53.0", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" }, "engines": { "node": ">=14" @@ -2842,553 +2698,742 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-fastify": { - "version": "0.44.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.44.1.tgz", - "integrity": "sha512-RoVeMGKcNttNfXMSl6W4fsYoCAYP1vi6ZAWIGhBY+o7R9Y0afA7f9JJL0j8LHbyb0P0QhSYk+6O56OwI2k4iRQ==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@probot/adapter-aws-lambda-serverless/-/adapter-aws-lambda-serverless-4.0.3.tgz", + "integrity": "sha512-0KGFM4TSqcyG3ol9m6Ybgo1MgZmVv+TAD5LYrBkM2X9PFQegf/UDdl9APB9MQjeaFxMFjrbtewLjBPp7TQ4WhQ==", + "license": "ISC", "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.0", - "@opentelemetry/semantic-conventions": "^1.27.0" + "@types/aws-lambda": "^8.10.147", + "lowercase-keys": "^2.0.0", + "probot": "^13.4.1" + } + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/auth-app": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-6.1.4.tgz", + "integrity": "sha512-QkXkSOHZK4dA5oUqY5Dk3S+5pN2s1igPjEASNQV8/vgJgW034fQWR16u7VsNOK/EljA00eyjYF5mWNxWKWhHRQ==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^7.1.0", + "@octokit/auth-oauth-user": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.1.0", + "deprecation": "^2.3.1", + "lru-cache": "npm:@wolfy1339/lru-cache@^11.0.2-patch.1", + "universal-github-app-jwt": "^1.1.2", + "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": ">= 18" } }, - "node_modules/@opentelemetry/instrumentation-fs": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.19.0.tgz", - "integrity": "sha512-JGwmHhBkRT2G/BYNV1aGI+bBjJu4fJUD/5/Jat0EWZa2ftrLV3YE8z84Fiij/wK32oMZ88eS8DI4ecLGZhpqsQ==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/auth-app/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/auth-app/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.0" + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/auth-oauth-app": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-7.1.0.tgz", + "integrity": "sha512-w+SyJN/b0l/HEb4EOPRudo7uUOSW51jcK1jwLa+4r7PA8FPFpoxEnHBHMITqCsc/3Vo2qqFjgQfz/xUUvsSQnA==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^6.1.0", + "@octokit/auth-oauth-user": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "@types/btoa-lite": "^1.0.0", + "btoa-lite": "^1.0.0", + "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": ">= 18" } }, - "node_modules/@opentelemetry/instrumentation-generic-pool": { - "version": "0.43.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.43.0.tgz", - "integrity": "sha512-at8GceTtNxD1NfFKGAuwtqM41ot/TpcLh+YsGe4dhf7gvv1HW/ZWdq6nfRtS6UjIvZJOokViqLPJ3GVtZItAnQ==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/auth-oauth-app/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/auth-oauth-app/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0" + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/auth-oauth-device": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-6.1.0.tgz", + "integrity": "sha512-FNQ7cb8kASufd6Ej4gnJ3f1QB5vJitkoV1O0/g6e6lUsQ7+VsSNRHRmFScN2tV4IgKA12frrr/cegUs0t+0/Lw==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-methods": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": ">= 18" } }, - "node_modules/@opentelemetry/instrumentation-graphql": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.47.0.tgz", - "integrity": "sha512-Cc8SMf+nLqp0fi8oAnooNEfwZWFnzMiBHCGmDFYqmgjPylyLmi83b+NiTns/rKGwlErpW0AGPt0sMpkbNlzn8w==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/auth-oauth-device/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/auth-oauth-device/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0" + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/auth-oauth-user": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-4.1.0.tgz", + "integrity": "sha512-FrEp8mtFuS/BrJyjpur+4GARteUCrPeR/tZJzD8YourzoVhRics7u7we/aDcKv+yywRNwNi/P4fRi631rG/OyQ==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^6.1.0", + "@octokit/oauth-methods": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "btoa-lite": "^1.0.0", + "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": ">= 18" } }, - "node_modules/@opentelemetry/instrumentation-hapi": { - "version": "0.45.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.45.1.tgz", - "integrity": "sha512-VH6mU3YqAKTePPfUPwfq4/xr049774qWtfTuJqVHoVspCLiT3bW+fCQ1toZxt6cxRPYASoYaBsMA3CWo8B8rcw==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/auth-oauth-user/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/auth-oauth-user/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.0", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "license": "MIT", "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": ">= 18" } }, - "node_modules/@opentelemetry/instrumentation-http": { - "version": "0.57.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.57.1.tgz", - "integrity": "sha512-ThLmzAQDs7b/tdKI3BV2+yawuF09jF111OFsovqT1Qj3D8vjwKBwhi/rDE5xethwn4tSXtZcJ9hBsVAlWFQZ7g==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/auth-unauthenticated": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-5.0.1.tgz", + "integrity": "sha512-oxeWzmBFxWd+XolxKTc4zr+h3mt+yofn4r7OfoIkR/Cj/o70eEGmPsFbueyJE2iBAGpjgTnEOKM3pnuEGVmiqg==", + "license": "MIT", "dependencies": { - "@opentelemetry/core": "1.30.1", - "@opentelemetry/instrumentation": "0.57.1", - "@opentelemetry/semantic-conventions": "1.28.0", - "forwarded-parse": "2.1.2", - "semver": "^7.5.2" + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0" }, "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": ">= 18" } }, - "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/api-logs": { - "version": "0.57.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.57.1.tgz", - "integrity": "sha512-I4PHczeujhQAQv6ZBzqHYEUiggZL4IdSMixtVD3EYqbdrjujE7kRfI5QohjlPoJm8BvenoW5YaTMWRrbpot6tg==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", + "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", + "license": "MIT", "dependencies": { - "@opentelemetry/api": "^1.3.0" + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">=14" + "node": ">= 18" } }, - "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/instrumentation": { - "version": "0.57.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.1.tgz", - "integrity": "sha512-SgHEKXoVxOjc20ZYusPG3Fh+RLIZTSa4x8QtD3NfgAUDyqdFFS9W1F2ZVbZkqDCdyMcQG02Ok4duUGLHJXHgbA==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/core/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/core/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", "dependencies": { - "@opentelemetry/api-logs": "0.57.1", - "@types/shimmer": "^1.2.0", - "import-in-the-middle": "^1.8.1", - "require-in-the-middle": "^7.1.1", - "semver": "^7.5.2", - "shimmer": "^1.2.1" + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": ">= 18" } }, - "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", - "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/endpoint/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" } }, - "node_modules/@opentelemetry/instrumentation-ioredis": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.47.0.tgz", - "integrity": "sha512-4HqP9IBC8e7pW9p90P3q4ox0XlbLGme65YTrA3UTLvqvo4Z6b0puqZQP203YFu8m9rE/luLfaG7/xrwwqMUpJw==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "license": "MIT", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", - "@opentelemetry/redis-common": "^0.36.2", - "@opentelemetry/semantic-conventions": "^1.27.0" + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": ">= 18" } }, - "node_modules/@opentelemetry/instrumentation-kafkajs": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.7.0.tgz", - "integrity": "sha512-LB+3xiNzc034zHfCtgs4ITWhq6Xvdo8bsq7amR058jZlf2aXXDrN9SV4si4z2ya9QX4tz6r4eZJwDkXOp14/AQ==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/graphql/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/oauth-authorization-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-6.0.2.tgz", + "integrity": "sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==", + "license": "MIT", "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": ">= 18" } }, - "node_modules/@opentelemetry/instrumentation-knex": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.44.0.tgz", - "integrity": "sha512-SlT0+bLA0Lg3VthGje+bSZatlGHw/vwgQywx0R/5u9QC59FddTQSPJeWNw29M6f8ScORMeUOOTwihlQAn4GkJQ==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/oauth-methods": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-4.1.0.tgz", + "integrity": "sha512-4tuKnCRecJ6CG6gr0XcEXdZtkTDbfbnD5oaHBmLERTjTMZNi2CbfEHZxPU41xXLDG4DfKf+sonu00zvKI9NSbw==", + "license": "MIT", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", - "@opentelemetry/semantic-conventions": "^1.27.0" + "@octokit/oauth-authorization-url": "^6.0.2", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.0.0", + "btoa-lite": "^1.0.0" }, "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": ">= 18" } }, - "node_modules/@opentelemetry/instrumentation-koa": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.47.0.tgz", - "integrity": "sha512-HFdvqf2+w8sWOuwtEXayGzdZ2vWpCKEQv5F7+2DSA74Te/Cv4rvb2E5So5/lh+ok4/RAIPuvCbCb/SHQFzMmbw==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/oauth-methods/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/oauth-methods/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.0", - "@opentelemetry/semantic-conventions": "^1.27.0" + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "license": "MIT" + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/plugin-enterprise-compatibility": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-enterprise-compatibility/-/plugin-enterprise-compatibility-4.1.0.tgz", + "integrity": "sha512-a8QehVu9Iy4k+m2XgG2rrF4m9vhlRIaefOMr0yJzgQCt4KpiTj5mZVrzSwagyOovkJdD0yDolQazBQZqPWTFSQ==", + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0" }, "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": ">= 18" } }, - "node_modules/@opentelemetry/instrumentation-lru-memoizer": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.44.0.tgz", - "integrity": "sha512-Tn7emHAlvYDFik3vGU0mdwvWJDwtITtkJ+5eT2cUquct6nIs+H8M47sqMJkCpyPe5QIBJoTOHxmc6mj9lz6zDw==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/plugin-paginate-rest": { + "version": "11.4.4-cjs.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.4-cjs.2.tgz", + "integrity": "sha512-2dK6z8fhs8lla5PaOTgqfCGBxgAv/le+EhPs27KklPhm1bKObpu6lXzwfUEQ16ajXzqNrKMujsFyo9K2eaoISw==", + "license": "MIT", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0" + "@octokit/types": "^13.7.0" }, "engines": { - "node": ">=14" + "node": ">= 18" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@octokit/core": "5" } }, - "node_modules/@opentelemetry/instrumentation-mongodb": { - "version": "0.51.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.51.0.tgz", - "integrity": "sha512-cMKASxCX4aFxesoj3WK8uoQ0YUrRvnfxaO72QWI2xLu5ZtgX/QvdGBlU3Ehdond5eb74c2s1cqRQUIptBnKz1g==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@octokit/openapi-types": "^24.2.0" } }, - "node_modules/@opentelemetry/instrumentation-mongoose": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.46.0.tgz", - "integrity": "sha512-mtVv6UeaaSaWTeZtLo4cx4P5/ING2obSqfWGItIFSunQBrYROfhuVe7wdIrFUs2RH1tn2YYpAJyMaRe/bnTTIQ==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "13.3.2-cjs.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.3.2-cjs.1.tgz", + "integrity": "sha512-VUjIjOOvF2oELQmiFpWA1aOPdawpyaCUqcEBc/UOUnj3Xp6DJGrJ1+bjUIIDzdHjnFNO6q57ODMfdEZnoBkCwQ==", + "license": "MIT", "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.0", - "@opentelemetry/semantic-conventions": "^1.27.0" + "@octokit/types": "^13.8.0" }, "engines": { - "node": ">=14" + "node": ">= 18" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@octokit/core": "^5" } }, - "node_modules/@opentelemetry/instrumentation-mysql": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.45.0.tgz", - "integrity": "sha512-tWWyymgwYcTwZ4t8/rLDfPYbOTF3oYB8SxnYMtIQ1zEf5uDm90Ku3i6U/vhaMyfHNlIHvDhvJh+qx5Nc4Z3Acg==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", - "@opentelemetry/semantic-conventions": "^1.27.0", - "@types/mysql": "2.15.26" + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/plugin-retry": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.1.0.tgz", + "integrity": "sha512-WrO3bvq4E1Xh1r2mT9w6SDFg01gFmP81nIG77+p/MqW1JeXXgL++6umim3t6x0Zj5pZm3rXAN+0HEjmmdhIRig==", + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^13.0.0", + "bottleneck": "^2.15.3" }, "engines": { - "node": ">=14" + "node": ">= 18" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@octokit/core": "5" } }, - "node_modules/@opentelemetry/instrumentation-mysql2": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.45.0.tgz", - "integrity": "sha512-qLslv/EPuLj0IXFvcE3b0EqhWI8LKmrgRPIa4gUd8DllbBpqJAvLNJSv3cC6vWwovpbSI3bagNO/3Q2SuXv2xA==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/plugin-retry/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/plugin-retry/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", - "@opentelemetry/semantic-conventions": "^1.27.0", - "@opentelemetry/sql-common": "^0.40.1" + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/plugin-throttling": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-8.2.0.tgz", + "integrity": "sha512-nOpWtLayKFpgqmgD0y3GqXafMFuKcA4tRPZIfu7BArd2lEZeb1988nhWhwx4aZWmjDmUfdgVf7W+Tt4AmvRmMQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.2.0", + "bottleneck": "^2.15.3" }, "engines": { - "node": ">=14" + "node": ">= 18" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@octokit/core": "^5.0.0" } }, - "node_modules/@opentelemetry/instrumentation-nestjs-core": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.44.0.tgz", - "integrity": "sha512-t16pQ7A4WYu1yyQJZhRKIfUNvl5PAaF2pEteLvgJb/BWdd1oNuU1rOYt4S825kMy+0q4ngiX281Ss9qiwHfxFQ==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "license": "MIT", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", - "@opentelemetry/semantic-conventions": "^1.27.0" + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": ">= 18" } }, - "node_modules/@opentelemetry/instrumentation-pg": { - "version": "0.50.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.50.0.tgz", - "integrity": "sha512-TtLxDdYZmBhFswm8UIsrDjh/HFBeDXd4BLmE8h2MxirNHewLJ0VS9UUddKKEverb5Sm2qFVjqRjcU+8Iw4FJ3w==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "license": "MIT", "dependencies": { - "@opentelemetry/core": "^1.26.0", - "@opentelemetry/instrumentation": "^0.57.0", - "@opentelemetry/semantic-conventions": "1.27.0", - "@opentelemetry/sql-common": "^0.40.1", - "@types/pg": "8.6.1", - "@types/pg-pool": "2.0.6" + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" }, "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": ">= 18" } }, - "node_modules/@opentelemetry/instrumentation-pg/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", - "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/request-error/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" } }, - "node_modules/@opentelemetry/instrumentation-redis-4": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.46.0.tgz", - "integrity": "sha512-aTUWbzbFMFeRODn3720TZO0tsh/49T8H3h8vVnVKJ+yE36AeW38Uj/8zykQ/9nO8Vrtjr5yKuX3uMiG/W8FKNw==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/request/node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/request/node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", - "@opentelemetry/redis-common": "^0.36.2", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@octokit/openapi-types": "^24.2.0" } }, - "node_modules/@opentelemetry/instrumentation-tedious": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.18.0.tgz", - "integrity": "sha512-9zhjDpUDOtD+coeADnYEJQ0IeLVCj7w/hqzIutdp5NqS1VqTAanaEfsEcSypyvYv5DX3YOsTUoF+nr2wDXPETA==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "license": "MIT", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", - "@opentelemetry/semantic-conventions": "^1.27.0", - "@types/tedious": "^4.0.14" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@octokit/openapi-types": "^20.0.0" } }, - "node_modules/@opentelemetry/instrumentation-undici": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.10.0.tgz", - "integrity": "sha512-vm+V255NGw9gaSsPD6CP0oGo8L55BffBc8KnxqsMuc6XiAD1L8SFNzsW0RHhxJFqy9CJaJh+YiJ5EHXuZ5rZBw==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/webhooks": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-12.3.2.tgz", + "integrity": "sha512-exj1MzVXoP7xnAcAB3jZ97pTvVPkQF9y6GA/dvYC47HV7vLv+24XRS6b/v/XnyikpEuvMhugEXdGtAlU086WkQ==", + "license": "MIT", "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.0" + "@octokit/request-error": "^5.0.0", + "@octokit/webhooks-methods": "^4.1.0", + "@octokit/webhooks-types": "7.6.1", + "aggregate-error": "^3.1.0" }, "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.7.0" + "node": ">= 18" } }, - "node_modules/@opentelemetry/redis-common": { - "version": "0.36.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz", - "integrity": "sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@octokit/webhooks-methods": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-4.1.0.tgz", + "integrity": "sha512-zoQyKw8h9STNPqtm28UGOYFE7O6D4Il8VJwhAtMHFt2C4L0VQT1qGKLeefUOqHNs1mNRYSadVv7x0z8U2yyeWQ==", + "license": "MIT", "engines": { - "node": ">=14" + "node": ">= 18" } }, - "node_modules/@opentelemetry/resources": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz", - "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@probot/get-private-key": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@probot/get-private-key/-/get-private-key-1.2.1.tgz", + "integrity": "sha512-ejh5fK4d0zJES+3aR2IiD8+afuzgpe4Nr3UzTSKbBVU8+u8R/nVCQJawJ5s3QilzgXFftjYkGxZFvXkZ0+u4wA==", + "license": "ISC" + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@probot/octokit-plugin-config": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@probot/octokit-plugin-config/-/octokit-plugin-config-2.0.1.tgz", + "integrity": "sha512-aWQYzPY2xiKscTVTKveghtbglqZ+W4eBLIdK1C/cNiFIofy3AxKogWgEZj29PjIe5ZRYx0sRHAPc/pkcXyOmTQ==", + "license": "MIT", "dependencies": { - "@opentelemetry/core": "1.30.1", - "@opentelemetry/semantic-conventions": "1.28.0" + "js-yaml": "^4.1.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", - "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" + "@octokit/core": ">=5" } }, - "node_modules/@opentelemetry/sdk-trace-base": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz", - "integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/@probot/pino": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@probot/pino/-/pino-3.0.0.tgz", + "integrity": "sha512-2pLi8gIzDI5NEbu24SOBXqflTc3o95StJE6+jiw3R3n4i3B5hCJjlzCB2j3OETeohtCTuDSc//GuGPyDfZehWg==", + "license": "MIT", "dependencies": { - "@opentelemetry/core": "1.30.1", - "@opentelemetry/resources": "1.30.1", - "@opentelemetry/semantic-conventions": "1.28.0" + "@sentry/node": "^8.0.0", + "pino-pretty": "^13.0.0", + "pump": "^3.0.0", + "split2": "^4.0.0" }, - "engines": { - "node": ">=14" + "bin": { + "pino-probot": "bin/cli.js" }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "engines": { + "node": ">=18" } }, - "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", - "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "license": "Apache-2.0" + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", "engines": { - "node": ">=14" + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, - "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.30.0.tgz", - "integrity": "sha512-4VlGgo32k2EQ2wcCY3vEU28A0O13aOtHz3Xt2/2U5FAh9EfhD6t6DqL5Z6yAnRCntbTFDU4YfbpyzSlHNWycPw==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/lru-cache": { + "name": "@wolfy1339/lru-cache", + "version": "11.0.2-patch.1", + "resolved": "https://registry.npmjs.org/@wolfy1339/lru-cache/-/lru-cache-11.0.2-patch.1.tgz", + "integrity": "sha512-BgYZfL2ADCXKOw2wJtkM3slhHotawWkgIRRxq4wEybnZQPjvAp71SPX35xepMykTw8gXlzWcWPTY31hlbnRsDA==", + "license": "ISC", "engines": { - "node": ">=14" + "node": "18 >=18.20 || 20 || >=22" } }, - "node_modules/@opentelemetry/sql-common": { - "version": "0.40.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.40.1.tgz", - "integrity": "sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/octokit-auth-probot": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/octokit-auth-probot/-/octokit-auth-probot-2.0.2.tgz", + "integrity": "sha512-/6eQQZ7CpGXRiczT4BoGcRZCixczJKjb5anfZbC+bkN1B4oyayvarwR6FrklRLuC81izHqgvbdX7E/F+0Iw+tw==", + "license": "ISC", "dependencies": { - "@opentelemetry/core": "^1.1.0" + "@octokit/auth-app": "^6.1.4", + "@octokit/auth-token": "^4.0.0", + "@octokit/auth-unauthenticated": "^5.0.1", + "@octokit/types": "^12.0.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "@opentelemetry/api": "^1.1.0" + "@octokit/core": ">=5" } }, - "node_modules/@prisma/instrumentation": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-5.22.0.tgz", - "integrity": "sha512-LxccF392NN37ISGxIurUljZSh1YWnphO34V5a0+T7FVQG2u9bhAXRTJpgmQ3483woVhkraQZFF7cbRrpbw/F4Q==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", "dependencies": { - "@opentelemetry/api": "^1.8", - "@opentelemetry/instrumentation": "^0.49 || ^0.50 || ^0.51 || ^0.52.0 || ^0.53.0", - "@opentelemetry/sdk-trace-base": "^1.22" + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" } }, - "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/api-logs": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz", - "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", "dependencies": { - "@opentelemetry/api": "^1.0.0" - }, - "engines": { - "node": ">=14" + "split2": "^4.0.0" } }, - "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/instrumentation": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz", - "integrity": "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==", - "license": "Apache-2.0", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/pino-http": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-10.5.0.tgz", + "integrity": "sha512-hD91XjgaKkSsdn8P7LaebrNzhGTdB086W3pyPihX0EzGPjq5uBJBXo4N5guqNaK6mUjg9aubMF7wDViYek9dRA==", + "license": "MIT", "dependencies": { - "@opentelemetry/api-logs": "0.53.0", - "@types/shimmer": "^1.2.0", - "import-in-the-middle": "^1.8.1", - "require-in-the-middle": "^7.1.1", - "semver": "^7.5.2", - "shimmer": "^1.2.1" + "get-caller-file": "^2.0.5", + "pino": "^9.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0" + } + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/probot": { + "version": "13.4.7", + "resolved": "https://registry.npmjs.org/probot/-/probot-13.4.7.tgz", + "integrity": "sha512-/NjxDJ/c/TXLeNs0TRpmYQ825sDQFc/BFJl2vgId2GX/rwS8HcD1dUWpMowvugEWcA6nCgMGJPNSFwKv5B4TZw==", + "license": "ISC", + "dependencies": { + "@octokit/core": "^5.2.2", + "@octokit/plugin-enterprise-compatibility": "^4.0.1", + "@octokit/plugin-paginate-rest": "11.4.4-cjs.2", + "@octokit/plugin-rest-endpoint-methods": "13.3.2-cjs.1", + "@octokit/plugin-retry": "^6.0.1", + "@octokit/plugin-throttling": "^8.1.3", + "@octokit/request": "^8.4.1", + "@octokit/types": "^12.3.0", + "@octokit/webhooks": "^12.3.2", + "@probot/get-private-key": "^1.1.2", + "@probot/octokit-plugin-config": "^2.0.1", + "@probot/pino": "^3.0.0", + "@types/express": "^5.0.1", + "bottleneck": "^2.19.5", + "commander": "^12.0.0", + "deepmerge": "^4.3.1", + "dotenv": "^16.3.1", + "express": "^5.0.0", + "ioredis": "^5.3.2", + "js-yaml": "^4.1.0", + "lru-cache": "npm:@wolfy1339/lru-cache@^11.0.2-patch.1", + "octokit-auth-probot": "^2.0.2", + "pino": "^9.0.0", + "pino-http": "^10.0.0", + "pkg-conf": "^3.1.0", + "update-dotenv": "^1.1.1" }, - "engines": { - "node": ">=14" + "bin": { + "probot": "bin/probot.js" }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "engines": { + "node": ">=18" } }, - "node_modules/@probot/adapter-aws-lambda-serverless": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@probot/adapter-aws-lambda-serverless/-/adapter-aws-lambda-serverless-4.0.3.tgz", - "integrity": "sha512-0KGFM4TSqcyG3ol9m6Ybgo1MgZmVv+TAD5LYrBkM2X9PFQegf/UDdl9APB9MQjeaFxMFjrbtewLjBPp7TQ4WhQ==", + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", "dependencies": { - "@types/aws-lambda": "^8.10.147", - "lowercase-keys": "^2.0.0", - "probot": "^13.4.1" + "real-require": "^0.2.0" + } + }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/universal-github-app-jwt": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-1.2.0.tgz", + "integrity": "sha512-dncpMpnsKBk0eetwfN8D8OUHGfiDhhJ+mtsbMl+7PfW7mYjiH8LIcqRmYMtzYLgSh47HjfdBtrBwIQ/gizKR3g==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "^9.0.0", + "jsonwebtoken": "^9.0.2" } }, + "node_modules/@probot/adapter-aws-lambda-serverless/node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "license": "ISC" + }, "node_modules/@probot/get-private-key": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@probot/get-private-key/-/get-private-key-1.1.2.tgz", - "integrity": "sha512-yVgyCdTyooGX6+czDLkJahEcwgBWZsKH9xbjvjDNVFjY3QtiI/tHRiB3zjgJCQMZehXxv2CFHZQSpWRXdr6CeQ==" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@probot/get-private-key/-/get-private-key-2.1.1.tgz", + "integrity": "sha512-i20LJE0751wWDCiTQmyS6UM65vNWOZMim/DAxIFQWfoPJSBPdFPgNffG1XbTiMfl4et8JCBZWab9ldxDtzhcWw==", + "license": "ISC", + "engines": { + "node": ">= 18" + } }, "node_modules/@probot/octokit-plugin-config": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@probot/octokit-plugin-config/-/octokit-plugin-config-2.0.1.tgz", - "integrity": "sha512-aWQYzPY2xiKscTVTKveghtbglqZ+W4eBLIdK1C/cNiFIofy3AxKogWgEZj29PjIe5ZRYx0sRHAPc/pkcXyOmTQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@probot/octokit-plugin-config/-/octokit-plugin-config-4.1.0.tgz", + "integrity": "sha512-G4uzWzLPBX6EKs6DgabT+ZsTas1OAxRQhaXeLArm6gJl5RVAskfrZ8VEQJNj7XUI91KvayM9C42hMIbrnstBkQ==", "license": "MIT", "dependencies": { - "js-yaml": "^4.1.0" + "yaml": "^2.8.1" }, "engines": { - "node": ">=18" + "node": "^20.18.1 || >= 22" }, "peerDependencies": { - "@octokit/core": ">=5" + "@octokit/core": ">=7" } }, "node_modules/@probot/pino": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@probot/pino/-/pino-3.0.0.tgz", - "integrity": "sha512-2pLi8gIzDI5NEbu24SOBXqflTc3o95StJE6+jiw3R3n4i3B5hCJjlzCB2j3OETeohtCTuDSc//GuGPyDfZehWg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@probot/pino/-/pino-5.0.1.tgz", + "integrity": "sha512-ULNKXY9jewO1/BHME3ZmsP3yHYocJBErT5D5fWxaQnoHNS635mTqBI2NszIGZO/56/i/lRFm+J9gp2NOkM6yjQ==", "license": "MIT", "dependencies": { - "@sentry/node": "^8.0.0", + "npx-import-light": "^1.0.0", "pino-pretty": "^13.0.0", "pump": "^3.0.0", "split2": "^4.0.0" @@ -3483,16 +3528,18 @@ } }, "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" } @@ -3502,14 +3549,15 @@ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "node_modules/@travi/any": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@travi/any/-/any-3.1.2.tgz", - "integrity": "sha512-Sg7+3Vw9Nttm24LnRbxdgWDmaNngEkR0cAXDqBRHsv8E2tSmibyQ8YYWvaQC7cTG2JM8RgGGZa4ZbZ8iX9gxhg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@travi/any/-/any-3.1.3.tgz", + "integrity": "sha512-cEMd6zcR+1Ubl9mGJrg5cY9i06Qi3RJG3OBOacpb9j1/c5wcWAGVnrHAqgI1zPLPLTYggfSLwH09UMkg+MkMng==", "dev": true, "license": "MIT", "dependencies": { @@ -3527,15 +3575,17 @@ } }, "node_modules/@types/aws-lambda": { - "version": "8.10.147", - "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.147.tgz", - "integrity": "sha512-nD0Z9fNIZcxYX5Mai2CTmFD7wX7UldCkW2ezCF8D1T5hdiLsnTWDGRpfRYntU6VjTdLQjOvyszru7I1c1oCQew==" + "version": "8.10.161", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.161.tgz", + "integrity": "sha512-rUYdp+MQwSFocxIOcSsYSF3YYYC/uUpMbCY/mbO21vGqfrEYvNSoPyKYDj6RhXXpPfS0KstW9RwG3qXh9sL7FQ==", + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -3545,10 +3595,11 @@ } }, "node_modules/@types/babel__generator": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" } @@ -3558,24 +3609,27 @@ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, + "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__traverse": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", - "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.20.7" + "@babel/types": "^7.28.2" } }, "node_modules/@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -3588,9 +3642,9 @@ "license": "MIT" }, "node_modules/@types/chance": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@types/chance/-/chance-1.1.6.tgz", - "integrity": "sha512-V+pm3stv1Mvz8fSKJJod6CglNGVqEQ6OyuqitoDkWywEODM/eJd1eSuIp9xt6DrX8BWZ2eDSIzbw1tPCUTvGbQ==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@types/chance/-/chance-1.1.7.tgz", + "integrity": "sha512-40you9610GTQPJyvjMBgmj9wiDO6qXhbfjizNYod/fmvLSfUUxURAJMTD8tjmbcZSsyYE5iEUox61AAcCjW/wQ==", "dev": true, "license": "MIT" }, @@ -3598,25 +3652,27 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "license": "MIT", "dependencies": { "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz", - "integrity": "sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "license": "MIT", "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -3629,26 +3685,30 @@ "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "dev": true, + "license": "MIT", "dependencies": { "@types/istanbul-lib-coverage": "*" } @@ -3658,6 +3718,7 @@ "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/istanbul-lib-report": "*" } @@ -3666,29 +3727,26 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/jsonwebtoken": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", - "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", "license": "MIT", "dependencies": { "@types/ms": "*", "@types/node": "*" } }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" - }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -3705,11 +3763,12 @@ } }, "node_modules/@types/node": { - "version": "20.12.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.8.tgz", - "integrity": "sha512-NU0rJLJnshZWdE/097cdCBbyW1h4hEg0xpovcoAQYHl8dnEyp/NAOiE45pvc+Bd1Dt+2r94v2eGFpQJ4R7g+2w==", + "version": "25.3.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz", + "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==", + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~7.18.0" } }, "node_modules/@types/pg": { @@ -3733,32 +3792,34 @@ } }, "node_modules/@types/qs": { - "version": "6.9.15", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", - "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==" + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" }, "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", "dependencies": { - "@types/mime": "^1", "@types/node": "*" } }, "node_modules/@types/serve-static": { - "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "license": "MIT", "dependencies": { "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" + "@types/node": "*" } }, "node_modules/@types/shimmer": { @@ -3771,7 +3832,8 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/tedious": { "version": "4.0.14", @@ -3783,10 +3845,11 @@ } }, "node_modules/@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", "dev": true, + "license": "MIT", "dependencies": { "@types/yargs-parser": "*" } @@ -3795,19 +3858,22 @@ "version": "21.0.3", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" }, "node_modules/@yarnpkg/parsers": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.0.tgz", - "integrity": "sha512-jVZa3njBv6tcOUw34nlUdUM/40wwtm/gnVF8rtk0tA6vNcokqYI8CFU1BZjlpFwUSZaXxYkrtuPE/f2MMFlTxQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.3.tgz", + "integrity": "sha512-mQZgUSgFurUtA07ceMjxrWkYz8QtDuYkvPlu0ZqncgjopQ0t6CNEo/OSealkmnagSUx8ZD5ewvezUwUuMqutQg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "js-yaml": "^3.10.0", "tslib": "^2.4.0" @@ -3821,14 +3887,15 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, + "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" } }, "node_modules/@yarnpkg/parsers/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", "dev": true, "dependencies": { "argparse": "^1.0.7", @@ -3838,28 +3905,23 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { "node": ">= 0.6" } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -3882,23 +3944,16 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" @@ -3908,10 +3963,11 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3928,6 +3984,7 @@ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.21.3" }, @@ -3938,23 +3995,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3964,6 +4010,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^1.9.0" }, @@ -3976,6 +4023,7 @@ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -3987,13 +4035,15 @@ "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==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" }, "node_modules/array-back": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -4015,23 +4065,21 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -4045,6 +4093,7 @@ "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -4061,17 +4110,19 @@ } }, "node_modules/array.prototype.findlastindex": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", - "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", + "es-abstract": "^1.23.9", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -4081,15 +4132,16 @@ } }, "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4156,11 +4208,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } }, "node_modules/atomic-sleep": { "version": "1.0.0", @@ -4192,6 +4248,7 @@ "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", "dev": true, + "license": "MIT", "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", @@ -4213,6 +4270,7 @@ "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" }, @@ -4228,6 +4286,7 @@ "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" @@ -4244,6 +4303,7 @@ "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" }, @@ -4255,13 +4315,15 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/babel-jest/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" } @@ -4271,6 +4333,7 @@ "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" }, @@ -4283,6 +4346,7 @@ "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", @@ -4299,6 +4363,7 @@ "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", @@ -4315,6 +4380,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -4324,6 +4390,7 @@ "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", @@ -4335,26 +4402,30 @@ } }, "node_modules/babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "node_modules/babel-preset-jest": { @@ -4362,6 +4433,7 @@ "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", "dev": true, + "license": "MIT", "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" @@ -4374,21 +4446,39 @@ } }, "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==", - "license": "MIT" + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } }, "node_modules/before-after-hook": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", - "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "license": "Apache-2.0" }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -4400,56 +4490,49 @@ "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/bottleneck": { "version": "2.19.5", "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", - "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==" + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", + "license": "MIT" }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -4457,6 +4540,7 @@ "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" }, @@ -4465,9 +4549,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -4483,11 +4567,13 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -4501,6 +4587,7 @@ "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "node-int64": "^0.4.0" } @@ -4521,13 +4608,15 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=6" @@ -4541,6 +4630,7 @@ "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz", "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==", "dev": true, + "license": "MIT", "dependencies": { "semver": "^7.0.0" } @@ -4549,6 +4639,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -4573,9 +4664,9 @@ } }, "node_modules/call-bind-apply-helpers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", - "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4586,13 +4677,13 @@ } }, "node_modules/call-bound": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", - "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "get-intrinsic": "^1.2.6" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -4606,6 +4697,7 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -4615,14 +4707,15 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001615", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001615.tgz", - "integrity": "sha512-1IpazM5G3r38meiae0bHRnPhz+CBQ3ZLqbQMtrg+AsTPKAXgW38JNsXkyZ+v8waCsDmPq87lmfun5Q2AGysNEQ==", + "version": "1.0.30001775", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz", + "integrity": "sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==", "dev": true, "funding": [ { @@ -4637,13 +4730,15 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -4654,16 +4749,18 @@ } }, "node_modules/chance": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/chance/-/chance-1.1.11.tgz", - "integrity": "sha512-kqTg3WWywappJPqtgrdvbA380VoXO2eu9VCV895JgbyHsaErXdyHK9LOZ911OvAk6L0obK7kDk9CGs8+oBawVA==", - "dev": true + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/chance/-/chance-1.1.13.tgz", + "integrity": "sha512-V6lQCljcLznE7tUYUM9EOAnnKXbctE6j/rdQkYOHIWbfGQbrzTsAXNW9CdU5XCo4ArXQCj/rb6HgxPlmGJcaUg==", + "dev": true, + "license": "MIT" }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } @@ -4673,6 +4770,7 @@ "resolved": "https://registry.npmjs.org/check-engine/-/check-engine-1.14.0.tgz", "integrity": "sha512-CZZ3UmZKMer4O63yNWit5KLm7FoO69shcdPbkP8Dj4N728jqI7d8YyAigOgKnajVBA7TtaL7BuaMRDXcoYJKxw==", "dev": true, + "license": "MIT", "dependencies": { "bluebird": "3.7.2", "colors": "1.4.0", @@ -4693,6 +4791,7 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -4717,6 +4816,7 @@ "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" }, @@ -4735,19 +4835,22 @@ "url": "https://github.com/sponsors/sibiraj-s" } ], + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/cjs-module-lexer": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", - "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==" + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "license": "MIT" }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", "engines": { "node": ">=6" } @@ -4757,6 +4860,7 @@ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -4780,22 +4884,25 @@ "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", "dev": true, + "license": "MIT", "engines": { "iojs": ">= 1.0.0", "node": ">= 0.12.0" } }, "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "1.1.3" } @@ -4804,7 +4911,8 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/colorette": { "version": "2.0.20", @@ -4817,27 +4925,17 @@ "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.1.90" } }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/command-line-usage": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz", "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==", "dev": true, + "license": "MIT", "dependencies": { "array-back": "^4.0.2", "chalk": "^2.4.2", @@ -4865,20 +4963,23 @@ "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==", - "dependencies": { - "safe-buffer": "5.2.1" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/content-type": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -4887,26 +4988,33 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } }, "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", "dev": true, + "license": "MIT", "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -4933,6 +5041,7 @@ "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", @@ -4954,6 +5063,7 @@ "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" }, @@ -4969,6 +5079,7 @@ "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" @@ -4985,6 +5096,7 @@ "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" }, @@ -4996,13 +5108,15 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/create-jest/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" } @@ -5012,6 +5126,7 @@ "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" }, @@ -5034,32 +5149,6 @@ "node": ">= 8" } }, - "node_modules/cssstyle": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.3.0.tgz", - "integrity": "sha512-6r0NiY0xizYqfBvWp1G7WXJ06/bZyrk7Dc6PHql82C/pKGUTKu4yAX4Y8JPamb1ob9nBKuxWzCGTRuGwU3yxJQ==", - "dev": true, - "dependencies": { - "@asamuzakjp/css-color": "^3.1.1", - "rrweb-cssom": "^0.8.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/data-urls": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", - "dev": true, - "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -5124,11 +5213,12 @@ } }, "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -5139,17 +5229,12 @@ } } }, - "node_modules/decimal.js": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", - "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", - "dev": true - }, "node_modules/dedent": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", - "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", "dev": true, + "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, @@ -5164,6 +5249,7 @@ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4.0.0" } @@ -5172,12 +5258,14 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "dev": true, + "license": "MIT" }, "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==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5187,6 +5275,7 @@ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -5204,6 +5293,7 @@ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -5216,15 +5306,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -5238,6 +5319,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -5245,22 +5327,15 @@ "node_modules/deprecation": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "license": "ISC" }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -5270,6 +5345,7 @@ "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true, + "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -5279,6 +5355,7 @@ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -5287,10 +5364,11 @@ } }, "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=12" }, @@ -5324,19 +5402,22 @@ "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.4.754", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.754.tgz", - "integrity": "sha512-7Kr5jUdns5rL/M9wFFmMZAgFDuL2YOnanFH4OI4iFzUqyh3XOL7nAGbSlSMZdzKMIyyTpNSbqZsWG9odwLeKvA==", - "dev": true + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true, + "license": "ISC" }, "node_modules/emittery": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -5348,58 +5429,50 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "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==", "license": "MIT", "dependencies": { "once": "^1.4.0" } }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } }, "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", "dev": true, "license": "MIT", "dependencies": { @@ -5407,18 +5480,18 @@ "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", @@ -5430,21 +5503,24 @@ "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", + "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", + "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", + "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -5453,7 +5529,7 @@ "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" + "which-typed-array": "^1.1.19" }, "engines": { "node": ">= 0.4" @@ -5475,32 +5551,33 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", + "es-abstract": "^1.24.1", "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", + "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", + "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", + "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" }, "engines": { @@ -5508,9 +5585,10 @@ } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -5535,12 +5613,16 @@ } }, "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, + "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/es-to-primitive": { @@ -5562,10 +5644,11 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -5573,28 +5656,32 @@ "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.0" } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -5640,10 +5727,11 @@ } }, "node_modules/eslint-compat-utils": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.0.tgz", - "integrity": "sha512-dc6Y8tzEcSYZMHa+CMPLi/hyo1FzNeonbhJL7Ol0ccuKQkwopJcJBA9YL/xmMTLU1eKigXo9vj9nALElWYSowg==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "semver": "^7.5.4" @@ -5674,6 +5762,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "engines": { "node": ">=12.0.0" }, @@ -5703,6 +5792,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "peerDependencies": { "eslint": "^8.8.0", "eslint-plugin-react": "^7.28.0" @@ -5713,6 +5803,7 @@ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", @@ -5724,14 +5815,15 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "dev": true, "license": "MIT", "dependencies": { @@ -5761,6 +5853,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", "dev": true, + "license": "MIT", "dependencies": { "eslint-utils": "^2.0.0", "regexpp": "^3.0.0" @@ -5776,51 +5869,53 @@ } }, "node_modules/eslint-plugin-es-x": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.6.0.tgz", - "integrity": "sha512-I0AmeNgevgaTR7y2lrVCJmGYF0rjoznpDvqV/kIkZSZbZ8Rw3eu4cGlvBBULScfkSOCzqKbff5LR4CNrV7mZHA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz", + "integrity": "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==", "dev": true, + "funding": [ + "https://github.com/sponsors/ota-meshi", + "https://opencollective.com/eslint" + ], + "license": "MIT", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.1.2", - "@eslint-community/regexpp": "^4.6.0", - "eslint-compat-utils": "^0.5.0" + "@eslint-community/regexpp": "^4.11.0", + "eslint-compat-utils": "^0.5.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ota-meshi" - }, "peerDependencies": { "eslint": ">=8" } }, "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", + "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", - "is-core-module": "^2.15.1", + "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", - "object.values": "^1.2.0", + "object.values": "^1.2.1", "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", + "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "engines": { @@ -5830,10 +5925,17 @@ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, + "node_modules/eslint-plugin-import/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/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -5846,6 +5948,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } @@ -5855,6 +5958,7 @@ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -5863,9 +5967,9 @@ } }, "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "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": { @@ -5880,6 +5984,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -5889,6 +5994,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.6.2.tgz", "integrity": "sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", @@ -5913,10 +6019,18 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-plugin-n/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", + "peer": true + }, "node_modules/eslint-plugin-n/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "peer": true, @@ -5925,10 +6039,27 @@ "concat-map": "0.0.1" } }, + "node_modules/eslint-plugin-n/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-plugin-n/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "peer": true, @@ -5939,11 +6070,26 @@ "node": "*" } }, + "node_modules/eslint-plugin-n/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-plugin-node": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", "dev": true, + "license": "MIT", "dependencies": { "eslint-plugin-es": "^3.0.0", "eslint-utils": "^2.0.0", @@ -5959,10 +6105,17 @@ "eslint": ">=5.16.0" } }, + "node_modules/eslint-plugin-node/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/eslint-plugin-node/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -5971,9 +6124,9 @@ } }, "node_modules/eslint-plugin-node/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "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": { @@ -5988,6 +6141,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -5997,6 +6151,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.6.0.tgz", "integrity": "sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==", "dev": true, + "license": "ISC", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -6008,9 +6163,9 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.37.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.3.tgz", - "integrity": "sha512-DomWuTQPFYZwF/7c9W2fkKkStqZmBd3uugfqBYLdkZ3Hii23WzZuOLUskGxB8qkSKqftxEeGL1TB2kMhrce0jA==", + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, "license": "MIT", "dependencies": { @@ -6024,7 +6179,7 @@ "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.8", + "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", @@ -6040,10 +6195,17 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, + "node_modules/eslint-plugin-react/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/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -6056,6 +6218,7 @@ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -6064,9 +6227,9 @@ } }, "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "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": { @@ -6077,18 +6240,25 @@ } }, "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", "dev": true, + "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6098,6 +6268,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -6107,6 +6278,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -6123,6 +6295,7 @@ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", "dev": true, + "license": "MIT", "dependencies": { "eslint-visitor-keys": "^1.1.0" }, @@ -6138,6 +6311,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=4" } @@ -6147,6 +6321,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -6183,6 +6358,7 @@ "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" }, @@ -6193,10 +6369,17 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/eslint/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/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -6209,6 +6392,7 @@ "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" @@ -6225,6 +6409,7 @@ "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" }, @@ -6236,13 +6421,15 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -6250,19 +6437,54 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint/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/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "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": { @@ -6277,6 +6499,7 @@ "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" }, @@ -6284,18 +6507,45 @@ "node": ">=8" } }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.2.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -6306,6 +6556,7 @@ "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" @@ -6315,10 +6566,11 @@ } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -6331,6 +6583,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -6343,6 +6596,7 @@ "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" } @@ -6352,6 +6606,7 @@ "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" } @@ -6372,14 +6627,15 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/eventsource": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-4.0.0.tgz", - "integrity": "sha512-fvIkb9qZzdMxgZrEQDyll+9oJsyaVvY92I2Re+qK0qEJ+w5s0X3dtz+M0VAPOjP1gtU3iqWyjQ0G3nvd5CLZ2g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-4.1.0.tgz", + "integrity": "sha512-2GuF51iuHX6A9xdTccMTsNb7VO0lHZihApxhvQzJB5A03DvHDd2FQepodbMaztPBmBcE/ox7o2gqaxGhYB9LhQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6404,6 +6660,7 @@ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -6436,6 +6693,7 @@ "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", @@ -6448,72 +6706,48 @@ } }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { - "node": ">= 0.10.0" + "node": ">= 18" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/express" } }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/fast-content-type-parse": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", @@ -6527,31 +6761,34 @@ "type": "opencollective", "url": "https://opencollective.com/fastify" } - ] + ], + "license": "MIT" }, "node_modules/fast-copy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", - "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.2.tgz", + "integrity": "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==", "license": "MIT" }, "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 + "dev": true, + "license": "MIT" }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "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.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -6562,6 +6799,7 @@ "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" }, @@ -6573,22 +6811,15 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fast-redact": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", - "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", - "license": "MIT", - "engines": { - "node": ">=6" - } + "dev": true, + "license": "MIT" }, "node_modules/fast-safe-stringify": { "version": "2.1.1", @@ -6597,10 +6828,11 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } @@ -6610,6 +6842,7 @@ "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "bser": "2.1.1" } @@ -6619,6 +6852,7 @@ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, + "license": "MIT", "dependencies": { "flat-cache": "^3.0.4" }, @@ -6631,6 +6865,7 @@ "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" }, @@ -6639,48 +6874,32 @@ } }, "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, "engines": { - "node": ">= 0.8" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -6692,11 +6911,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/flat-cache": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -6707,42 +6939,33 @@ } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true - }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", + "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.1.3" - } + "license": "ISC" }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, "license": "MIT", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "is-callable": "^1.2.7" }, "engines": { - "node": ">= 6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -6754,18 +6977,20 @@ "license": "MIT" }, "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.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 + "dev": true, + "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", @@ -6773,6 +6998,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -6785,6 +7011,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6815,15 +7042,27 @@ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -6832,22 +7071,23 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, "node_modules/get-intrinsic": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", - "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", + "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "get-proto": "^1.0.0", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", @@ -6865,6 +7105,7 @@ "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.0.0" } @@ -6887,6 +7128,7 @@ "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -6899,6 +7141,7 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -6925,10 +7168,11 @@ } }, "node_modules/get-tsconfig": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.3.tgz", - "integrity": "sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==", + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -6941,7 +7185,9 @@ "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", @@ -6962,6 +7208,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -6969,10 +7216,17 @@ "node": ">=10.13.0" } }, + "node_modules/glob/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/glob/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -6981,9 +7235,9 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "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": { @@ -6994,15 +7248,13 @@ } }, "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7013,6 +7265,7 @@ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, + "license": "MIT", "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" @@ -7039,13 +7292,15 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/has-bigints": { "version": "1.1.0", @@ -7065,6 +7320,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -7074,6 +7330,7 @@ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" }, @@ -7114,6 +7371,7 @@ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, + "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" }, @@ -7128,6 +7386,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -7145,98 +7404,75 @@ "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "node_modules/html-encoding-sniffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, - "dependencies": { - "whatwg-encoding": "^3.1.1" - }, - "engines": { - "node": ">=18" - } + "license": "ISC" }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.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, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" }, - "engines": { - "node": ">= 14" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/http-status-codes": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", - "dev": true - }, - "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, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } + "license": "MIT" }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=10.17.0" } }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -7245,13 +7481,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "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" @@ -7264,9 +7502,9 @@ } }, "node_modules/import-in-the-middle": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.13.1.tgz", - "integrity": "sha512-k2V9wNm9B+ysuelDTHjI9d5KPc4l8zAZTGqj+pcynvWkypZd857ryzN8jNC7Pg2YZXNMJcHRPpaDyCBbNyVRpA==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.15.0.tgz", + "integrity": "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==", "license": "Apache-2.0", "dependencies": { "acorn": "^8.14.0", @@ -7276,10 +7514,11 @@ } }, "node_modules/import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dev": true, + "license": "MIT", "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" @@ -7294,11 +7533,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.19" } @@ -7307,6 +7557,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", "engines": { "node": ">=8" } @@ -7315,7 +7566,9 @@ "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" @@ -7324,7 +7577,8 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" }, "node_modules/internal-slot": { "version": "1.1.0", @@ -7342,12 +7596,12 @@ } }, "node_modules/ioredis": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.5.0.tgz", - "integrity": "sha512-7CutT89g23FfSa8MDoIFs2GYYa0PaNiW/OrT+nRyjRXHDZd17HmIgy+reOQ/yhh72NznNjGuS8kbCAcA4Ro4mw==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.0.tgz", + "integrity": "sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA==", "license": "MIT", "dependencies": { - "@ioredis/commands": "^1.1.1", + "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", @@ -7369,6 +7623,7 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", "engines": { "node": ">= 0.10" } @@ -7394,15 +7649,17 @@ "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==" + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" }, "node_modules/is-async-function": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.0.tgz", - "integrity": "sha512-GExz9MtyhlZyXYLxzlJRj5WUCE661zhDa1Yna52CN57AJsymh+DvXXjyveSioqSRdxvUrdKdvqB1b5cVKsNpWQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, "license": "MIT", "dependencies": { + "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", @@ -7436,6 +7693,7 @@ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -7444,13 +7702,13 @@ } }, "node_modules/is-boolean-object": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.1.tgz", - "integrity": "sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", + "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" }, "engines": { @@ -7465,6 +7723,7 @@ "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "builtin-modules": "^3.3.0" @@ -7544,6 +7803,7 @@ "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" } @@ -7569,6 +7829,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -7578,19 +7839,21 @@ "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" }, @@ -7606,6 +7869,7 @@ "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" }, @@ -7626,17 +7890,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-node-process": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", - "dev": true + "dev": true, + "license": "MIT" }, "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" } @@ -7663,15 +7942,16 @@ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" }, "node_modules/is-regex": { "version": "1.2.1", @@ -7726,6 +8006,7 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -7798,13 +8079,13 @@ } }, "node_modules/is-weakref": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.0.tgz", - "integrity": "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -7841,22 +8122,25 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=8" } }, "node_modules/istanbul-lib-instrument": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", - "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.23.9", "@babel/parser": "^7.23.9", @@ -7873,6 +8157,7 @@ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", @@ -7887,6 +8172,7 @@ "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" } @@ -7896,6 +8182,7 @@ "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" }, @@ -7908,6 +8195,7 @@ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", @@ -7918,10 +8206,11 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" @@ -7953,6 +8242,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -7979,6 +8269,7 @@ "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", "dev": true, + "license": "MIT", "dependencies": { "execa": "^5.0.0", "jest-util": "^29.7.0", @@ -7993,6 +8284,7 @@ "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -8024,6 +8316,7 @@ "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" }, @@ -8039,6 +8332,7 @@ "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" @@ -8055,6 +8349,7 @@ "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" }, @@ -8066,13 +8361,15 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/jest-circus/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" } @@ -8082,6 +8379,7 @@ "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" }, @@ -8094,6 +8392,7 @@ "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", "dev": true, + "license": "MIT", "dependencies": { "@jest/core": "^29.7.0", "@jest/test-result": "^29.7.0", @@ -8127,6 +8426,7 @@ "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" }, @@ -8142,6 +8442,7 @@ "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" @@ -8158,6 +8459,7 @@ "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" }, @@ -8169,13 +8471,15 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/jest-cli/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" } @@ -8185,6 +8489,7 @@ "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" }, @@ -8197,6 +8502,7 @@ "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", "@jest/test-sequencer": "^29.7.0", @@ -8242,6 +8548,7 @@ "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" }, @@ -8257,6 +8564,7 @@ "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" @@ -8273,6 +8581,7 @@ "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" }, @@ -8284,13 +8593,15 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/jest-config/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" } @@ -8300,6 +8611,7 @@ "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" }, @@ -8312,6 +8624,7 @@ "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", @@ -8327,6 +8640,7 @@ "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" }, @@ -8342,6 +8656,7 @@ "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" @@ -8358,6 +8673,7 @@ "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" }, @@ -8369,13 +8685,15 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/jest-diff/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" } @@ -8385,6 +8703,7 @@ "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" }, @@ -8397,6 +8716,7 @@ "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", "dev": true, + "license": "MIT", "dependencies": { "detect-newline": "^3.0.0" }, @@ -8409,6 +8729,7 @@ "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", @@ -8425,6 +8746,7 @@ "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" }, @@ -8440,6 +8762,7 @@ "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" @@ -8456,6 +8779,7 @@ "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" }, @@ -8467,13 +8791,15 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/jest-each/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" } @@ -8483,6 +8809,7 @@ "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" }, @@ -8495,6 +8822,7 @@ "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -8512,6 +8840,7 @@ "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true, + "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -8521,6 +8850,7 @@ "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", @@ -8546,6 +8876,7 @@ "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-16.0.0.tgz", "integrity": "sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "mkdirp": "^1.0.4", "strip-ansi": "^6.0.1", @@ -8561,6 +8892,7 @@ "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", "dev": true, + "license": "MIT", "dependencies": { "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" @@ -8574,6 +8906,7 @@ "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", @@ -8589,6 +8922,7 @@ "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" }, @@ -8604,6 +8938,7 @@ "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" @@ -8620,6 +8955,7 @@ "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" }, @@ -8631,13 +8967,15 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/jest-matcher-utils/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" } @@ -8647,6 +8985,7 @@ "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" }, @@ -8659,6 +8998,7 @@ "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", @@ -8679,6 +9019,7 @@ "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" }, @@ -8694,6 +9035,7 @@ "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" @@ -8710,6 +9052,7 @@ "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" }, @@ -8721,13 +9064,15 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/jest-message-util/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" } @@ -8737,6 +9082,7 @@ "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" }, @@ -8749,6 +9095,7 @@ "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -8763,6 +9110,7 @@ "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" }, @@ -8780,6 +9128,7 @@ "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true, + "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -8789,6 +9138,7 @@ "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", @@ -8809,6 +9159,7 @@ "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", "dev": true, + "license": "MIT", "dependencies": { "jest-regex-util": "^29.6.3", "jest-snapshot": "^29.7.0" @@ -8822,6 +9173,7 @@ "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" }, @@ -8837,6 +9189,7 @@ "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" @@ -8853,6 +9206,7 @@ "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" }, @@ -8864,13 +9218,15 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/jest-resolve/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" } @@ -8880,6 +9236,7 @@ "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" }, @@ -8892,6 +9249,7 @@ "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", "@jest/environment": "^29.7.0", @@ -8924,6 +9282,7 @@ "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" }, @@ -8939,6 +9298,7 @@ "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" @@ -8955,6 +9315,7 @@ "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" }, @@ -8966,13 +9327,15 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/jest-runner/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" } @@ -8982,6 +9345,7 @@ "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" }, @@ -8994,6 +9358,7 @@ "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -9027,6 +9392,7 @@ "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" }, @@ -9042,6 +9408,7 @@ "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" @@ -9058,6 +9425,7 @@ "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" }, @@ -9069,13 +9437,15 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/jest-runtime/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" } @@ -9085,6 +9455,7 @@ "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" }, @@ -9097,6 +9468,7 @@ "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", @@ -9128,6 +9500,7 @@ "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" }, @@ -9143,6 +9516,7 @@ "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" @@ -9159,6 +9533,7 @@ "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" }, @@ -9170,13 +9545,15 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/jest-snapshot/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" } @@ -9186,6 +9563,7 @@ "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" }, @@ -9198,6 +9576,7 @@ "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -9215,6 +9594,7 @@ "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" }, @@ -9230,6 +9610,7 @@ "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" @@ -9246,6 +9627,7 @@ "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" }, @@ -9257,13 +9639,15 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/jest-util/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" } @@ -9273,6 +9657,7 @@ "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" }, @@ -9285,6 +9670,7 @@ "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", @@ -9302,6 +9688,7 @@ "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" }, @@ -9317,6 +9704,7 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -9329,6 +9717,7 @@ "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" @@ -9345,6 +9734,7 @@ "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" }, @@ -9356,13 +9746,15 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/jest-validate/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" } @@ -9372,6 +9764,7 @@ "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" }, @@ -9384,6 +9777,7 @@ "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", "dev": true, + "license": "MIT", "dependencies": { "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", @@ -9403,6 +9797,7 @@ "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" }, @@ -9418,6 +9813,7 @@ "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" @@ -9434,6 +9830,7 @@ "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" }, @@ -9445,13 +9842,15 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/jest-watcher/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" } @@ -9461,6 +9860,7 @@ "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" }, @@ -9473,6 +9873,7 @@ "resolved": "https://registry.npmjs.org/jest-when/-/jest-when-3.7.0.tgz", "integrity": "sha512-aLbiyxmtksijcrKFir7n+t+XPbqSLV01eDkRyX28WM4VgA/iSc3mG8R8O2evDtOAa6SefrJiTIt/rTqqyrwVZg==", "dev": true, + "license": "MIT", "peerDependencies": { "jest": ">= 25" } @@ -9482,6 +9883,7 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", @@ -9497,6 +9899,7 @@ "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" } @@ -9506,6 +9909,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -9529,12 +9933,23 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "dependencies": { "argparse": "^2.0.1" }, @@ -9542,98 +9957,72 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsdom": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.0.0.tgz", - "integrity": "sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==", - "dev": true, - "dependencies": { - "cssstyle": "^4.2.1", - "data-urls": "^5.0.0", - "decimal.js": "^10.4.3", - "form-data": "^4.0.1", - "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.16", - "parse5": "^7.2.1", - "rrweb-cssom": "^0.8.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^5.0.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^3.1.1", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.1.0", - "ws": "^8.18.0", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, + "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "license": "MIT" }, "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 + "dev": true, + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true + "dev": true, + "license": "ISC" + }, + "node_modules/json-with-bigint": { + "version": "3.5.7", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.7.tgz", + "integrity": "sha512-7ei3MdAI5+fJPVnKlW77TKNKwQ5ppSzWvhPuSuINT/GYW9ZOC1eRKOuhV9yHG5aEsUPj9BBx5JIekkmoLHxZOw==", + "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, + "license": "MIT", "bin": { "json5": "lib/cli.js" }, @@ -9646,6 +10035,7 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, + "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -9654,12 +10044,12 @@ } }, "node_modules/jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", "license": "MIT", "dependencies": { - "jws": "^3.2.2", + "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", @@ -9680,6 +10070,7 @@ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, + "license": "MIT", "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", @@ -9691,23 +10082,23 @@ } }, "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "license": "MIT", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, @@ -9716,6 +10107,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -9725,6 +10117,7 @@ "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -9734,6 +10127,7 @@ "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -9743,6 +10137,7 @@ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -9755,49 +10150,19 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/load-json-file": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz", - "integrity": "sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==", - "dependencies": { - "graceful-fs": "^4.1.15", - "parse-json": "^4.0.0", - "pify": "^4.0.1", - "strip-bom": "^3.0.0", - "type-fest": "^0.3.0" - }, + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-7.0.1.tgz", + "integrity": "sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ==", + "license": "MIT", "engines": { - "node": ">=6" - } - }, - "node_modules/load-json-file/node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/load-json-file/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/load-json-file/node_modules/type-fest": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", - "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", - "engines": { - "node": ">=6" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/locate-path": { @@ -9805,6 +10170,7 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -9820,6 +10186,7 @@ "resolved": "https://registry.npmjs.org/lockfile-lint/-/lockfile-lint-4.14.1.tgz", "integrity": "sha512-NW0Tk1qfldhbhJWQENYQWANdmlanXKxvTJYRYKn56INYjaP2M07Ua2SJYkUMS+ZbYwxDzul/C6pDsV/NEXrl+A==", "dev": true, + "license": "Apache-2.0", "dependencies": { "cosmiconfig": "^9.0.0", "debug": "^4.3.4", @@ -9839,6 +10206,7 @@ "resolved": "https://registry.npmjs.org/lockfile-lint-api/-/lockfile-lint-api-5.9.2.tgz", "integrity": "sha512-3QhxWxl3jT9GcMxuCnTsU8Tz5U6U1lKBlKBu2zOYOz/x3ONUoojEtky3uzoaaDgExcLqIX0Aqv2I7TZXE383CQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@yarnpkg/parsers": "^3.0.0-rc.48.1", "debug": "^4.3.4", @@ -9853,6 +10221,7 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, + "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -9867,9 +10236,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" }, "node_modules/lodash.defaults": { "version": "4.2.0", @@ -9923,7 +10292,8 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.once": { "version": "4.1.1", @@ -9936,6 +10306,7 @@ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, + "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -9947,6 +10318,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "license": "MIT", "engines": { "node": ">=8" } @@ -9956,6 +10328,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^3.0.2" } @@ -9965,6 +10338,7 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, + "license": "MIT", "dependencies": { "semver": "^7.5.3" }, @@ -9980,6 +10354,7 @@ "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "tmpl": "1.0.5" } @@ -9994,11 +10369,12 @@ } }, "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/memorystream": { @@ -10011,9 +10387,13 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -10022,13 +10402,15 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true + "dev": true, + "license": "MIT" }, "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" } @@ -10037,15 +10419,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/mersenne-twister/-/mersenne-twister-1.1.0.tgz", "integrity": "sha512-mUYWsMKNrm4lfygPkL3OfGzOPTR2DBlTkBNHM//F6hGp8cLThY897crAlk3/Jo17LEOOjQUrNAx6DvgO77QJkA==", - "dev": true - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } + "dev": true, + "license": "MIT" }, "node_modules/micromatch": { "version": "4.0.8", @@ -10061,34 +10436,29 @@ "node": ">=8.6" } }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, "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==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "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==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/mimic-fn": { @@ -10096,20 +10466,21 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "license": "ISC", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -10119,6 +10490,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -10128,6 +10500,7 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true, + "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" }, @@ -10136,26 +10509,29 @@ } }, "node_modules/module-details-from-path": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", - "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", "license": "MIT" }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -10164,15 +10540,17 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/nock": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.2.tgz", - "integrity": "sha512-jDUBpCNY7KLyOF2FTiT4mLRxV2kpjo4YrcGtv3IMCIwqFKxOQFrmeXefvuEiWvS3ApPWQHAkakkkVpoOrDNCVg==", + "version": "14.0.11", + "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.11.tgz", + "integrity": "sha512-u5xUnYE+UOOBA6SpELJheMCtj2Laqx15Vl70QxKo43Wz/6nMHXS7PrEioXLjXAwhmawdEMNImwKCcPhBJWbKVw==", "dev": true, + "license": "MIT", "dependencies": { - "@mswjs/interceptors": "^0.38.0", + "@mswjs/interceptors": "^0.41.0", "json-stringify-safe": "^5.0.1", "propagate": "^2.0.0" }, @@ -10181,38 +10559,68 @@ } }, "node_modules/node-cron": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", - "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", + "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", "dependencies": { - "uuid": "8.3.2" + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" }, "engines": { - "node": ">=6.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" }, "node_modules/nodemon": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", - "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", "dev": true, + "license": "MIT", "dependencies": { "chokidar": "^3.5.2", "debug": "^4", "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", + "minimatch": "^10.2.1", "pstree.remy": "^1.1.8", "semver": "^7.5.3", "simple-update-notifier": "^2.0.0", @@ -10231,50 +10639,12 @@ "url": "https://opencollective.com/nodemon" } }, - "node_modules/nodemon/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/nodemon/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", - "dev": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "*" - } - }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", @@ -10287,6 +10657,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver" } @@ -10296,6 +10667,7 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -10305,6 +10677,7 @@ "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", "chalk": "^2.4.1", @@ -10325,10 +10698,17 @@ "node": ">= 4" } }, + "node_modules/npm-run-all/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/npm-run-all/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -10354,9 +10734,9 @@ } }, "node_modules/npm-run-all/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "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": { @@ -10371,6 +10751,7 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -10380,6 +10761,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver" } @@ -10389,6 +10771,7 @@ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^1.0.0" }, @@ -10401,6 +10784,7 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -10410,6 +10794,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -10422,6 +10807,7 @@ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.0.0" }, @@ -10429,17 +10815,18 @@ "node": ">=8" } }, - "node_modules/nwsapi": { - "version": "2.2.20", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", - "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", - "dev": true + "node_modules/npx-import-light": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/npx-import-light/-/npx-import-light-1.0.0.tgz", + "integrity": "sha512-OgOY4wh4tfj6G7nKYobbpYg2yhYH/19dz/ZRzcOGN+rP3wjLHTRb6ZMM5UshLu4gbDdwwZ8VX2fXBr4krsYHzQ==", + "license": "ISC" }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -10449,14 +10836,15 @@ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/object-inspect": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", - "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -10470,6 +10858,7 @@ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -10496,14 +10885,16 @@ } }, "node_modules/object.entries": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", - "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" + "es-object-atoms": "^1.1.1" }, "engines": { "node": ">= 0.4" @@ -10514,6 +10905,7 @@ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -10532,6 +10924,7 @@ "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -10561,20 +10954,21 @@ } }, "node_modules/octokit": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/octokit/-/octokit-5.0.3.tgz", - "integrity": "sha512-+bwYsAIRmYv30NTmBysPIlgH23ekVDriB07oRxlPIAH5PI0yTMSxg5i5Xy0OetcnZw+nk/caD4szD7a9YZ3QyQ==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/octokit/-/octokit-5.0.5.tgz", + "integrity": "sha512-4+/OFSqOjoyULo7eN7EA97DE0Xydj/PW5aIckxqQIoFjFwqXKuFCvXUJObyJfBF9Khu4RL/jlDRI9FPaMGfPnw==", + "license": "MIT", "dependencies": { - "@octokit/app": "^16.0.1", - "@octokit/core": "^7.0.2", - "@octokit/oauth-app": "^8.0.1", + "@octokit/app": "^16.1.2", + "@octokit/core": "^7.0.6", + "@octokit/oauth-app": "^8.0.3", "@octokit/plugin-paginate-graphql": "^6.0.0", - "@octokit/plugin-paginate-rest": "^13.0.0", - "@octokit/plugin-rest-endpoint-methods": "^16.0.0", - "@octokit/plugin-retry": "^8.0.1", - "@octokit/plugin-throttling": "^11.0.1", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0", + "@octokit/plugin-paginate-rest": "^14.0.0", + "@octokit/plugin-rest-endpoint-methods": "^17.0.0", + "@octokit/plugin-retry": "^8.0.3", + "@octokit/plugin-throttling": "^11.0.3", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", "@octokit/webhooks": "^14.0.0" }, "engines": { @@ -10582,213 +10976,23 @@ } }, "node_modules/octokit-auth-probot": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/octokit-auth-probot/-/octokit-auth-probot-2.0.1.tgz", - "integrity": "sha512-HzOJ4EPC5OJN6oZEoKTMYtqUQ2ZSKHmDWbLHfFB7JYpho9Zb+aJmDfRShd5a/eGvmIzbZ0NRIWjmnvspDp8JAQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/octokit-auth-probot/-/octokit-auth-probot-4.2.2.tgz", + "integrity": "sha512-fxJ8iIu030wh6wSCpT586IIH77+qLCLv0Jofx55aNotNC+8NN6MClE4EU0+3Fez6CRSyYa1RzIh1WcryEBjgRg==", "license": "ISC", "dependencies": { - "@octokit/auth-app": "^6.1.3", - "@octokit/auth-token": "^4.0.0", - "@octokit/auth-unauthenticated": "^5.0.1", - "@octokit/types": "^12.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@octokit/core": ">=5" - } - }, - "node_modules/octokit/node_modules/@octokit/auth-token": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", - "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", - "engines": { - "node": ">= 20" - } - }, - "node_modules/octokit/node_modules/@octokit/core": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.2.tgz", - "integrity": "sha512-ODsoD39Lq6vR6aBgvjTnA3nZGliknKboc9Gtxr7E4WDNqY24MxANKcuDQSF0jzapvGb3KWOEDrKfve4HoWGK+g==", - "dependencies": { + "@octokit/auth-app": "^8.1.2", "@octokit/auth-token": "^6.0.0", - "@octokit/graphql": "^9.0.1", - "@octokit/request": "^10.0.2", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0", - "before-after-hook": "^4.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/octokit/node_modules/@octokit/endpoint": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", - "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", - "dependencies": { - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/octokit/node_modules/@octokit/graphql": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.1.tgz", - "integrity": "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==", - "dependencies": { - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/octokit/node_modules/@octokit/openapi-types": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", - "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==" - }, - "node_modules/octokit/node_modules/@octokit/plugin-paginate-graphql": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-6.0.0.tgz", - "integrity": "sha512-crfpnIoFiBtRkvPqOyLOsw12XsveYuY2ieP6uYDosoUegBJpSVxGwut9sxUgFFcll3VTOTqpUf8yGd8x1OmAkQ==", - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/octokit/node_modules/@octokit/plugin-paginate-rest": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.0.0.tgz", - "integrity": "sha512-nPXM3wgil9ONnAINcm8cN+nwso4QhNB13PtnlRFkYFHCUIogcH9DHak/StQYcwkkjuc7pUluLG1AWZNscgvH7Q==", - "dependencies": { - "@octokit/types": "^14.0.0" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/octokit/node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-16.0.0.tgz", - "integrity": "sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g==", - "dependencies": { - "@octokit/types": "^14.1.0" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/octokit/node_modules/@octokit/plugin-retry": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-8.0.1.tgz", - "integrity": "sha512-KUoYR77BjF5O3zcwDQHRRZsUvJwepobeqiSSdCJ8lWt27FZExzb0GgVxrhhfuyF6z2B2zpO0hN5pteni1sqWiw==", - "dependencies": { - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0", - "bottleneck": "^2.15.3" + "@octokit/auth-unauthenticated": "^7.0.3", + "@octokit/types": "^16.0.0" }, "engines": { - "node": ">= 20" + "node": "^20.17 || >=22" }, "peerDependencies": { "@octokit/core": ">=7" } }, - "node_modules/octokit/node_modules/@octokit/plugin-throttling": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-11.0.1.tgz", - "integrity": "sha512-S+EVhy52D/272L7up58dr3FNSMXWuNZolkL4zMJBNIfIxyZuUcczsQAU4b5w6dewJXnKYVgSHSV5wxitMSW1kw==", - "dependencies": { - "@octokit/types": "^14.0.0", - "bottleneck": "^2.15.3" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": "^7.0.0" - } - }, - "node_modules/octokit/node_modules/@octokit/request": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.2.tgz", - "integrity": "sha512-iYj4SJG/2bbhh+iIpFmG5u49DtJ4lipQ+aPakjL9OKpsGY93wM8w06gvFbEQxcMsZcCvk5th5KkIm2m8o14aWA==", - "dependencies": { - "@octokit/endpoint": "^11.0.0", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0", - "fast-content-type-parse": "^3.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/octokit/node_modules/@octokit/request-error": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", - "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", - "dependencies": { - "@octokit/types": "^14.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/octokit/node_modules/@octokit/types": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", - "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", - "dependencies": { - "@octokit/openapi-types": "^25.1.0" - } - }, - "node_modules/octokit/node_modules/@octokit/webhooks": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-14.0.0.tgz", - "integrity": "sha512-IZV4vg/s1pqIpCs86a0tp5FQ/O94DUaqksMdNrXFSaE037TXsB+fIhr8OVig09oEx3WazVgE6B2U+u7/Fvdlsw==", - "dependencies": { - "@octokit/openapi-webhooks-types": "11.0.0", - "@octokit/request-error": "^7.0.0", - "@octokit/webhooks-methods": "^6.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/octokit/node_modules/@octokit/webhooks-methods": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-6.0.0.tgz", - "integrity": "sha512-MFlzzoDJVw/GcbfzVC1RLR36QqkTLUf79vLVO3D+xn7r0QgxnFoLZgtrzxiQErAjFUOdH6fas2KeQJ1yr/qaXQ==", - "engines": { - "node": ">= 20" - } - }, - "node_modules/octokit/node_modules/before-after-hook": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", - "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==" - }, - "node_modules/octokit/node_modules/universal-user-agent": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", - "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==" - }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", @@ -10802,6 +11006,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, @@ -10813,6 +11018,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", "dependencies": { "wrappy": "1" } @@ -10822,6 +11028,7 @@ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, + "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" }, @@ -10837,6 +11044,7 @@ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, + "license": "MIT", "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -10853,7 +11061,8 @@ "version": "1.4.3", "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/own-keys": { "version": "1.0.1", @@ -10878,6 +11087,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -10893,6 +11103,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -10907,15 +11118,33 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/package-config": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/package-config/-/package-config-5.0.0.tgz", + "integrity": "sha512-GYTTew2slBcYdvRHqjhwaaydVMvn/qrGC323+nKclYioNSLTDUM/lGgtGTgyHVtYcozb+XkE8CNhwcraOmZ9Mg==", + "license": "MIT", + "dependencies": { + "find-up-simple": "^1.0.0", + "load-json-file": "^7.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "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" }, @@ -10928,6 +11157,7 @@ "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", @@ -10941,22 +11171,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse5": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", - "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", - "dev": true, - "dependencies": { - "entities": "^4.5.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -10966,6 +11185,7 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -10975,6 +11195,7 @@ "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" } @@ -10984,6 +11205,7 @@ "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" } @@ -10991,13 +11213,31 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.1.tgz", + "integrity": "sha512-fvU78fIjZ+SBM9YwCknCvKOUKkLVqtWDVctl0s7xIqfmfb38t2TT4ZU2gHm+Z8xGwgW+QWEU3oQSAzIbo89Ggw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } }, "node_modules/pg-int8": { "version": "1.0.1", @@ -11009,9 +11249,9 @@ } }, "node_modules/pg-protocol": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.1.tgz", - "integrity": "sha512-gjTHWGYWsEgy9MsY0Gp6ZJxV24IjDqdpTW7Eh0x+WfJLFsm/TJx1MzL6T0D88mBvkpxotCQ6TwW6N+Kko7lhgQ==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.12.0.tgz", + "integrity": "sha512-uOANXNRACNdElMXJ0tPz6RBM0XQ61nONGAwlt8da5zs/iUOOCLBQOHSXnrC6fMsvtjxbOJrZZl5IScGv+7mpbg==", "license": "MIT" }, "node_modules/pg-types": { @@ -11042,6 +11282,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -11054,6 +11295,7 @@ "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", "dev": true, + "license": "MIT", "bin": { "pidtree": "bin/pidtree.js" }, @@ -11062,91 +11304,106 @@ } }, "node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=6" + "node": ">=4" } }, "node_modules/pino": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-9.6.0.tgz", - "integrity": "sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==", + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", "license": "MIT", "dependencies": { + "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", - "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", + "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", - "process-warning": "^4.0.0", + "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", - "thread-stream": "^3.0.0" + "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "node_modules/pino-abstract-transport": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", - "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", "license": "MIT", "dependencies": { "split2": "^4.0.0" } }, "node_modules/pino-http": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-10.4.0.tgz", - "integrity": "sha512-vjQsKBE+VN1LVchjbfLE7B6nBeGASZNRNKsR68VS0DolTm5R3zo+47JX1wjm0O96dcbvA7vnqt8YqOWlG5nN0w==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-11.0.0.tgz", + "integrity": "sha512-wqg5XIAGRRIWtTk8qPGxkbrfiwEWz1lgedVLvhLALudKXvg1/L2lTFgTGPJ4Z2e3qcRmxoFxDuSdMdMGNM6I1g==", "license": "MIT", "dependencies": { "get-caller-file": "^2.0.5", - "pino": "^9.0.0", + "pino": "^10.0.0", "pino-std-serializers": "^7.0.0", - "process-warning": "^4.0.0" + "process-warning": "^5.0.0" } }, "node_modules/pino-pretty": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.0.0.tgz", - "integrity": "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==", + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", + "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", "license": "MIT", "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", - "fast-copy": "^3.0.2", + "fast-copy": "^4.0.0", "fast-safe-stringify": "^2.1.1", "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", + "pino-abstract-transport": "^3.0.0", "pump": "^3.0.0", - "secure-json-parse": "^2.4.0", + "secure-json-parse": "^4.0.0", "sonic-boom": "^4.0.1", - "strip-json-comments": "^3.1.1" + "strip-json-comments": "^5.0.2" }, "bin": { "pino-pretty": "bin.js" } }, + "node_modules/pino-pretty/node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pino-std-serializers": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", - "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", "license": "MIT" }, "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } @@ -11155,6 +11412,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-3.1.0.tgz", "integrity": "sha512-m0OTbR/5VPNPqO1ph6Fqbj7Hv6QU7gR/tQW40ZqrL1rjgCU85W6C1bJn0BItuJqnR98PWzw7Z8hHeChD1WrgdQ==", + "license": "MIT", "dependencies": { "find-up": "^3.0.0", "load-json-file": "^5.2.0" @@ -11163,12 +11421,29 @@ "node": ">=6" } }, - "node_modules/pkg-conf/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "node_modules/pkg-conf/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-conf/node_modules/load-json-file": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz", + "integrity": "sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==", + "license": "MIT", "dependencies": { - "locate-path": "^3.0.0" + "graceful-fs": "^4.1.15", + "parse-json": "^4.0.0", + "pify": "^4.0.1", + "strip-bom": "^3.0.0", + "type-fest": "^0.3.0" }, "engines": { "node": ">=6" @@ -11178,6 +11453,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "license": "MIT", "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" @@ -11190,6 +11466,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -11204,6 +11481,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "license": "MIT", "dependencies": { "p-limit": "^2.0.0" }, @@ -11211,19 +11489,61 @@ "node": ">=6" } }, + "node_modules/pkg-conf/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/pkg-conf/node_modules/path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-conf/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-conf/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "license": "MIT", "engines": { "node": ">=4" } }, + "node_modules/pkg-conf/node_modules/type-fest": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", + "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=6" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, + "license": "MIT", "dependencies": { "find-up": "^4.0.0" }, @@ -11236,6 +11556,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -11249,6 +11570,7 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -11261,6 +11583,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, + "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -11276,6 +11599,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -11284,9 +11608,9 @@ } }, "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, "license": "MIT", "engines": { @@ -11303,9 +11627,9 @@ } }, "node_modules/postgres-bytea": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", - "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11337,6 +11661,7 @@ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } @@ -11346,6 +11671,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -11360,6 +11686,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -11368,59 +11695,45 @@ } }, "node_modules/probot": { - "version": "13.4.4", - "resolved": "https://registry.npmjs.org/probot/-/probot-13.4.4.tgz", - "integrity": "sha512-2f/Vqho0rQpTppx4mfuyB1k2xAuAd/qpJaEp7gLjZ6rBr804KiCey6tcGzSBYt/S6O5umdbqQLpfVyh2RsdpyQ==", + "version": "14.3.2", + "resolved": "https://registry.npmjs.org/probot/-/probot-14.3.2.tgz", + "integrity": "sha512-Bprr2KwlVF4Jpb6bnIcfadA4Dtns8nDJxSEEPNy3FhRrtHUYxZGiczReG5Vo6M7t4iS8Kcvek7CDUTTXgkaSBw==", "license": "ISC", "dependencies": { - "@octokit/core": "^5.2.0", - "@octokit/plugin-enterprise-compatibility": "^4.0.1", - "@octokit/plugin-paginate-rest": "11.4.4-cjs.2", - "@octokit/plugin-rest-endpoint-methods": "13.3.2-cjs.1", - "@octokit/plugin-retry": "^6.0.1", - "@octokit/plugin-throttling": "^8.1.3", - "@octokit/request": "^8.4.1", - "@octokit/types": "^12.3.0", - "@octokit/webhooks": "^12.0.10", - "@probot/get-private-key": "^1.1.2", - "@probot/octokit-plugin-config": "^2.0.1", - "@probot/pino": "^3.0.0", - "@types/express": "^4.17.21", + "@octokit/core": "^7.0.3", + "@octokit/plugin-enterprise-compatibility": "^6.0.1", + "@octokit/plugin-paginate-rest": "^14.0.0", + "@octokit/plugin-rest-endpoint-methods": "^17.0.0", + "@octokit/plugin-retry": "^8.0.1", + "@octokit/plugin-throttling": "^11.0.1", + "@octokit/request": "^10.0.3", + "@octokit/types": "^16.0.0", + "@octokit/webhooks": "^14.1.2", + "@probot/get-private-key": "^2.1.1", + "@probot/octokit-plugin-config": "^4.0.0", + "@probot/pino": "^5.0.1", "bottleneck": "^2.19.5", - "commander": "^12.0.0", "deepmerge": "^4.3.1", - "dotenv": "^16.3.1", - "express": "^4.21.0", - "ioredis": "^5.3.2", - "js-yaml": "^4.1.0", - "lru-cache": "npm:@wolfy1339/lru-cache@^11.0.2-patch.1", - "octokit-auth-probot": "^2.0.1", - "pino": "^9.0.0", - "pino-http": "^10.0.0", - "pkg-conf": "^3.1.0", - "update-dotenv": "^1.1.1" + "import-meta-resolve": "^4.1.0", + "npx-import-light": "^1.0.0", + "octokit-auth-probot": "^4.0.1", + "package-config": "^5.0.0", + "pino": "^10.0.0", + "pino-http": "^11.0.0", + "toad-cache": "^3.7.0", + "yaml": "^2.8.1" }, "bin": { "probot": "bin/probot.js" }, "engines": { - "node": ">=18" - } - }, - "node_modules/probot/node_modules/lru-cache": { - "name": "@wolfy1339/lru-cache", - "version": "11.0.2-patch.1", - "resolved": "https://registry.npmjs.org/@wolfy1339/lru-cache/-/lru-cache-11.0.2-patch.1.tgz", - "integrity": "sha512-BgYZfL2ADCXKOw2wJtkM3slhHotawWkgIRRxq4wEybnZQPjvAp71SPX35xepMykTw8gXlzWcWPTY31hlbnRsDA==", - "license": "ISC", - "engines": { - "node": "18 >=18.20 || 20 || >=22" + "node": "^20.18.1 || >= 22" } }, "node_modules/process-warning": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", - "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", "funding": [ { "type": "github", @@ -11438,6 +11751,7 @@ "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", "dev": true, + "license": "MIT", "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" @@ -11451,6 +11765,7 @@ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dev": true, + "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -11461,13 +11776,15 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/propagate": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -11476,6 +11793,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -11485,20 +11803,25 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -11510,6 +11833,7 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -11528,14 +11852,16 @@ "type": "opencollective", "url": "https://opencollective.com/fast-check" } - ] + ], + "license": "MIT" }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", "dependencies": { - "side-channel": "^1.0.6" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -11562,7 +11888,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/quick-format-unescaped": { "version": "4.0.4", @@ -11574,35 +11901,39 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" } }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", "dev": true, + "license": "MIT", "dependencies": { "load-json-file": "^4.0.0", "normalize-package-data": "^2.3.2", @@ -11617,6 +11948,7 @@ "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.1.2", "parse-json": "^4.0.0", @@ -11632,6 +11964,7 @@ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", "dev": true, + "license": "MIT", "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" @@ -11640,32 +11973,12 @@ "node": ">=4" } }, - "node_modules/read-pkg/node_modules/path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/read-pkg/node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -11675,6 +11988,7 @@ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, + "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -11717,6 +12031,7 @@ "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -11770,6 +12085,7 @@ "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -11782,6 +12098,7 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -11801,17 +12118,21 @@ } }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -11821,6 +12142,7 @@ "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", "dev": true, + "license": "MIT", "dependencies": { "resolve-from": "^5.0.0" }, @@ -11833,6 +12155,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -11842,6 +12165,7 @@ "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" } @@ -11851,25 +12175,28 @@ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "dev": true, + "license": "MIT", "peer": true, "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, "node_modules/resolve.exports": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", - "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "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" @@ -11879,7 +12206,9 @@ "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" }, @@ -11890,11 +12219,21 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rrweb-cssom": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", - "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", - "dev": true + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } }, "node_modules/run-parallel": { "version": "1.2.0", @@ -11915,6 +12254,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -11956,7 +12296,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/safe-push-apply": { "version": "1.0.0", @@ -12005,30 +12346,30 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" }, "node_modules/secure-json-parse": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", - "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "BSD-3-Clause" }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -12043,6 +12384,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -12053,69 +12395,52 @@ "node_modules/semver/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" }, "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" }, "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/send/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==" - }, "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" }, "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-static/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "engines": { - "node": ">= 0.8" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/set-function-length": { @@ -12123,6 +12448,7 @@ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -12140,6 +12466,7 @@ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -12168,13 +12495,15 @@ "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" }, "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" }, @@ -12187,15 +12516,19 @@ "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/shell-quote": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", - "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -12207,14 +12540,13 @@ "license": "BSD-2-Clause" }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -12226,13 +12558,12 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -12282,13 +12613,15 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", "dev": true, + "license": "MIT", "dependencies": { "semver": "^7.5.3" }, @@ -12300,27 +12633,28 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/smee-client": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/smee-client/-/smee-client-4.3.1.tgz", - "integrity": "sha512-4x8Plwtzx/w9N/0TT2tegle6zaDkPnUrPLZUhDt1phUlMMRhlYuSDdEKl0LygZPegsztYd7sWmcYAm8UQ60xQA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/smee-client/-/smee-client-5.0.0.tgz", + "integrity": "sha512-OYL8xPr1Tc0iqGohTGqHO/qEvUrPktK0a2Qcb0Uuj+j4KLnH5bsOhOlmpnmdh4zgzL3OwTfodcmkg5fVgiMUHw==", "dev": true, "license": "ISC", "dependencies": { "eventsource": "^4.0.0", - "undici": "^7.0.0", - "validator": "^13.11.0" + "undici": "^7.0.0" }, "bin": { "smee": "bin/smee.js" @@ -12330,9 +12664,9 @@ } }, "node_modules/sonic-boom": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", - "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", "license": "MIT", "dependencies": { "atomic-sleep": "^1.0.0" @@ -12343,6 +12677,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -12352,6 +12687,7 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", "dev": true, + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -12362,6 +12698,7 @@ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -12371,23 +12708,26 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true + "dev": true, + "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, + "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "node_modules/spdx-license-ids": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", - "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", - "dev": true + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" }, "node_modules/split2": { "version": "4.2.0", @@ -12402,13 +12742,15 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "dev": true, + "license": "MIT", "dependencies": { "escape-string-regexp": "^2.0.0" }, @@ -12421,6 +12763,7 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -12488,6 +12831,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "get-stdin": "^8.0.0", "minimist": "^1.2.6", @@ -12498,10 +12842,17 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/standard/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/standard/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -12514,6 +12865,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.7.0.tgz", "integrity": "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==", "dev": true, + "license": "MIT", "dependencies": { "builtins": "^5.0.1", "eslint-plugin-es": "^4.1.0", @@ -12539,6 +12891,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", "dev": true, + "license": "MIT", "dependencies": { "eslint-utils": "^2.0.0", "regexpp": "^3.0.0" @@ -12558,6 +12911,7 @@ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", "dev": true, + "license": "MIT", "dependencies": { "eslint-visitor-keys": "^1.1.0" }, @@ -12573,6 +12927,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=4" } @@ -12582,6 +12937,7 @@ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", "dev": true, + "license": "MIT", "dependencies": { "eslint-visitor-keys": "^2.0.0" }, @@ -12600,14 +12956,15 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=10" } }, "node_modules/standard/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "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": { @@ -12618,24 +12975,41 @@ } }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/strict-event-emitter": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", "dev": true, + "license": "MIT", "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" @@ -12649,6 +13023,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -12691,6 +13066,7 @@ "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz", "integrity": "sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -12761,6 +13137,7 @@ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -12778,6 +13155,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -12790,6 +13168,7 @@ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -12799,6 +13178,7 @@ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -12807,6 +13187,8 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -12819,6 +13201,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^3.0.0" }, @@ -12830,6 +13213,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -12837,17 +13221,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true - }, "node_modules/table-layout": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", "dev": true, + "license": "MIT", "dependencies": { "array-back": "^4.0.1", "deep-extend": "~0.6.0", @@ -12863,6 +13242,7 @@ "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, + "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", @@ -12872,10 +13252,17 @@ "node": ">=8" } }, + "node_modules/test-exclude/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/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -12884,9 +13271,9 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "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": { @@ -12900,46 +13287,34 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/thread-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", - "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz", + "integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==", "license": "MIT", "dependencies": { "real-require": "^0.2.0" - } - }, - "node_modules/tldts": { - "version": "6.1.85", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.85.tgz", - "integrity": "sha512-gBdZ1RjCSevRPFix/hpaUWeak2/RNUZB4/8frF1r5uYMHjFptkiT0JXIebWvgI/0ZHXvxaUDDJshiA0j6GdL3w==", - "dev": true, - "dependencies": { - "tldts-core": "^6.1.85" }, - "bin": { - "tldts": "bin/cli.js" + "engines": { + "node": ">=20" } }, - "node_modules/tldts-core": { - "version": "6.1.85", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.85.tgz", - "integrity": "sha512-DTjUVvxckL1fIoPSb3KE7ISNtkWSawZdpfxGxwiIrZoO6EbHVDXXUIlIuWympPaeS+BLGyggozX/HTMsRAdsoA==", - "dev": true - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "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" }, @@ -12951,6 +13326,7 @@ "version": "3.7.0", "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", "engines": { "node": ">=12" } @@ -12959,51 +13335,27 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", "engines": { "node": ">=0.6" } }, "node_modules/touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", "dev": true, - "dependencies": { - "nopt": "~1.0.10" - }, + "license": "ISC", "bin": { "nodetouch": "bin/nodetouch.js" } }, - "node_modules/tough-cookie": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", - "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", - "dev": true, - "dependencies": { - "tldts": "^6.1.32" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tr46": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.0.tgz", - "integrity": "sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==", - "dev": true, - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, + "license": "MIT", "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", @@ -13016,6 +13368,7 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, + "license": "MIT", "dependencies": { "minimist": "^1.2.0" }, @@ -13028,21 +13381,24 @@ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "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/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -13055,15 +13411,17 @@ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -13072,12 +13430,14 @@ } }, "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" }, "engines": { "node": ">= 0.6" @@ -13166,6 +13526,7 @@ "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -13193,41 +13554,42 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/undici": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.7.0.tgz", - "integrity": "sha512-tZ6+5NBq4KH35rr46XJ2JPFKxfcBlYNaqLF/wyWIO9RMHqqU/gx/CLB1Y2qMcgB8lWw/bKHa7qzspqCN7mUHvA==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.5.tgz", + "integrity": "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==", + "license": "MIT", "engines": { "node": ">=20.18.1" } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" }, "node_modules/universal-github-app-jwt": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-1.2.0.tgz", - "integrity": "sha512-dncpMpnsKBk0eetwfN8D8OUHGfiDhhJ+mtsbMl+7PfW7mYjiH8LIcqRmYMtzYLgSh47HjfdBtrBwIQ/gizKR3g==", - "license": "MIT", - "dependencies": { - "@types/jsonwebtoken": "^9.0.0", - "jsonwebtoken": "^9.0.2" - } + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.2.tgz", + "integrity": "sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==", + "license": "MIT" }, "node_modules/universal-user-agent": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", - "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "license": "ISC" }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.0.0" } @@ -13236,14 +13598,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/update-browserslist-db": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.14.tgz", - "integrity": "sha512-JixKH8GR2pWYshIPUg/NujK3JO7JiqEEUiNArE86NQyrgUuZeTlZQN3xuS/yiV5Kb48ev9K6RqNkaJjXsdg7Jw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -13259,9 +13622,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.0" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -13274,6 +13638,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-dotenv/-/update-dotenv-1.1.1.tgz", "integrity": "sha512-3cIC18In/t0X/yH793c00qqxcKD8jVCgNOPif/fGQkFpYMGecM9YAc+kaAKXuZsM2dE9I9wFI7KvAuNX22SGMQ==", + "license": "ISC", "peerDependencies": { "dotenv": "*" } @@ -13283,31 +13648,27 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } }, "node_modules/v8-to-istanbul": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", - "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", "dev": true, + "license": "ISC", "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", @@ -13322,118 +13683,47 @@ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, + "license": "Apache-2.0", "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, - "node_modules/validator": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", - "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/version-guard": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/version-guard/-/version-guard-1.1.2.tgz", - "integrity": "sha512-D8d+YxCUpoqtCnQzDxm6SF7DLU3gr2535T4khAtMq4osBahsQnmSxuwXFdrbAdDGG8Uokzfis/jvyeFPdmlc7w==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/version-guard/-/version-guard-1.1.3.tgz", + "integrity": "sha512-JwPr6erhX53EWH/HCSzfy1tTFrtPXUe927wdM1jqBBeYp1OM+qPHjWbsvv6pIBduqdgxxS+ScfG7S28pzyr2DQ==", "dev": true, + "license": "0BSD", "engines": { "node": ">=0.10.48" } }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "makeerror": "1.0.12" } }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "dev": true, - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "dev": true, - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, "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" }, @@ -13512,16 +13802,17 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", - "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "for-each": "^0.3.3", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, @@ -13537,6 +13828,7 @@ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -13546,6 +13838,7 @@ "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", "dev": true, + "license": "MIT", "dependencies": { "reduce-flatten": "^2.0.0", "typical": "^5.2.0" @@ -13559,6 +13852,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -13576,6 +13870,7 @@ "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" }, @@ -13591,6 +13886,7 @@ "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" }, @@ -13602,18 +13898,21 @@ "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 + "dev": true, + "license": "MIT" }, "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==" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" }, "node_modules/write-file-atomic": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", "dev": true, + "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" @@ -13622,32 +13921,12 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", - "dev": true, - "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/xdg-basedir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -13656,22 +13935,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", - "dev": true - }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true + "license": "MIT" }, "node_modules/xtend": { "version": "4.0.2", @@ -13687,6 +13952,7 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } @@ -13695,13 +13961,30 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } }, "node_modules/yargs": { "version": "17.7.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", "dev": true, + "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -13720,6 +14003,7 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } @@ -13729,6 +14013,7 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, diff --git a/package.json b/package.json index fbfa284da..db6bed660 100644 --- a/package.json +++ b/package.json @@ -20,22 +20,25 @@ "test:me": "jest ", "test:unit:watch": "npm run test:unit -- --watch", "test:integration": "jest --roots=lib --roots=test/integration", - "test:integration:debug": "LOG_LEVEL=debug DEBUG=nock run-s test:integration" + "test:integration:debug": "LOG_LEVEL=debug DEBUG=nock run-s test:integration", + "smoke-test": "node smoke-test.js", + "smoke-test:interactive": "node smoke-test.js --interactive", + "smoke-test:phase": "node smoke-test.js --phase" }, "author": "Yadhav Jayaraman", "license": "ISC", "dependencies": { - "@apidevtools/json-schema-ref-parser": "^12.0.2", + "@apidevtools/json-schema-ref-parser": "^14.2.1", "@probot/adapter-aws-lambda-serverless": "^4.0.3", "deepmerge": "^4.3.1", "eta": "^3.5.0", "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "minimatch": "^10.0.1", - "node-cron": "^3.0.2", + "lodash": "^4.18.1", + "minimatch": "^10.2.1", + "node-cron": "^4.2.1", "octokit": "^5.0.2", - "probot": "^13.4.4", - "proxy-from-env": "^1.1.0", + "probot": "^14.3.2", + "proxy-from-env": "^2.1.0", "undici": "^7.7.0" }, "devDependencies": { @@ -55,7 +58,7 @@ "nock": "^14.0.1", "nodemon": "^3.1.9", "npm-run-all": "^4.1.5", - "smee-client": "^4.3.1", + "smee-client": "^5.0.0", "standard": "^17.1.2" }, "standard": { @@ -64,10 +67,13 @@ ] }, "engines": { - "node": ">= 16.0.0" + "node": ">= 22.0.0" }, "jest": { "testEnvironment": "node", + "setupFilesAfterEnv": [ + "/test/setup.js" + ], "reporters": [ "default", "jest-junit" @@ -90,4 +96,4 @@ "." ] } -} +} \ No newline at end of file diff --git a/schema/dereferenced/repos.json b/schema/dereferenced/repos.json new file mode 100644 index 000000000..59bf0a050 --- /dev/null +++ b/schema/dereferenced/repos.json @@ -0,0 +1,2845 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Safe-settings repo-level configuration", + "description": "Schema for .github/repos/{repo-name}.yml — repo-level safe-settings override configuration. Settings here are merged on top of org-level and suborg-level settings for the specific repository.", + "type": "object", + "properties": { + "repositories": { + "description": "Repository settings", + "allOf": [ + { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the repository." + }, + "description": { + "type": "string", + "description": "A short description of the repository." + }, + "homepage": { + "type": "string", + "description": "A URL with more information about the repository." + }, + "private": { + "type": "boolean", + "description": "Either `true` to make the repository private or `false` to make it public. Default: `false`. \n**Note**: You will get a `422` error if the organization restricts [changing repository visibility](https://docs.github.com/articles/repository-permission-levels-for-an-organization#changing-the-visibility-of-repositories) to organization owners and a non-owner tries to change the value of private.", + "default": false + }, + "visibility": { + "type": "string", + "description": "The visibility of the repository.", + "enum": [ + "public", + "private" + ] + }, + "security_and_analysis": { + "type": "object", + "description": "Specify which security and analysis features to enable or disable for the repository.\n\nTo use this parameter, you must have admin permissions for the repository or be an owner or security manager for the organization that owns the repository. For more information, see \"[Managing security managers in your organization](https://docs.github.com/organizations/managing-peoples-access-to-your-organization-with-roles/managing-security-managers-in-your-organization).\"\n\nFor example, to enable GitHub Advanced Security, use this data in the body of the `PATCH` request:\n`{ \"security_and_analysis\": {\"advanced_security\": { \"status\": \"enabled\" } } }`.\n\nYou can check which security and analysis features are currently enabled by using a `GET /repos/{owner}/{repo}` request.", + "nullable": true, + "properties": { + "advanced_security": { + "type": "object", + "description": "Use the `status` property to enable or disable GitHub Advanced Security for this repository.\nFor more information, see \"[About GitHub Advanced\nSecurity](/github/getting-started-with-github/learning-about-github/about-github-advanced-security).\"\n\nFor standalone Code Scanning or Secret Protection products, this parameter cannot be used.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "code_security": { + "type": "object", + "description": "Use the `status` property to enable or disable GitHub Code Security for this repository.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning for this repository. For more information, see \"[About secret scanning](/code-security/secret-security/about-secret-scanning).\"", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_push_protection": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning push protection for this repository. For more information, see \"[Protecting pushes with secret scanning](/code-security/secret-scanning/protecting-pushes-with-secret-scanning).\"", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_ai_detection": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning AI detection for this repository. For more information, see \"[Responsible detection of generic secrets with AI](https://docs.github.com/code-security/secret-scanning/using-advanced-secret-scanning-and-push-protection-features/generic-secret-detection/responsible-ai-generic-secrets).\"", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_non_provider_patterns": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning non-provider patterns for this repository. For more information, see \"[Supported secret scanning patterns](/code-security/secret-scanning/introduction/supported-secret-scanning-patterns#supported-secrets).\"", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_delegated_alert_dismissal": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning delegated alert dismissal for this repository.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_delegated_bypass": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning delegated bypass for this repository.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_delegated_bypass_options": { + "type": "object", + "description": "Feature options for secret scanning delegated bypass.\nThis object is only honored when `security_and_analysis.secret_scanning_delegated_bypass.status` is set to `enabled`.\nYou can send this object in the same request as `secret_scanning_delegated_bypass`, or update just the options in a separate request.", + "properties": { + "reviewers": { + "type": "array", + "description": "The bypass reviewers for secret scanning delegated bypass.\nIf you omit this field, the existing set of reviewers is unchanged.", + "items": { + "type": "object", + "required": [ + "reviewer_id", + "reviewer_type" + ], + "properties": { + "reviewer_id": { + "type": "integer", + "description": "The ID of the team or role selected as a bypass reviewer" + }, + "reviewer_type": { + "type": "string", + "description": "The type of the bypass reviewer", + "enum": [ + "TEAM", + "ROLE" + ] + }, + "mode": { + "type": "string", + "description": "The bypass mode for the reviewer", + "enum": [ + "ALWAYS", + "EXEMPT" + ], + "default": "ALWAYS" + } + } + } + } + } + } + } + }, + "has_issues": { + "type": "boolean", + "description": "Either `true` to enable issues for this repository or `false` to disable them.", + "default": true + }, + "has_projects": { + "type": "boolean", + "description": "Either `true` to enable projects for this repository or `false` to disable them. **Note:** If you're creating a repository in an organization that has disabled repository projects, the default is `false`, and if you pass `true`, the API returns an error.", + "default": true + }, + "has_wiki": { + "type": "boolean", + "description": "Either `true` to enable the wiki for this repository or `false` to disable it.", + "default": true + }, + "has_pull_requests": { + "type": "boolean", + "description": "Either `true` to allow pull requests for this repository or `false` to prevent pull requests.", + "default": true + }, + "pull_request_creation_policy": { + "type": "string", + "description": "The policy that controls who can create pull requests for this repository: `all` or `collaborators_only`.", + "enum": [ + "all", + "collaborators_only" + ] + }, + "is_template": { + "type": "boolean", + "description": "Either `true` to make this repo available as a template repository or `false` to prevent it.", + "default": false + }, + "default_branch": { + "type": "string", + "description": "Updates the default branch for this repository." + }, + "allow_squash_merge": { + "type": "boolean", + "description": "Either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging.", + "default": true + }, + "allow_merge_commit": { + "type": "boolean", + "description": "Either `true` to allow merging pull requests with a merge commit, or `false` to prevent merging pull requests with merge commits.", + "default": true + }, + "allow_rebase_merge": { + "type": "boolean", + "description": "Either `true` to allow rebase-merging pull requests, or `false` to prevent rebase-merging.", + "default": true + }, + "allow_auto_merge": { + "type": "boolean", + "description": "Either `true` to allow auto-merge on pull requests, or `false` to disallow auto-merge.", + "default": false + }, + "delete_branch_on_merge": { + "type": "boolean", + "description": "Either `true` to allow automatically deleting head branches when pull requests are merged, or `false` to prevent automatic deletion.", + "default": false + }, + "allow_update_branch": { + "type": "boolean", + "description": "Either `true` to always allow a pull request head branch that is behind its base branch to be updated even if it is not required to be up to date before merging, or false otherwise.", + "default": false + }, + "use_squash_pr_title_as_default": { + "type": "boolean", + "description": "Either `true` to allow squash-merge commits to use pull request title, or `false` to use commit message. **This property is closing down. Please use `squash_merge_commit_title` instead.", + "default": false, + "deprecated": true + }, + "squash_merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "COMMIT_OR_PR_TITLE" + ], + "description": "Required when using `squash_merge_commit_message`.\n\nThe default value for a squash merge commit title:\n\n- `PR_TITLE` - default to the pull request's title.\n- `COMMIT_OR_PR_TITLE` - default to the commit's title (if only one commit) or the pull request's title (when more than one commit)." + }, + "squash_merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "COMMIT_MESSAGES", + "BLANK" + ], + "description": "The default value for a squash merge commit message:\n\n- `PR_BODY` - default to the pull request's body.\n- `COMMIT_MESSAGES` - default to the branch's commit messages.\n- `BLANK` - default to a blank commit message." + }, + "merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "MERGE_MESSAGE" + ], + "description": "Required when using `merge_commit_message`.\n\nThe default value for a merge commit title.\n\n- `PR_TITLE` - default to the pull request's title.\n- `MERGE_MESSAGE` - default to the classic title for a merge message (e.g., Merge pull request #123 from branch-name)." + }, + "merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "PR_TITLE", + "BLANK" + ], + "description": "The default value for a merge commit message.\n\n- `PR_TITLE` - default to the pull request's title.\n- `PR_BODY` - default to the pull request's body.\n- `BLANK` - default to a blank commit message." + }, + "archived": { + "type": "boolean", + "description": "Whether to archive this repository. `false` will unarchive a previously archived repository.", + "default": false + }, + "allow_forking": { + "type": "boolean", + "description": "Either `true` to allow private forks, or `false` to prevent private forks.", + "default": false + }, + "web_commit_signoff_required": { + "type": "boolean", + "description": "Either `true` to require contributors to sign off on web-based commits, or `false` to not require contributors to sign off on web-based commits.", + "default": false + } + } + }, + { + "type": "object", + "properties": { + "auto_init": { + "description": "Create an initial commit with empty README. Keep this set to true in most cases since many of the policies below cannot be implemented on bare repos", + "type": "boolean" + }, + "gitignore_template": { + "description": "Desired language or platform [.gitignore template](https://github.com/github/gitignore) to apply. Use the name of the template without the extension. For example, 'Haskell'.", + "type": "string" + }, + "license_template": { + "description": "Choose an [open source license template](https://choosealicense.com/) that best suits your needs, and then use the [license keyword](https://help.github.com/articles/licensing-a-repository/#searching-github-by-license-type) as the `license_template` string. For example, 'mit' or 'mpl-2.0'.", + "type": "string" + }, + "topics": { + "description": "A list of topics to set on the repository", + "type": "array", + "items": { + "type": "string" + } + }, + "security": { + "description": "Settings for Code security and analysis", + "type": "object", + "properties": { + "enableVulnerabilityAlerts": { + "type": "boolean" + }, + "enableAutomatedSecurityFixes": { + "type": "boolean" + } + } + }, + "force_create": { + "description": "If true, create the repository if it does not already exist.", + "type": "boolean" + }, + "template": { + "description": "Name of a template repository to use when creating a new repository.", + "type": "string" + } + } + } + ] + }, + "labels": { + "description": "Labels: define labels for Issues and Pull Requests", + "type": "object", + "properties": { + "include": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "color": { + "description": "The hexadecimal color code for the label. If including a `#`, make sure to wrap it with quotes!", + "type": "string" + }, + "description": { + "type": "string" + }, + "oldname": { + "description": "Include the old name to rename an existing label", + "type": "string" + } + } + } + }, + "exclude": { + "description": "Ignore any labels matching these regexes (don't delete them)", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "collaborators": { + "description": "Collaborators: give specific users access to any repository.", + "type": "array", + "items": { + "description": "A collaborator entry giving a specific user access to a repository.", + "allOf": [ + { + "type": "object", + "properties": { + "permission": { + "type": "string", + "description": "The permission to grant the collaborator. **Only valid on organization-owned repositories.** We accept the following permissions to be set: `pull`, `triage`, `push`, `maintain`, `admin` and you can also specify a custom repository role name, if the owning organization has defined any.", + "default": "push" + } + } + }, + { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "exclude": { + "description": "You can exclude a list of repos for this collaborator and all repos except these repos would have this collaborator", + "type": "array", + "items": { + "type": "string" + } + }, + "include": { + "description": "You can include a list of repos for this collaborator and only those repos would have this collaborator", + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + } + }, + "teams": { + "description": "Teams", + "type": "array", + "items": { + "description": "A team entry", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the team." + }, + "description": { + "type": "string", + "description": "The description of the team." + }, + "maintainers": { + "type": "array", + "description": "List GitHub usernames for organization members who will become team maintainers.", + "items": { + "type": "string" + } + }, + "repo_names": { + "type": "array", + "description": "The full name (e.g., \"organization-name/repository-name\") of repositories to add the team to.", + "items": { + "type": "string" + } + }, + "privacy": { + "type": "string", + "description": "The level of privacy this team should have. The options are: \n**For a non-nested team:** \n * `secret` - only visible to organization owners and members of this team. \n * `closed` - visible to all members of this organization. \nDefault: `secret` \n**For a parent or child team:** \n * `closed` - visible to all members of this organization. \nDefault for child team: `closed`", + "enum": [ + "secret", + "closed" + ] + }, + "notification_setting": { + "type": "string", + "description": "The notification setting the team has chosen. The options are: \n * `notifications_enabled` - team members receive notifications when the team is @mentioned. \n * `notifications_disabled` - no one receives notifications. \nDefault: `notifications_enabled`", + "enum": [ + "notifications_enabled", + "notifications_disabled" + ] + }, + "parent_team_id": { + "type": "integer", + "description": "The ID of a team to set as the parent team." + }, + "parent_team_slug": { + "type": "string", + "description": "The slug of a team to set as the parent team. Ignored when `parent_team_id` is also provided." + } + }, + "required": [ + "name" + ] + } + }, + "milestones": { + "description": "Milestones: define milestones for Issues and Pull Requests", + "type": "array", + "items": { + "description": "A milestone entry", + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "state": { + "description": "The state of the milestone. Either `open` or `closed`", + "type": "string" + } + } + } + }, + "branches": { + "description": "Branch protection rules", + "type": "array", + "items": { + "description": "A branch protection rule entry", + "type": "object", + "properties": { + "name": { + "description": "If the name of the branch value is specified as `default`, then the app will create a branch protection rule to apply against the default branch in the repo", + "type": "string" + }, + "protection": { + "type": "object", + "properties": { + "required_status_checks": { + "type": "object", + "description": "Require status checks to pass before merging. Set to `null` to disable.", + "nullable": true, + "properties": { + "strict": { + "type": "boolean", + "description": "Require branches to be up to date before merging." + }, + "contexts": { + "type": "array", + "deprecated": true, + "description": "**Closing down notice**: The list of status checks to require in order to merge into this branch. If any of these checks have recently been set by a particular GitHub App, they will be required to come from that app in future for the branch to merge. Use `checks` instead of `contexts` for more fine-grained control.", + "items": { + "type": "string" + } + }, + "checks": { + "type": "array", + "description": "The list of status checks to require in order to merge into this branch.", + "items": { + "type": "object", + "required": [ + "context" + ], + "properties": { + "context": { + "type": "string", + "description": "The name of the required check" + }, + "app_id": { + "type": "integer", + "description": "The ID of the GitHub App that must provide this check. Omit this field to automatically select the GitHub App that has recently provided this check, or any app if it was not set by a GitHub App. Pass -1 to explicitly allow any app to set the status." + } + } + } + } + }, + "required": [ + "strict", + "contexts" + ] + }, + "enforce_admins": { + "type": "boolean", + "description": "Enforce all configured restrictions for administrators. Set to `true` to enforce required status checks for repository administrators. Set to `null` to disable.", + "nullable": true + }, + "required_pull_request_reviews": { + "type": "object", + "description": "Require at least one approving review on a pull request, before merging. Set to `null` to disable.", + "nullable": true, + "properties": { + "dismissal_restrictions": { + "type": "object", + "description": "Specify which users, teams, and apps can dismiss pull request reviews. Pass an empty `dismissal_restrictions` object to disable. User and team `dismissal_restrictions` are only available for organization-owned repositories. Omit this parameter for personal repositories.", + "properties": { + "users": { + "type": "array", + "description": "The list of user `login`s with dismissal access", + "items": { + "type": "string" + } + }, + "teams": { + "type": "array", + "description": "The list of team `slug`s with dismissal access", + "items": { + "type": "string" + } + }, + "apps": { + "type": "array", + "description": "The list of app `slug`s with dismissal access", + "items": { + "type": "string" + } + } + } + }, + "dismiss_stale_reviews": { + "type": "boolean", + "description": "Set to `true` if you want to automatically dismiss approving reviews when someone pushes a new commit." + }, + "require_code_owner_reviews": { + "type": "boolean", + "description": "Blocks merging pull requests until [code owners](https://docs.github.com/articles/about-code-owners/) review them." + }, + "required_approving_review_count": { + "type": "integer", + "description": "Specify the number of reviewers required to approve pull requests. Use a number between 1 and 6 or 0 to not require reviewers." + }, + "require_last_push_approval": { + "type": "boolean", + "description": "Whether the most recent push must be approved by someone other than the person who pushed it. Default: `false`.", + "default": false + }, + "bypass_pull_request_allowances": { + "type": "object", + "description": "Allow specific users, teams, or apps to bypass pull request requirements.", + "properties": { + "users": { + "type": "array", + "description": "The list of user `login`s allowed to bypass pull request requirements.", + "items": { + "type": "string" + } + }, + "teams": { + "type": "array", + "description": "The list of team `slug`s allowed to bypass pull request requirements.", + "items": { + "type": "string" + } + }, + "apps": { + "type": "array", + "description": "The list of app `slug`s allowed to bypass pull request requirements.", + "items": { + "type": "string" + } + } + } + } + } + }, + "restrictions": { + "type": "object", + "description": "Restrict who can push to the protected branch. User, app, and team `restrictions` are only available for organization-owned repositories. Set to `null` to disable.", + "nullable": true, + "properties": { + "users": { + "type": "array", + "description": "The list of user `login`s with push access", + "items": { + "type": "string" + } + }, + "teams": { + "type": "array", + "description": "The list of team `slug`s with push access", + "items": { + "type": "string" + } + }, + "apps": { + "type": "array", + "description": "The list of app `slug`s with push access", + "items": { + "type": "string" + } + } + }, + "required": [ + "users", + "teams" + ] + }, + "required_linear_history": { + "type": "boolean", + "description": "Enforces a linear commit Git history, which prevents anyone from pushing merge commits to a branch. Set to `true` to enforce a linear commit history. Set to `false` to disable a linear commit Git history. Your repository must allow squash merging or rebase merging before you can enable a linear commit history. Default: `false`. For more information, see \"[Requiring a linear commit history](https://docs.github.com/github/administering-a-repository/requiring-a-linear-commit-history)\" in the GitHub Help documentation." + }, + "allow_force_pushes": { + "type": "boolean", + "description": "Permits force pushes to the protected branch by anyone with write access to the repository. Set to `true` to allow force pushes. Set to `false` or `null` to block force pushes. Default: `false`. For more information, see \"[Enabling force pushes to a protected branch](https://docs.github.com/github/administering-a-repository/enabling-force-pushes-to-a-protected-branch)\" in the GitHub Help documentation.\"", + "nullable": true + }, + "allow_deletions": { + "type": "boolean", + "description": "Allows deletion of the protected branch by anyone with write access to the repository. Set to `false` to prevent deletion of the protected branch. Default: `false`. For more information, see \"[Enabling force pushes to a protected branch](https://docs.github.com/github/administering-a-repository/enabling-force-pushes-to-a-protected-branch)\" in the GitHub Help documentation." + }, + "block_creations": { + "type": "boolean", + "description": "If set to `true`, the `restrictions` branch protection settings which limits who can push will also block pushes which create new branches, unless the push is initiated by a user, team, or app which has the ability to push. Set to `true` to restrict new branch creation. Default: `false`." + }, + "required_conversation_resolution": { + "type": "boolean", + "description": "Requires all conversations on code to be resolved before a pull request can be merged into a branch that matches this rule. Set to `false` to disable. Default: `false`." + }, + "lock_branch": { + "type": "boolean", + "description": "Whether to set the branch as read-only. If this is true, users will not be able to push to the branch. Default: `false`.", + "default": false + }, + "allow_fork_syncing": { + "type": "boolean", + "description": "Whether users can pull changes from upstream when the branch is locked. Set to `true` to allow fork syncing. Set to `false` to prevent fork syncing. Default: `false`.", + "default": false + } + }, + "required": [ + "required_status_checks", + "enforce_admins", + "required_pull_request_reviews", + "restrictions" + ] + } + } + } + }, + "autolinks": { + "description": "Autolinks", + "type": "array", + "items": { + "description": "An autolink reference entry", + "type": "object", + "properties": { + "key_prefix": { + "type": "string", + "description": "This prefix appended by certain characters will generate a link any time it is found in an issue, pull request, or commit." + }, + "url_template": { + "type": "string", + "description": "The URL must contain `` for the reference number. `` matches different characters depending on the value of `is_alphanumeric`." + }, + "is_alphanumeric": { + "type": "boolean", + "default": true, + "description": "Whether this autolink reference matches alphanumeric characters. If true, the `` parameter of the `url_template` matches alphanumeric characters `A-Z` (case insensitive), `0-9`, and `-`. If false, this autolink reference only matches numeric characters." + } + }, + "required": [ + "key_prefix", + "url_template" + ] + } + }, + "validator": { + "description": "Repository name validation", + "type": "object", + "properties": { + "pattern": { + "type": "string" + } + } + }, + "environments": { + "description": "Deployment environments", + "type": "array", + "items": { + "description": "A deployment environment configuration entry", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "The name of the deployment environment", + "type": "string" + }, + "wait_timer": { + "description": "The amount of time to delay a job after the job is initially triggered (in minutes)", + "type": "integer" + }, + "reviewers": { + "description": "The people or teams that may review jobs that reference the environment", + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "description": "The type of reviewer (`User` or `Team`)", + "type": "string" + }, + "id": { + "description": "The id of the user or team who can review the deployment", + "type": "integer" + } + } + } + }, + "deployment_branch_policy": { + "description": "The type of deployment branch policy for this environment", + "type": "object", + "properties": { + "protected_branches": { + "description": "Whether only protected branches can be deployed to this environment", + "type": "boolean" + }, + "custom_branch_policies": { + "description": "Whether only branches that match the specified name patterns can deploy to this environment", + "type": "boolean" + } + } + }, + "prevent_self_review": { + "description": "Whether or not a user who created the job is prevented from approving their own job", + "type": "boolean" + } + } + } + }, + "custom_properties": { + "description": "Custom properties", + "type": "array", + "items": { + "description": "A custom property entry", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + }, + "variables": { + "description": "Repository or org-level Actions variables", + "type": "array", + "items": { + "description": "An Actions variable entry", + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "description": "The name of the variable", + "type": "string" + }, + "value": { + "description": "The value of the variable", + "type": "string" + }, + "visibility": { + "description": "The visibility of the variable. Can be `all`, `private`, or `selected`", + "type": "string", + "enum": [ + "all", + "private", + "selected" + ] + } + } + } + } + }, + "$defs": { + "RepositorySettings": { + "description": "Repository settings", + "allOf": [ + { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the repository." + }, + "description": { + "type": "string", + "description": "A short description of the repository." + }, + "homepage": { + "type": "string", + "description": "A URL with more information about the repository." + }, + "private": { + "type": "boolean", + "description": "Either `true` to make the repository private or `false` to make it public. Default: `false`. \n**Note**: You will get a `422` error if the organization restricts [changing repository visibility](https://docs.github.com/articles/repository-permission-levels-for-an-organization#changing-the-visibility-of-repositories) to organization owners and a non-owner tries to change the value of private.", + "default": false + }, + "visibility": { + "type": "string", + "description": "The visibility of the repository.", + "enum": [ + "public", + "private" + ] + }, + "security_and_analysis": { + "type": "object", + "description": "Specify which security and analysis features to enable or disable for the repository.\n\nTo use this parameter, you must have admin permissions for the repository or be an owner or security manager for the organization that owns the repository. For more information, see \"[Managing security managers in your organization](https://docs.github.com/organizations/managing-peoples-access-to-your-organization-with-roles/managing-security-managers-in-your-organization).\"\n\nFor example, to enable GitHub Advanced Security, use this data in the body of the `PATCH` request:\n`{ \"security_and_analysis\": {\"advanced_security\": { \"status\": \"enabled\" } } }`.\n\nYou can check which security and analysis features are currently enabled by using a `GET /repos/{owner}/{repo}` request.", + "nullable": true, + "properties": { + "advanced_security": { + "type": "object", + "description": "Use the `status` property to enable or disable GitHub Advanced Security for this repository.\nFor more information, see \"[About GitHub Advanced\nSecurity](/github/getting-started-with-github/learning-about-github/about-github-advanced-security).\"\n\nFor standalone Code Scanning or Secret Protection products, this parameter cannot be used.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "code_security": { + "type": "object", + "description": "Use the `status` property to enable or disable GitHub Code Security for this repository.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning for this repository. For more information, see \"[About secret scanning](/code-security/secret-security/about-secret-scanning).\"", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_push_protection": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning push protection for this repository. For more information, see \"[Protecting pushes with secret scanning](/code-security/secret-scanning/protecting-pushes-with-secret-scanning).\"", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_ai_detection": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning AI detection for this repository. For more information, see \"[Responsible detection of generic secrets with AI](https://docs.github.com/code-security/secret-scanning/using-advanced-secret-scanning-and-push-protection-features/generic-secret-detection/responsible-ai-generic-secrets).\"", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_non_provider_patterns": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning non-provider patterns for this repository. For more information, see \"[Supported secret scanning patterns](/code-security/secret-scanning/introduction/supported-secret-scanning-patterns#supported-secrets).\"", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_delegated_alert_dismissal": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning delegated alert dismissal for this repository.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_delegated_bypass": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning delegated bypass for this repository.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_delegated_bypass_options": { + "type": "object", + "description": "Feature options for secret scanning delegated bypass.\nThis object is only honored when `security_and_analysis.secret_scanning_delegated_bypass.status` is set to `enabled`.\nYou can send this object in the same request as `secret_scanning_delegated_bypass`, or update just the options in a separate request.", + "properties": { + "reviewers": { + "type": "array", + "description": "The bypass reviewers for secret scanning delegated bypass.\nIf you omit this field, the existing set of reviewers is unchanged.", + "items": { + "type": "object", + "required": [ + "reviewer_id", + "reviewer_type" + ], + "properties": { + "reviewer_id": { + "type": "integer", + "description": "The ID of the team or role selected as a bypass reviewer" + }, + "reviewer_type": { + "type": "string", + "description": "The type of the bypass reviewer", + "enum": [ + "TEAM", + "ROLE" + ] + }, + "mode": { + "type": "string", + "description": "The bypass mode for the reviewer", + "enum": [ + "ALWAYS", + "EXEMPT" + ], + "default": "ALWAYS" + } + } + } + } + } + } + } + }, + "has_issues": { + "type": "boolean", + "description": "Either `true` to enable issues for this repository or `false` to disable them.", + "default": true + }, + "has_projects": { + "type": "boolean", + "description": "Either `true` to enable projects for this repository or `false` to disable them. **Note:** If you're creating a repository in an organization that has disabled repository projects, the default is `false`, and if you pass `true`, the API returns an error.", + "default": true + }, + "has_wiki": { + "type": "boolean", + "description": "Either `true` to enable the wiki for this repository or `false` to disable it.", + "default": true + }, + "has_pull_requests": { + "type": "boolean", + "description": "Either `true` to allow pull requests for this repository or `false` to prevent pull requests.", + "default": true + }, + "pull_request_creation_policy": { + "type": "string", + "description": "The policy that controls who can create pull requests for this repository: `all` or `collaborators_only`.", + "enum": [ + "all", + "collaborators_only" + ] + }, + "is_template": { + "type": "boolean", + "description": "Either `true` to make this repo available as a template repository or `false` to prevent it.", + "default": false + }, + "default_branch": { + "type": "string", + "description": "Updates the default branch for this repository." + }, + "allow_squash_merge": { + "type": "boolean", + "description": "Either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging.", + "default": true + }, + "allow_merge_commit": { + "type": "boolean", + "description": "Either `true` to allow merging pull requests with a merge commit, or `false` to prevent merging pull requests with merge commits.", + "default": true + }, + "allow_rebase_merge": { + "type": "boolean", + "description": "Either `true` to allow rebase-merging pull requests, or `false` to prevent rebase-merging.", + "default": true + }, + "allow_auto_merge": { + "type": "boolean", + "description": "Either `true` to allow auto-merge on pull requests, or `false` to disallow auto-merge.", + "default": false + }, + "delete_branch_on_merge": { + "type": "boolean", + "description": "Either `true` to allow automatically deleting head branches when pull requests are merged, or `false` to prevent automatic deletion.", + "default": false + }, + "allow_update_branch": { + "type": "boolean", + "description": "Either `true` to always allow a pull request head branch that is behind its base branch to be updated even if it is not required to be up to date before merging, or false otherwise.", + "default": false + }, + "use_squash_pr_title_as_default": { + "type": "boolean", + "description": "Either `true` to allow squash-merge commits to use pull request title, or `false` to use commit message. **This property is closing down. Please use `squash_merge_commit_title` instead.", + "default": false, + "deprecated": true + }, + "squash_merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "COMMIT_OR_PR_TITLE" + ], + "description": "Required when using `squash_merge_commit_message`.\n\nThe default value for a squash merge commit title:\n\n- `PR_TITLE` - default to the pull request's title.\n- `COMMIT_OR_PR_TITLE` - default to the commit's title (if only one commit) or the pull request's title (when more than one commit)." + }, + "squash_merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "COMMIT_MESSAGES", + "BLANK" + ], + "description": "The default value for a squash merge commit message:\n\n- `PR_BODY` - default to the pull request's body.\n- `COMMIT_MESSAGES` - default to the branch's commit messages.\n- `BLANK` - default to a blank commit message." + }, + "merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "MERGE_MESSAGE" + ], + "description": "Required when using `merge_commit_message`.\n\nThe default value for a merge commit title.\n\n- `PR_TITLE` - default to the pull request's title.\n- `MERGE_MESSAGE` - default to the classic title for a merge message (e.g., Merge pull request #123 from branch-name)." + }, + "merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "PR_TITLE", + "BLANK" + ], + "description": "The default value for a merge commit message.\n\n- `PR_TITLE` - default to the pull request's title.\n- `PR_BODY` - default to the pull request's body.\n- `BLANK` - default to a blank commit message." + }, + "archived": { + "type": "boolean", + "description": "Whether to archive this repository. `false` will unarchive a previously archived repository.", + "default": false + }, + "allow_forking": { + "type": "boolean", + "description": "Either `true` to allow private forks, or `false` to prevent private forks.", + "default": false + }, + "web_commit_signoff_required": { + "type": "boolean", + "description": "Either `true` to require contributors to sign off on web-based commits, or `false` to not require contributors to sign off on web-based commits.", + "default": false + } + } + }, + { + "type": "object", + "properties": { + "auto_init": { + "description": "Create an initial commit with empty README. Keep this set to true in most cases since many of the policies below cannot be implemented on bare repos", + "type": "boolean" + }, + "gitignore_template": { + "description": "Desired language or platform [.gitignore template](https://github.com/github/gitignore) to apply. Use the name of the template without the extension. For example, 'Haskell'.", + "type": "string" + }, + "license_template": { + "description": "Choose an [open source license template](https://choosealicense.com/) that best suits your needs, and then use the [license keyword](https://help.github.com/articles/licensing-a-repository/#searching-github-by-license-type) as the `license_template` string. For example, 'mit' or 'mpl-2.0'.", + "type": "string" + }, + "topics": { + "description": "A list of topics to set on the repository", + "type": "array", + "items": { + "type": "string" + } + }, + "security": { + "description": "Settings for Code security and analysis", + "type": "object", + "properties": { + "enableVulnerabilityAlerts": { + "type": "boolean" + }, + "enableAutomatedSecurityFixes": { + "type": "boolean" + } + } + }, + "force_create": { + "description": "If true, create the repository if it does not already exist.", + "type": "boolean" + }, + "template": { + "description": "Name of a template repository to use when creating a new repository.", + "type": "string" + } + } + } + ] + }, + "LabelSettings": { + "description": "Labels: define labels for Issues and Pull Requests", + "type": "object", + "properties": { + "include": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "color": { + "description": "The hexadecimal color code for the label. If including a `#`, make sure to wrap it with quotes!", + "type": "string" + }, + "description": { + "type": "string" + }, + "oldname": { + "description": "Include the old name to rename an existing label", + "type": "string" + } + } + } + }, + "exclude": { + "description": "Ignore any labels matching these regexes (don't delete them)", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "CollaboratorSettings": { + "description": "A collaborator entry giving a specific user access to a repository.", + "allOf": [ + { + "type": "object", + "properties": { + "permission": { + "type": "string", + "description": "The permission to grant the collaborator. **Only valid on organization-owned repositories.** We accept the following permissions to be set: `pull`, `triage`, `push`, `maintain`, `admin` and you can also specify a custom repository role name, if the owning organization has defined any.", + "default": "push" + } + } + }, + { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "exclude": { + "description": "You can exclude a list of repos for this collaborator and all repos except these repos would have this collaborator", + "type": "array", + "items": { + "type": "string" + } + }, + "include": { + "description": "You can include a list of repos for this collaborator and only those repos would have this collaborator", + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + }, + "TeamSettings": { + "description": "A team entry", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the team." + }, + "description": { + "type": "string", + "description": "The description of the team." + }, + "maintainers": { + "type": "array", + "description": "List GitHub usernames for organization members who will become team maintainers.", + "items": { + "type": "string" + } + }, + "repo_names": { + "type": "array", + "description": "The full name (e.g., \"organization-name/repository-name\") of repositories to add the team to.", + "items": { + "type": "string" + } + }, + "privacy": { + "type": "string", + "description": "The level of privacy this team should have. The options are: \n**For a non-nested team:** \n * `secret` - only visible to organization owners and members of this team. \n * `closed` - visible to all members of this organization. \nDefault: `secret` \n**For a parent or child team:** \n * `closed` - visible to all members of this organization. \nDefault for child team: `closed`", + "enum": [ + "secret", + "closed" + ] + }, + "notification_setting": { + "type": "string", + "description": "The notification setting the team has chosen. The options are: \n * `notifications_enabled` - team members receive notifications when the team is @mentioned. \n * `notifications_disabled` - no one receives notifications. \nDefault: `notifications_enabled`", + "enum": [ + "notifications_enabled", + "notifications_disabled" + ] + }, + "parent_team_id": { + "type": "integer", + "description": "The ID of a team to set as the parent team." + }, + "parent_team_slug": { + "type": "string", + "description": "The slug of a team to set as the parent team. Ignored when `parent_team_id` is also provided." + } + }, + "required": [ + "name" + ] + }, + "MilestoneSettings": { + "description": "A milestone entry", + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "state": { + "description": "The state of the milestone. Either `open` or `closed`", + "type": "string" + } + } + }, + "BranchSettings": { + "description": "A branch protection rule entry", + "type": "object", + "properties": { + "name": { + "description": "If the name of the branch value is specified as `default`, then the app will create a branch protection rule to apply against the default branch in the repo", + "type": "string" + }, + "protection": { + "type": "object", + "properties": { + "required_status_checks": { + "type": "object", + "description": "Require status checks to pass before merging. Set to `null` to disable.", + "nullable": true, + "properties": { + "strict": { + "type": "boolean", + "description": "Require branches to be up to date before merging." + }, + "contexts": { + "type": "array", + "deprecated": true, + "description": "**Closing down notice**: The list of status checks to require in order to merge into this branch. If any of these checks have recently been set by a particular GitHub App, they will be required to come from that app in future for the branch to merge. Use `checks` instead of `contexts` for more fine-grained control.", + "items": { + "type": "string" + } + }, + "checks": { + "type": "array", + "description": "The list of status checks to require in order to merge into this branch.", + "items": { + "type": "object", + "required": [ + "context" + ], + "properties": { + "context": { + "type": "string", + "description": "The name of the required check" + }, + "app_id": { + "type": "integer", + "description": "The ID of the GitHub App that must provide this check. Omit this field to automatically select the GitHub App that has recently provided this check, or any app if it was not set by a GitHub App. Pass -1 to explicitly allow any app to set the status." + } + } + } + } + }, + "required": [ + "strict", + "contexts" + ] + }, + "enforce_admins": { + "type": "boolean", + "description": "Enforce all configured restrictions for administrators. Set to `true` to enforce required status checks for repository administrators. Set to `null` to disable.", + "nullable": true + }, + "required_pull_request_reviews": { + "type": "object", + "description": "Require at least one approving review on a pull request, before merging. Set to `null` to disable.", + "nullable": true, + "properties": { + "dismissal_restrictions": { + "type": "object", + "description": "Specify which users, teams, and apps can dismiss pull request reviews. Pass an empty `dismissal_restrictions` object to disable. User and team `dismissal_restrictions` are only available for organization-owned repositories. Omit this parameter for personal repositories.", + "properties": { + "users": { + "type": "array", + "description": "The list of user `login`s with dismissal access", + "items": { + "type": "string" + } + }, + "teams": { + "type": "array", + "description": "The list of team `slug`s with dismissal access", + "items": { + "type": "string" + } + }, + "apps": { + "type": "array", + "description": "The list of app `slug`s with dismissal access", + "items": { + "type": "string" + } + } + } + }, + "dismiss_stale_reviews": { + "type": "boolean", + "description": "Set to `true` if you want to automatically dismiss approving reviews when someone pushes a new commit." + }, + "require_code_owner_reviews": { + "type": "boolean", + "description": "Blocks merging pull requests until [code owners](https://docs.github.com/articles/about-code-owners/) review them." + }, + "required_approving_review_count": { + "type": "integer", + "description": "Specify the number of reviewers required to approve pull requests. Use a number between 1 and 6 or 0 to not require reviewers." + }, + "require_last_push_approval": { + "type": "boolean", + "description": "Whether the most recent push must be approved by someone other than the person who pushed it. Default: `false`.", + "default": false + }, + "bypass_pull_request_allowances": { + "type": "object", + "description": "Allow specific users, teams, or apps to bypass pull request requirements.", + "properties": { + "users": { + "type": "array", + "description": "The list of user `login`s allowed to bypass pull request requirements.", + "items": { + "type": "string" + } + }, + "teams": { + "type": "array", + "description": "The list of team `slug`s allowed to bypass pull request requirements.", + "items": { + "type": "string" + } + }, + "apps": { + "type": "array", + "description": "The list of app `slug`s allowed to bypass pull request requirements.", + "items": { + "type": "string" + } + } + } + } + } + }, + "restrictions": { + "type": "object", + "description": "Restrict who can push to the protected branch. User, app, and team `restrictions` are only available for organization-owned repositories. Set to `null` to disable.", + "nullable": true, + "properties": { + "users": { + "type": "array", + "description": "The list of user `login`s with push access", + "items": { + "type": "string" + } + }, + "teams": { + "type": "array", + "description": "The list of team `slug`s with push access", + "items": { + "type": "string" + } + }, + "apps": { + "type": "array", + "description": "The list of app `slug`s with push access", + "items": { + "type": "string" + } + } + }, + "required": [ + "users", + "teams" + ] + }, + "required_linear_history": { + "type": "boolean", + "description": "Enforces a linear commit Git history, which prevents anyone from pushing merge commits to a branch. Set to `true` to enforce a linear commit history. Set to `false` to disable a linear commit Git history. Your repository must allow squash merging or rebase merging before you can enable a linear commit history. Default: `false`. For more information, see \"[Requiring a linear commit history](https://docs.github.com/github/administering-a-repository/requiring-a-linear-commit-history)\" in the GitHub Help documentation." + }, + "allow_force_pushes": { + "type": "boolean", + "description": "Permits force pushes to the protected branch by anyone with write access to the repository. Set to `true` to allow force pushes. Set to `false` or `null` to block force pushes. Default: `false`. For more information, see \"[Enabling force pushes to a protected branch](https://docs.github.com/github/administering-a-repository/enabling-force-pushes-to-a-protected-branch)\" in the GitHub Help documentation.\"", + "nullable": true + }, + "allow_deletions": { + "type": "boolean", + "description": "Allows deletion of the protected branch by anyone with write access to the repository. Set to `false` to prevent deletion of the protected branch. Default: `false`. For more information, see \"[Enabling force pushes to a protected branch](https://docs.github.com/github/administering-a-repository/enabling-force-pushes-to-a-protected-branch)\" in the GitHub Help documentation." + }, + "block_creations": { + "type": "boolean", + "description": "If set to `true`, the `restrictions` branch protection settings which limits who can push will also block pushes which create new branches, unless the push is initiated by a user, team, or app which has the ability to push. Set to `true` to restrict new branch creation. Default: `false`." + }, + "required_conversation_resolution": { + "type": "boolean", + "description": "Requires all conversations on code to be resolved before a pull request can be merged into a branch that matches this rule. Set to `false` to disable. Default: `false`." + }, + "lock_branch": { + "type": "boolean", + "description": "Whether to set the branch as read-only. If this is true, users will not be able to push to the branch. Default: `false`.", + "default": false + }, + "allow_fork_syncing": { + "type": "boolean", + "description": "Whether users can pull changes from upstream when the branch is locked. Set to `true` to allow fork syncing. Set to `false` to prevent fork syncing. Default: `false`.", + "default": false + } + }, + "required": [ + "required_status_checks", + "enforce_admins", + "required_pull_request_reviews", + "restrictions" + ] + } + } + }, + "AutolinkSettings": { + "description": "An autolink reference entry", + "type": "object", + "properties": { + "key_prefix": { + "type": "string", + "description": "This prefix appended by certain characters will generate a link any time it is found in an issue, pull request, or commit." + }, + "url_template": { + "type": "string", + "description": "The URL must contain `` for the reference number. `` matches different characters depending on the value of `is_alphanumeric`." + }, + "is_alphanumeric": { + "type": "boolean", + "default": true, + "description": "Whether this autolink reference matches alphanumeric characters. If true, the `` parameter of the `url_template` matches alphanumeric characters `A-Z` (case insensitive), `0-9`, and `-`. If false, this autolink reference only matches numeric characters." + } + }, + "required": [ + "key_prefix", + "url_template" + ] + }, + "ValidatorSettings": { + "description": "Repository name validation", + "type": "object", + "properties": { + "pattern": { + "type": "string" + } + } + }, + "RulesetSettings": { + "description": "A ruleset entry", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the ruleset." + }, + "target": { + "type": "string", + "description": "The target of the ruleset", + "enum": [ + "branch", + "tag", + "push", + "repository" + ], + "default": "branch" + }, + "enforcement": { + "type": "string", + "description": "The enforcement level of the ruleset. `evaluate` allows admins to test rules before enforcing them. Admins can view insights on the Rule Insights page (`evaluate` is only available with GitHub Enterprise).", + "enum": [ + "disabled", + "active", + "evaluate" + ] + }, + "bypass_actors": { + "type": "array", + "description": "The actors that can bypass the rules in this ruleset", + "items": { + "title": "Repository Ruleset Bypass Actor", + "type": "object", + "description": "An actor that can bypass rules in a ruleset", + "required": [ + "actor_type" + ], + "properties": { + "actor_id": { + "type": "integer", + "nullable": true, + "description": "The ID of the actor that can bypass a ruleset. Required for `Integration`, `RepositoryRole`, `Team`, and `User` actor types. If `actor_type` is `OrganizationAdmin`, `actor_id` is ignored. If `actor_type` is `DeployKey`, this should be null. `OrganizationAdmin` is not applicable for personal repositories." + }, + "actor_type": { + "type": "string", + "enum": [ + "Integration", + "OrganizationAdmin", + "RepositoryRole", + "Team", + "DeployKey", + "User" + ], + "description": "The type of actor that can bypass a ruleset." + }, + "bypass_mode": { + "type": "string", + "description": "When the specified actor can bypass the ruleset. `pull_request` means that an actor can only bypass rules on pull requests. `pull_request` is not applicable for the `DeployKey` actor type. Also, `pull_request` is only applicable to branch rulesets. When `bypass_mode` is `exempt`, rules will not be run for that actor and a bypass audit entry will not be created.", + "enum": [ + "always", + "pull_request", + "exempt" + ], + "default": "always" + }, + "name": { + "type": "string", + "description": "Human-friendly alternative to `actor_id`. The team slug, username, GitHub App slug, or repository role name (resolved using `actor_type`). Cannot be combined with `actor_id`." + } + } + } + }, + "conditions": { + "title": "Organization ruleset conditions", + "type": "object", + "description": "Conditions for an organization ruleset.\nThe branch and tag rulesets conditions object should contain both `repository_name` and `ref_name` properties, or both `repository_id` and `ref_name` properties, or both `repository_property` and `ref_name` properties.\nThe push rulesets conditions object does not require the `ref_name` property.\nFor repository policy rulesets, the conditions object should only contain the `repository_name`, the `repository_id`, or the `repository_property`.", + "oneOf": [ + { + "type": "object", + "title": "repository_name_and_ref_name", + "description": "Conditions to target repositories by name and refs by name", + "allOf": [ + { + "title": "Repository ruleset conditions for ref names", + "type": "object", + "description": "Parameters for a repository ruleset ref name condition", + "properties": { + "ref_name": { + "type": "object", + "properties": { + "include": { + "type": "array", + "description": "Array of ref names or patterns to include. One of these patterns must match for the condition to pass. Also accepts `~DEFAULT_BRANCH` to include the default branch or `~ALL` to include all branches.", + "items": { + "type": "string" + } + }, + "exclude": { + "type": "array", + "description": "Array of ref names or patterns to exclude. The condition will not pass if any of these patterns match.", + "items": { + "type": "string" + } + } + } + } + } + }, + { + "title": "Repository ruleset conditions for repository names", + "type": "object", + "description": "Parameters for a repository name condition", + "properties": { + "repository_name": { + "type": "object", + "properties": { + "include": { + "type": "array", + "description": "Array of repository names or patterns to include. One of these patterns must match for the condition to pass. Also accepts `~ALL` to include all repositories.", + "items": { + "type": "string" + } + }, + "exclude": { + "type": "array", + "description": "Array of repository names or patterns to exclude. The condition will not pass if any of these patterns match.", + "items": { + "type": "string" + } + }, + "protected": { + "type": "boolean", + "description": "Whether renaming of target repositories is prevented." + } + } + } + }, + "required": [ + "repository_name" + ] + } + ] + }, + { + "type": "object", + "title": "repository_id_and_ref_name", + "description": "Conditions to target repositories by id and refs by name", + "allOf": [ + { + "title": "Repository ruleset conditions for ref names", + "type": "object", + "description": "Parameters for a repository ruleset ref name condition", + "properties": { + "ref_name": { + "type": "object", + "properties": { + "include": { + "type": "array", + "description": "Array of ref names or patterns to include. One of these patterns must match for the condition to pass. Also accepts `~DEFAULT_BRANCH` to include the default branch or `~ALL` to include all branches.", + "items": { + "type": "string" + } + }, + "exclude": { + "type": "array", + "description": "Array of ref names or patterns to exclude. The condition will not pass if any of these patterns match.", + "items": { + "type": "string" + } + } + } + } + } + }, + { + "title": "Repository ruleset conditions for repository IDs", + "type": "object", + "description": "Parameters for a repository ID condition", + "properties": { + "repository_id": { + "type": "object", + "properties": { + "repository_ids": { + "type": "array", + "description": "The repository IDs that the ruleset applies to. One of these IDs must match for the condition to pass.", + "items": { + "type": "integer" + } + } + } + } + }, + "required": [ + "repository_id" + ] + } + ] + }, + { + "type": "object", + "title": "repository_property_and_ref_name", + "description": "Conditions to target repositories by property and refs by name", + "allOf": [ + { + "title": "Repository ruleset conditions for ref names", + "type": "object", + "description": "Parameters for a repository ruleset ref name condition", + "properties": { + "ref_name": { + "type": "object", + "properties": { + "include": { + "type": "array", + "description": "Array of ref names or patterns to include. One of these patterns must match for the condition to pass. Also accepts `~DEFAULT_BRANCH` to include the default branch or `~ALL` to include all branches.", + "items": { + "type": "string" + } + }, + "exclude": { + "type": "array", + "description": "Array of ref names or patterns to exclude. The condition will not pass if any of these patterns match.", + "items": { + "type": "string" + } + } + } + } + } + }, + { + "title": "Repository ruleset conditions for repository properties", + "type": "object", + "description": "Parameters for a repository property condition", + "properties": { + "repository_property": { + "type": "object", + "properties": { + "include": { + "type": "array", + "description": "The repository properties and values to include. All of these properties must match for the condition to pass.", + "items": { + "title": "Repository ruleset property targeting definition", + "type": "object", + "description": "Parameters for a targeting a repository property", + "properties": { + "name": { + "type": "string", + "description": "The name of the repository property to target" + }, + "property_values": { + "type": "array", + "description": "The values to match for the repository property", + "items": { + "type": "string" + } + }, + "source": { + "type": "string", + "description": "The source of the repository property. Defaults to 'custom' if not specified.", + "enum": [ + "custom", + "system" + ] + } + }, + "required": [ + "name", + "property_values" + ] + } + }, + "exclude": { + "type": "array", + "description": "The repository properties and values to exclude. The condition will not pass if any of these properties match.", + "items": { + "title": "Repository ruleset property targeting definition", + "type": "object", + "description": "Parameters for a targeting a repository property", + "properties": { + "name": { + "type": "string", + "description": "The name of the repository property to target" + }, + "property_values": { + "type": "array", + "description": "The values to match for the repository property", + "items": { + "type": "string" + } + }, + "source": { + "type": "string", + "description": "The source of the repository property. Defaults to 'custom' if not specified.", + "enum": [ + "custom", + "system" + ] + } + }, + "required": [ + "name", + "property_values" + ] + } + } + } + } + }, + "required": [ + "repository_property" + ] + } + ] + } + ] + }, + "rules": { + "type": "array", + "description": "An array of rules within the ruleset.", + "items": { + "title": "Repository Rule", + "type": "object", + "description": "A repository rule.", + "oneOf": [ + { + "title": "creation", + "description": "Only allow users with bypass permission to create matching refs.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "creation" + ] + } + } + }, + { + "title": "update", + "description": "Only allow users with bypass permission to update matching refs.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "update" + ] + }, + "parameters": { + "type": "object", + "properties": { + "update_allows_fetch_and_merge": { + "type": "boolean", + "description": "Branch can pull changes from its upstream repository" + } + }, + "required": [ + "update_allows_fetch_and_merge" + ] + } + } + }, + { + "title": "deletion", + "description": "Only allow users with bypass permissions to delete matching refs.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "deletion" + ] + } + } + }, + { + "title": "required_linear_history", + "description": "Prevent merge commits from being pushed to matching refs.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "required_linear_history" + ] + } + } + }, + { + "title": "required_deployments", + "description": "Choose which environments must be successfully deployed to before refs can be pushed into a ref that matches this rule.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "required_deployments" + ] + }, + "parameters": { + "type": "object", + "properties": { + "required_deployment_environments": { + "type": "array", + "description": "The environments that must be successfully deployed to before branches can be merged.", + "items": { + "type": "string" + } + } + }, + "required": [ + "required_deployment_environments" + ] + } + } + }, + { + "title": "required_signatures", + "description": "Commits pushed to matching refs must have verified signatures.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "required_signatures" + ] + } + } + }, + { + "title": "pull_request", + "description": "Require all commits be made to a non-target branch and submitted via a pull request before they can be merged.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "pull_request" + ] + }, + "parameters": { + "type": "object", + "properties": { + "allowed_merge_methods": { + "type": "array", + "description": "Array of allowed merge methods. Allowed values include `merge`, `squash`, and `rebase`. At least one option must be enabled.", + "items": { + "type": "string", + "enum": [ + "merge", + "squash", + "rebase" + ] + } + }, + "dismiss_stale_reviews_on_push": { + "type": "boolean", + "description": "New, reviewable commits pushed will dismiss previous pull request review approvals." + }, + "dismissal_restriction": { + "title": "DismissalRestriction", + "description": "Specify people, teams, or apps allowed to dismiss pull request reviews.", + "type": "object", + "properties": { + "allowed_actors": { + "type": "array", + "description": "Specify people, teams, or apps allowed to dismiss pull request reviews.", + "items": { + "title": "Actor", + "description": "An actor allowed to dismiss pull request reviews", + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "ID of the actor that can dismiss reviews." + }, + "type": { + "type": "string", + "description": "The type of the actor", + "enum": [ + "User", + "Team", + "IntegrationInstallation", + "RepositoryRole" + ] + } + }, + "required": [ + "id", + "type" + ] + } + }, + "enabled": { + "type": "boolean", + "description": "Whether to restrict review dismissal to specific actors." + } + }, + "required": [ + "enabled" + ] + }, + "require_code_owner_review": { + "type": "boolean", + "description": "Require an approving review in pull requests that modify files that have a designated code owner." + }, + "require_last_push_approval": { + "type": "boolean", + "description": "Whether the most recent reviewable push must be approved by someone other than the person who pushed it." + }, + "required_approving_review_count": { + "type": "integer", + "description": "The number of approving reviews that are required before a pull request can be merged.", + "minimum": 0, + "maximum": 10 + }, + "required_review_thread_resolution": { + "type": "boolean", + "description": "All conversations on code must be resolved before a pull request can be merged." + }, + "required_reviewers": { + "type": "array", + "description": "> [!NOTE]\n> `required_reviewers` is in beta and subject to change.\n\nA collection of reviewers and associated file patterns. Each reviewer has a list of file patterns which determine the files that reviewer is required to review.", + "items": { + "title": "RequiredReviewerConfiguration", + "description": "A reviewing team, and file patterns describing which files they must approve changes to.", + "type": "object", + "properties": { + "file_patterns": { + "type": "array", + "description": "Array of file patterns. Pull requests which change matching files must be approved by the specified team. File patterns use fnmatch syntax.", + "items": { + "type": "string" + } + }, + "minimum_approvals": { + "type": "integer", + "description": "Minimum number of approvals required from the specified team. If set to zero, the team will be added to the pull request but approval is optional." + }, + "reviewer": { + "title": "Reviewer", + "description": "A required reviewing team", + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "ID of the reviewer which must review changes to matching files." + }, + "type": { + "type": "string", + "description": "The type of the reviewer", + "enum": [ + "Team" + ] + }, + "slug": { + "type": "string", + "description": "Human-friendly alternative to `id`. The slug of the team that must review changes to matching files. Cannot be combined with `id`." + } + }, + "required": [ + "type" + ] + } + }, + "required": [ + "file_patterns", + "minimum_approvals", + "reviewer" + ] + } + } + }, + "required": [ + "dismiss_stale_reviews_on_push", + "require_code_owner_review", + "require_last_push_approval", + "required_approving_review_count", + "required_review_thread_resolution" + ] + } + } + }, + { + "title": "required_status_checks", + "description": "Choose which status checks must pass before the ref is updated. When enabled, commits must first be pushed to another ref where the checks pass.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "required_status_checks" + ] + }, + "parameters": { + "type": "object", + "properties": { + "do_not_enforce_on_create": { + "type": "boolean", + "description": "Allow repositories and branches to be created if a check would otherwise prohibit it." + }, + "required_status_checks": { + "type": "array", + "description": "Status checks that are required.", + "items": { + "title": "StatusCheckConfiguration", + "description": "Required status check", + "type": "object", + "properties": { + "context": { + "type": "string", + "description": "The status check context name that must be present on the commit." + }, + "integration_id": { + "type": "integer", + "description": "The optional integration ID that this status check must originate from." + } + }, + "required": [ + "context" + ] + } + }, + "strict_required_status_checks_policy": { + "type": "boolean", + "description": "Whether pull requests targeting a matching branch must be tested with the latest code. This setting will not take effect unless at least one status check is enabled." + } + }, + "required": [ + "required_status_checks", + "strict_required_status_checks_policy" + ] + } + } + }, + { + "title": "non_fast_forward", + "description": "Prevent users with push access from force pushing to refs.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "non_fast_forward" + ] + } + } + }, + { + "title": "commit_message_pattern", + "description": "Parameters to be used for the commit_message_pattern rule", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "commit_message_pattern" + ] + }, + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "How this rule appears when configuring it." + }, + "negate": { + "type": "boolean", + "description": "If true, the rule will fail if the pattern matches." + }, + "operator": { + "type": "string", + "description": "The operator to use for matching.", + "enum": [ + "starts_with", + "ends_with", + "contains", + "regex" + ] + }, + "pattern": { + "type": "string", + "description": "The pattern to match with." + } + }, + "required": [ + "operator", + "pattern" + ] + } + } + }, + { + "title": "commit_author_email_pattern", + "description": "Parameters to be used for the commit_author_email_pattern rule", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "commit_author_email_pattern" + ] + }, + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "How this rule appears when configuring it." + }, + "negate": { + "type": "boolean", + "description": "If true, the rule will fail if the pattern matches." + }, + "operator": { + "type": "string", + "description": "The operator to use for matching.", + "enum": [ + "starts_with", + "ends_with", + "contains", + "regex" + ] + }, + "pattern": { + "type": "string", + "description": "The pattern to match with." + } + }, + "required": [ + "operator", + "pattern" + ] + } + } + }, + { + "title": "committer_email_pattern", + "description": "Parameters to be used for the committer_email_pattern rule", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "committer_email_pattern" + ] + }, + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "How this rule appears when configuring it." + }, + "negate": { + "type": "boolean", + "description": "If true, the rule will fail if the pattern matches." + }, + "operator": { + "type": "string", + "description": "The operator to use for matching.", + "enum": [ + "starts_with", + "ends_with", + "contains", + "regex" + ] + }, + "pattern": { + "type": "string", + "description": "The pattern to match with." + } + }, + "required": [ + "operator", + "pattern" + ] + } + } + }, + { + "title": "branch_name_pattern", + "description": "Parameters to be used for the branch_name_pattern rule", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "branch_name_pattern" + ] + }, + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "How this rule appears when configuring it." + }, + "negate": { + "type": "boolean", + "description": "If true, the rule will fail if the pattern matches." + }, + "operator": { + "type": "string", + "description": "The operator to use for matching.", + "enum": [ + "starts_with", + "ends_with", + "contains", + "regex" + ] + }, + "pattern": { + "type": "string", + "description": "The pattern to match with." + } + }, + "required": [ + "operator", + "pattern" + ] + } + } + }, + { + "title": "tag_name_pattern", + "description": "Parameters to be used for the tag_name_pattern rule", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "tag_name_pattern" + ] + }, + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "How this rule appears when configuring it." + }, + "negate": { + "type": "boolean", + "description": "If true, the rule will fail if the pattern matches." + }, + "operator": { + "type": "string", + "description": "The operator to use for matching.", + "enum": [ + "starts_with", + "ends_with", + "contains", + "regex" + ] + }, + "pattern": { + "type": "string", + "description": "The pattern to match with." + } + }, + "required": [ + "operator", + "pattern" + ] + } + } + }, + { + "title": "file_path_restriction", + "description": "Prevent commits that include changes in specified file and folder paths from being pushed to the commit graph. This includes absolute paths that contain file names.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "file_path_restriction" + ] + }, + "parameters": { + "type": "object", + "properties": { + "restricted_file_paths": { + "type": "array", + "description": "The file paths that are restricted from being pushed to the commit graph.", + "items": { + "type": "string" + } + } + }, + "required": [ + "restricted_file_paths" + ] + } + } + }, + { + "title": "max_file_path_length", + "description": "Prevent commits that include file paths that exceed the specified character limit from being pushed to the commit graph.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "max_file_path_length" + ] + }, + "parameters": { + "type": "object", + "properties": { + "max_file_path_length": { + "type": "integer", + "description": "The maximum amount of characters allowed in file paths.", + "minimum": 1, + "maximum": 32767 + } + }, + "required": [ + "max_file_path_length" + ] + } + } + }, + { + "title": "file_extension_restriction", + "description": "Prevent commits that include files with specified file extensions from being pushed to the commit graph.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "file_extension_restriction" + ] + }, + "parameters": { + "type": "object", + "properties": { + "restricted_file_extensions": { + "type": "array", + "description": "The file extensions that are restricted from being pushed to the commit graph.", + "items": { + "type": "string" + } + } + }, + "required": [ + "restricted_file_extensions" + ] + } + } + }, + { + "title": "max_file_size", + "description": "Prevent commits with individual files that exceed the specified limit from being pushed to the commit graph.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "max_file_size" + ] + }, + "parameters": { + "type": "object", + "properties": { + "max_file_size": { + "type": "integer", + "description": "The maximum file size allowed in megabytes. This limit does not apply to Git Large File Storage (Git LFS).", + "minimum": 1, + "maximum": 100 + } + }, + "required": [ + "max_file_size" + ] + } + } + }, + { + "title": "workflows", + "description": "Require all changes made to a targeted branch to pass the specified workflows before they can be merged.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "workflows" + ] + }, + "parameters": { + "type": "object", + "properties": { + "do_not_enforce_on_create": { + "type": "boolean", + "description": "Allow repositories and branches to be created if a check would otherwise prohibit it." + }, + "workflows": { + "type": "array", + "description": "Workflows that must pass for this rule to pass.", + "items": { + "title": "WorkflowFileReference", + "description": "A workflow that must run for this rule to pass", + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "The path to the workflow file" + }, + "ref": { + "type": "string", + "description": "The ref (branch or tag) of the workflow file to use" + }, + "repository_id": { + "type": "integer", + "description": "The ID of the repository where the workflow is defined" + }, + "sha": { + "type": "string", + "description": "The commit SHA of the workflow file to use" + } + }, + "required": [ + "path", + "repository_id" + ] + } + } + }, + "required": [ + "workflows" + ] + } + } + }, + { + "title": "code_scanning", + "description": "Choose which tools must provide code scanning results before the reference is updated. When configured, code scanning must be enabled and have results for both the commit and the reference being updated.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "code_scanning" + ] + }, + "parameters": { + "type": "object", + "properties": { + "code_scanning_tools": { + "type": "array", + "description": "Tools that must provide code scanning results for this rule to pass.", + "items": { + "title": "CodeScanningTool", + "description": "A tool that must provide code scanning results for this rule to pass.", + "type": "object", + "properties": { + "alerts_threshold": { + "type": "string", + "description": "The severity level at which code scanning results that raise alerts block a reference update. For more information on alert severity levels, see \"[About code scanning alerts](https://docs.github.com/code-security/code-scanning/managing-code-scanning-alerts/about-code-scanning-alerts#about-alert-severity-and-security-severity-levels).\"", + "enum": [ + "none", + "errors", + "errors_and_warnings", + "all" + ] + }, + "security_alerts_threshold": { + "type": "string", + "description": "The severity level at which code scanning results that raise security alerts block a reference update. For more information on security severity levels, see \"[About code scanning alerts](https://docs.github.com/code-security/code-scanning/managing-code-scanning-alerts/about-code-scanning-alerts#about-alert-severity-and-security-severity-levels).\"", + "enum": [ + "none", + "critical", + "high_or_higher", + "medium_or_higher", + "all" + ] + }, + "tool": { + "type": "string", + "description": "The name of a code scanning tool" + } + }, + "required": [ + "alerts_threshold", + "security_alerts_threshold", + "tool" + ] + } + } + }, + "required": [ + "code_scanning_tools" + ] + } + } + }, + { + "title": "copilot_code_review", + "description": "Request Copilot code review for new pull requests automatically if the author has access to Copilot code review and their premium requests quota has not reached the limit.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "copilot_code_review" + ] + }, + "parameters": { + "type": "object", + "properties": { + "review_draft_pull_requests": { + "type": "boolean", + "description": "Copilot automatically reviews draft pull requests before they are marked as ready for review." + }, + "review_on_push": { + "type": "boolean", + "description": "Copilot automatically reviews each new push to the pull request." + } + } + } + } + } + ] + } + } + }, + "required": [ + "name", + "enforcement" + ] + }, + "EnvironmentsSettings": { + "description": "A deployment environment configuration entry", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "The name of the deployment environment", + "type": "string" + }, + "wait_timer": { + "description": "The amount of time to delay a job after the job is initially triggered (in minutes)", + "type": "integer" + }, + "reviewers": { + "description": "The people or teams that may review jobs that reference the environment", + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "description": "The type of reviewer (`User` or `Team`)", + "type": "string" + }, + "id": { + "description": "The id of the user or team who can review the deployment", + "type": "integer" + } + } + } + }, + "deployment_branch_policy": { + "description": "The type of deployment branch policy for this environment", + "type": "object", + "properties": { + "protected_branches": { + "description": "Whether only protected branches can be deployed to this environment", + "type": "boolean" + }, + "custom_branch_policies": { + "description": "Whether only branches that match the specified name patterns can deploy to this environment", + "type": "boolean" + } + } + }, + "prevent_self_review": { + "description": "Whether or not a user who created the job is prevented from approving their own job", + "type": "boolean" + } + } + }, + "CustomPropertiesSettings": { + "description": "A custom property entry", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "VariablesSettings": { + "description": "An Actions variable entry", + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "description": "The name of the variable", + "type": "string" + }, + "value": { + "description": "The value of the variable", + "type": "string" + }, + "visibility": { + "description": "The visibility of the variable. Can be `all`, `private`, or `selected`", + "type": "string", + "enum": [ + "all", + "private", + "selected" + ] + } + } + } + } +} \ No newline at end of file diff --git a/schema/dereferenced/settings.json b/schema/dereferenced/settings.json index e94a66e57..4beccd3b5 100644 --- a/schema/dereferenced/settings.json +++ b/schema/dereferenced/settings.json @@ -1,8 +1,11 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Safe-settings org-level configuration", + "description": "Schema for .github/settings.yml — org-level safe-settings configuration", "type": "object", "properties": { "repositories": { + "description": "Repository settings", "allOf": [ { "type": "object", @@ -39,7 +42,17 @@ "properties": { "advanced_security": { "type": "object", - "description": "Use the `status` property to enable or disable GitHub Advanced Security for this repository. For more information, see \"[About GitHub Advanced Security](/github/getting-started-with-github/learning-about-github/about-github-advanced-security).\"", + "description": "Use the `status` property to enable or disable GitHub Advanced Security for this repository.\nFor more information, see \"[About GitHub Advanced\nSecurity](/github/getting-started-with-github/learning-about-github/about-github-advanced-security).\"\n\nFor standalone Code Scanning or Secret Protection products, this parameter cannot be used.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "code_security": { + "type": "object", + "description": "Use the `status` property to enable or disable GitHub Code Security for this repository.", "properties": { "status": { "type": "string", @@ -67,6 +80,16 @@ } } }, + "secret_scanning_ai_detection": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning AI detection for this repository. For more information, see \"[Responsible detection of generic secrets with AI](https://docs.github.com/code-security/secret-scanning/using-advanced-secret-scanning-and-push-protection-features/generic-secret-detection/responsible-ai-generic-secrets).\"", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, "secret_scanning_non_provider_patterns": { "type": "object", "description": "Use the `status` property to enable or disable secret scanning non-provider patterns for this repository. For more information, see \"[Supported secret scanning patterns](/code-security/secret-scanning/introduction/supported-secret-scanning-patterns#supported-secrets).\"", @@ -76,6 +99,66 @@ "description": "Can be `enabled` or `disabled`." } } + }, + "secret_scanning_delegated_alert_dismissal": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning delegated alert dismissal for this repository.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_delegated_bypass": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning delegated bypass for this repository.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_delegated_bypass_options": { + "type": "object", + "description": "Feature options for secret scanning delegated bypass.\nThis object is only honored when `security_and_analysis.secret_scanning_delegated_bypass.status` is set to `enabled`.\nYou can send this object in the same request as `secret_scanning_delegated_bypass`, or update just the options in a separate request.", + "properties": { + "reviewers": { + "type": "array", + "description": "The bypass reviewers for secret scanning delegated bypass.\nIf you omit this field, the existing set of reviewers is unchanged.", + "items": { + "type": "object", + "required": [ + "reviewer_id", + "reviewer_type" + ], + "properties": { + "reviewer_id": { + "type": "integer", + "description": "The ID of the team or role selected as a bypass reviewer" + }, + "reviewer_type": { + "type": "string", + "description": "The type of the bypass reviewer", + "enum": [ + "TEAM", + "ROLE" + ] + }, + "mode": { + "type": "string", + "description": "The bypass mode for the reviewer", + "enum": [ + "ALWAYS", + "EXEMPT" + ], + "default": "ALWAYS" + } + } + } + } + } } } }, @@ -94,6 +177,19 @@ "description": "Either `true` to enable the wiki for this repository or `false` to disable it.", "default": true }, + "has_pull_requests": { + "type": "boolean", + "description": "Either `true` to allow pull requests for this repository or `false` to prevent pull requests.", + "default": true + }, + "pull_request_creation_policy": { + "type": "string", + "description": "The policy that controls who can create pull requests for this repository: `all` or `collaborators_only`.", + "enum": [ + "all", + "collaborators_only" + ] + }, "is_template": { "type": "boolean", "description": "Either `true` to make this repo available as a template repository or `false` to prevent it.", @@ -135,7 +231,7 @@ }, "use_squash_pr_title_as_default": { "type": "boolean", - "description": "Either `true` to allow squash-merge commits to use pull request title, or `false` to use commit message. **This property has been deprecated. Please use `squash_merge_commit_title` instead.", + "description": "Either `true` to allow squash-merge commits to use pull request title, or `false` to use commit message. **This property is closing down. Please use `squash_merge_commit_title` instead.", "default": false, "deprecated": true }, @@ -223,6 +319,14 @@ "type": "boolean" } } + }, + "force_create": { + "description": "Force create the repository", + "type": "boolean" + }, + "template": { + "description": "Name of a template repository to use when creating the repository", + "type": "string" } } } @@ -263,29 +367,11 @@ } } }, - "milestones": { - "description": "Milestones: define milestones for Issues and Pull Requests", - "type": "array", - "items": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "state": { - "description": "The state of the milestone. Either `open` or `closed`", - "type": "string" - } - } - } - }, "collaborators": { "description": "Collaborators: give specific users access to any repository.", "type": "array", "items": { + "description": "A collaborator entry giving a specific user access to a repository.", "allOf": [ { "type": "object", @@ -326,69 +412,84 @@ "description": "Teams", "type": "array", "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "The name of the team." - }, - "description": { - "type": "string", - "description": "The description of the team." - }, - "maintainers": { - "type": "array", - "description": "List GitHub IDs for organization members who will become team maintainers.", - "items": { - "type": "string" + "description": "A team entry", + "allOf": [ + { + "type": "object", + "properties": { + "permission": { + "type": "string", + "description": "The permission to grant the team on this repository. We accept the following permissions to be set: `pull`, `triage`, `push`, `maintain`, `admin` and you can also specify a custom repository role name, if the owning organization has defined any. If no permission is specified, the team's `permission` attribute will be used to determine what permission to grant the team on this repository." + } } }, - "repo_names": { - "type": "array", - "description": "The full name (e.g., \"organization-name/repository-name\") of repositories to add the team to.", - "items": { - "type": "string" + { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "The name (or slug) of the team to grant access to the repo.", + "type": "string" + }, + "privacy": { + "description": "The level of privacy the team should have when it is created by safe-settings. The options are `secret` (visible only to organization owners and members of the team) or `closed` (visible to all members of the organization).", + "type": "string", + "enum": [ + "secret", + "closed" + ] + }, + "external_group": { + "description": "The display name of an external IdP group to link to this team.", + "type": "string" + }, + "exclude": { + "description": "Exclude this team from repos whose names match one of these glob patterns.", + "type": "array", + "items": { + "type": "string" + } + }, + "include": { + "description": "Apply this team only to repos whose names match one of these glob patterns.", + "type": "array", + "items": { + "type": "string" + } + } } + } + ] + } + }, + "milestones": { + "description": "Milestones: define milestones for Issues and Pull Requests", + "type": "array", + "items": { + "description": "A milestone entry", + "type": "object", + "properties": { + "title": { + "type": "string" }, - "privacy": { - "type": "string", - "description": "The level of privacy this team should have. The options are: \n**For a non-nested team:** \n * `secret` - only visible to organization owners and members of this team. \n * `closed` - visible to all members of this organization. \nDefault: `secret` \n**For a parent or child team:** \n * `closed` - visible to all members of this organization. \nDefault for child team: `closed`", - "enum": [ - "secret", - "closed" - ] - }, - "notification_setting": { - "type": "string", - "description": "The notification setting the team has chosen. The options are: \n * `notifications_enabled` - team members receive notifications when the team is @mentioned. \n * `notifications_disabled` - no one receives notifications. \nDefault: `notifications_enabled`", - "enum": [ - "notifications_enabled", - "notifications_disabled" - ] - }, - "permission": { - "type": "string", - "description": "**Deprecated**. The permission that new repositories will be added to the team with when none is specified.", - "enum": [ - "pull", - "push" - ], - "default": "pull" + "description": { + "type": "string" }, - "parent_team_id": { - "type": "integer", - "description": "The ID of a team to set as the parent team." + "state": { + "description": "The state of the milestone. Either `open` or `closed`", + "type": "string" } - }, - "required": [ - "name" - ] + } } }, "branches": { "description": "Branch protection rules", "type": "array", "items": { + "description": "A branch protection rule entry", + "type": "object", "properties": { "name": { "description": "If the name of the branch value is specified as `default`, then the app will create a branch protection rule to apply against the default branch in the repo", @@ -409,7 +510,7 @@ "contexts": { "type": "array", "deprecated": true, - "description": "**Deprecated**: The list of status checks to require in order to merge into this branch. If any of these checks have recently been set by a particular GitHub App, they will be required to come from that app in future for the branch to merge. Use `checks` instead of `contexts` for more fine-grained control.", + "description": "**Closing down notice**: The list of status checks to require in order to merge into this branch. If any of these checks have recently been set by a particular GitHub App, they will be required to come from that app in future for the branch to merge. Use `checks` instead of `contexts` for more fine-grained control.", "items": { "type": "string" } @@ -597,25 +698,11 @@ } } }, - "custom_properties": { - "description": "Custom properties", - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - } - } - } - }, "autolinks": { "description": "Autolinks", "type": "array", "items": { + "description": "An autolink reference entry", "type": "object", "properties": { "key_prefix": { @@ -648,9 +735,10 @@ } }, "rulesets": { - "description": "Rulesets", + "description": "Rulesets. Org-level only — rulesets defined here apply to the organization and are NOT inherited by suborg or repo override files.", "type": "array", "items": { + "description": "A ruleset entry", "type": "object", "properties": { "name": { @@ -663,7 +751,8 @@ "enum": [ "branch", "tag", - "push" + "push", + "repository" ], "default": "branch" }, @@ -690,7 +779,7 @@ "actor_id": { "type": "integer", "nullable": true, - "description": "The ID of the actor that can bypass a ruleset. If `actor_type` is `OrganizationAdmin`, this should be `1`. If `actor_type` is `DeployKey`, this should be null. `OrganizationAdmin` is not applicable for personal repositories." + "description": "The ID of the actor that can bypass a ruleset. Required for `Integration`, `RepositoryRole`, `Team`, and `User` actor types. If `actor_type` is `OrganizationAdmin`, `actor_id` is ignored. If `actor_type` is `DeployKey`, this should be null. `OrganizationAdmin` is not applicable for personal repositories." }, "actor_type": { "type": "string", @@ -699,18 +788,24 @@ "OrganizationAdmin", "RepositoryRole", "Team", - "DeployKey" + "DeployKey", + "User" ], "description": "The type of actor that can bypass a ruleset." }, "bypass_mode": { "type": "string", - "description": "When the specified actor can bypass the ruleset. `pull_request` means that an actor can only bypass rules on pull requests. `pull_request` is not applicable for the `DeployKey` actor type. Also, `pull_request` is only applicable to branch rulesets.", + "description": "When the specified actor can bypass the ruleset. `pull_request` means that an actor can only bypass rules on pull requests. `pull_request` is not applicable for the `DeployKey` actor type. Also, `pull_request` is only applicable to branch rulesets. When `bypass_mode` is `exempt`, rules will not be run for that actor and a bypass audit entry will not be created.", "enum": [ "always", - "pull_request" + "pull_request", + "exempt" ], "default": "always" + }, + "name": { + "type": "string", + "description": "Human-friendly alternative to `actor_id`. The team slug, username, GitHub App slug, or repository role name (resolved using `actor_type`). Cannot be combined with `actor_id`." } } } @@ -718,7 +813,7 @@ "conditions": { "title": "Organization ruleset conditions", "type": "object", - "description": "Conditions for an organization ruleset.\nThe branch and tag rulesets conditions object should contain both `repository_name` and `ref_name` properties, or both `repository_id` and `ref_name` properties, or both `repository_property` and `ref_name` properties.\nThe push rulesets conditions object does not require the `ref_name` property.", + "description": "Conditions for an organization ruleset.\nThe branch and tag rulesets conditions object should contain both `repository_name` and `ref_name` properties, or both `repository_id` and `ref_name` properties, or both `repository_property` and `ref_name` properties.\nThe push rulesets conditions object does not require the `ref_name` property.\nFor repository policy rulesets, the conditions object should only contain the `repository_name`, the `repository_id`, or the `repository_property`.", "oneOf": [ { "type": "object", @@ -1043,83 +1138,6 @@ } } }, - { - "title": "merge_queue", - "description": "Merges must be performed via a merge queue.", - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "merge_queue" - ] - }, - "parameters": { - "type": "object", - "properties": { - "check_response_timeout_minutes": { - "type": "integer", - "description": "Maximum time for a required status check to report a conclusion. After this much time has elapsed, checks that have not reported a conclusion will be assumed to have failed", - "minimum": 1, - "maximum": 360 - }, - "grouping_strategy": { - "type": "string", - "description": "When set to ALLGREEN, the merge commit created by merge queue for each PR in the group must pass all required checks to merge. When set to HEADGREEN, only the commit at the head of the merge group, i.e. the commit containing changes from all of the PRs in the group, must pass its required checks to merge.", - "enum": [ - "ALLGREEN", - "HEADGREEN" - ] - }, - "max_entries_to_build": { - "type": "integer", - "description": "Limit the number of queued pull requests requesting checks and workflow runs at the same time.", - "minimum": 0, - "maximum": 100 - }, - "max_entries_to_merge": { - "type": "integer", - "description": "The maximum number of PRs that will be merged together in a group.", - "minimum": 0, - "maximum": 100 - }, - "merge_method": { - "type": "string", - "description": "Method to use when merging changes from queued pull requests.", - "enum": [ - "MERGE", - "SQUASH", - "REBASE" - ] - }, - "min_entries_to_merge": { - "type": "integer", - "description": "The minimum number of PRs that will be merged together in a group.", - "minimum": 0, - "maximum": 100 - }, - "min_entries_to_merge_wait_minutes": { - "type": "integer", - "description": "The time merge queue should wait after the first PR is added to the queue for the minimum group size to be met. After this time has elapsed, the minimum group size will be ignored and a smaller group will be merged.", - "minimum": 0, - "maximum": 360 - } - }, - "required": [ - "check_response_timeout_minutes", - "grouping_strategy", - "max_entries_to_build", - "max_entries_to_merge", - "merge_method", - "min_entries_to_merge", - "min_entries_to_merge_wait_minutes" - ] - } - } - }, { "title": "required_deployments", "description": "Choose which environments must be successfully deployed to before refs can be pushed into a ref that matches this rule.", @@ -1184,10 +1202,65 @@ "parameters": { "type": "object", "properties": { + "allowed_merge_methods": { + "type": "array", + "description": "Array of allowed merge methods. Allowed values include `merge`, `squash`, and `rebase`. At least one option must be enabled.", + "items": { + "type": "string", + "enum": [ + "merge", + "squash", + "rebase" + ] + } + }, "dismiss_stale_reviews_on_push": { "type": "boolean", "description": "New, reviewable commits pushed will dismiss previous pull request review approvals." }, + "dismissal_restriction": { + "title": "DismissalRestriction", + "description": "Specify people, teams, or apps allowed to dismiss pull request reviews.", + "type": "object", + "properties": { + "allowed_actors": { + "type": "array", + "description": "Specify people, teams, or apps allowed to dismiss pull request reviews.", + "items": { + "title": "Actor", + "description": "An actor allowed to dismiss pull request reviews", + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "ID of the actor that can dismiss reviews." + }, + "type": { + "type": "string", + "description": "The type of the actor", + "enum": [ + "User", + "Team", + "IntegrationInstallation", + "RepositoryRole" + ] + } + }, + "required": [ + "id", + "type" + ] + } + }, + "enabled": { + "type": "boolean", + "description": "Whether to restrict review dismissal to specific actors." + } + }, + "required": [ + "enabled" + ] + }, "require_code_owner_review": { "type": "boolean", "description": "Require an approving review in pull requests that modify files that have a designated code owner." @@ -1205,6 +1278,58 @@ "required_review_thread_resolution": { "type": "boolean", "description": "All conversations on code must be resolved before a pull request can be merged." + }, + "required_reviewers": { + "type": "array", + "description": "> [!NOTE]\n> `required_reviewers` is in beta and subject to change.\n\nA collection of reviewers and associated file patterns. Each reviewer has a list of file patterns which determine the files that reviewer is required to review.", + "items": { + "title": "RequiredReviewerConfiguration", + "description": "A reviewing team, and file patterns describing which files they must approve changes to.", + "type": "object", + "properties": { + "file_patterns": { + "type": "array", + "description": "Array of file patterns. Pull requests which change matching files must be approved by the specified team. File patterns use fnmatch syntax.", + "items": { + "type": "string" + } + }, + "minimum_approvals": { + "type": "integer", + "description": "Minimum number of approvals required from the specified team. If set to zero, the team will be added to the pull request but approval is optional." + }, + "reviewer": { + "title": "Reviewer", + "description": "A required reviewing team", + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "ID of the reviewer which must review changes to matching files." + }, + "type": { + "type": "string", + "description": "The type of the reviewer", + "enum": [ + "Team" + ] + }, + "slug": { + "type": "string", + "description": "Human-friendly alternative to `id`. The slug of the team that must review changes to matching files. Cannot be combined with `id`." + } + }, + "required": [ + "type" + ] + } + }, + "required": [ + "file_patterns", + "minimum_approvals", + "reviewer" + ] + } } }, "required": [ @@ -1307,7 +1432,7 @@ "properties": { "name": { "type": "string", - "description": "How this rule will appear to users." + "description": "How this rule appears when configuring it." }, "negate": { "type": "boolean", @@ -1354,7 +1479,7 @@ "properties": { "name": { "type": "string", - "description": "How this rule will appear to users." + "description": "How this rule appears when configuring it." }, "negate": { "type": "boolean", @@ -1401,7 +1526,7 @@ "properties": { "name": { "type": "string", - "description": "How this rule will appear to users." + "description": "How this rule appears when configuring it." }, "negate": { "type": "boolean", @@ -1448,7 +1573,7 @@ "properties": { "name": { "type": "string", - "description": "How this rule will appear to users." + "description": "How this rule appears when configuring it." }, "negate": { "type": "boolean", @@ -1495,7 +1620,7 @@ "properties": { "name": { "type": "string", - "description": "How this rule will appear to users." + "description": "How this rule appears when configuring it." }, "negate": { "type": "boolean", @@ -1525,7 +1650,7 @@ }, { "title": "file_path_restriction", - "description": "Prevent commits that include changes in specified file paths from being pushed to the commit graph.", + "description": "Prevent commits that include changes in specified file and folder paths from being pushed to the commit graph. This includes absolute paths that contain file names.", "type": "object", "required": [ "type" @@ -1556,7 +1681,7 @@ }, { "title": "max_file_path_length", - "description": "Prevent commits that include file paths that exceed a specified character limit from being pushed to the commit graph.", + "description": "Prevent commits that include file paths that exceed the specified character limit from being pushed to the commit graph.", "type": "object", "required": [ "type" @@ -1573,9 +1698,9 @@ "properties": { "max_file_path_length": { "type": "integer", - "description": "The maximum amount of characters allowed in file paths", + "description": "The maximum amount of characters allowed in file paths.", "minimum": 1, - "maximum": 256 + "maximum": 32767 } }, "required": [ @@ -1617,7 +1742,7 @@ }, { "title": "max_file_size", - "description": "Prevent commits that exceed a specified file size limit from being pushed to the commit.", + "description": "Prevent commits with individual files that exceed the specified limit from being pushed to the commit graph.", "type": "object", "required": [ "type" @@ -1768,16 +1893,2291 @@ ] } } - } - ] - } - } - }, - "required": [ - "name", + }, + { + "title": "copilot_code_review", + "description": "Request Copilot code review for new pull requests automatically if the author has access to Copilot code review and their premium requests quota has not reached the limit.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "copilot_code_review" + ] + }, + "parameters": { + "type": "object", + "properties": { + "review_draft_pull_requests": { + "type": "boolean", + "description": "Copilot automatically reviews draft pull requests before they are marked as ready for review." + }, + "review_on_push": { + "type": "boolean", + "description": "Copilot automatically reviews each new push to the pull request." + } + } + } + } + } + ] + } + } + }, + "required": [ + "name", "enforcement" ] } + }, + "environments": { + "description": "Deployment environments", + "type": "array", + "items": { + "description": "A deployment environment configuration entry", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "The name of the deployment environment", + "type": "string" + }, + "wait_timer": { + "description": "The amount of time to delay a job after the job is initially triggered (in minutes)", + "type": "integer" + }, + "reviewers": { + "description": "The people or teams that may review jobs that reference the environment", + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "description": "The type of reviewer (`User` or `Team`)", + "type": "string" + }, + "id": { + "description": "The id of the user or team who can review the deployment", + "type": "integer" + } + } + } + }, + "deployment_branch_policy": { + "description": "The type of deployment branch policy for this environment", + "type": "object", + "properties": { + "protected_branches": { + "description": "Whether only protected branches can be deployed to this environment", + "type": "boolean" + }, + "custom_branch_policies": { + "description": "Whether only branches that match the specified name patterns can deploy to this environment", + "type": "boolean" + } + } + }, + "prevent_self_review": { + "description": "Whether or not a user who created the job is prevented from approving their own job", + "type": "boolean" + } + } + } + }, + "custom_properties": { + "description": "Custom properties", + "type": "array", + "items": { + "description": "A custom property entry", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + }, + "variables": { + "description": "Repository or org-level Actions variables", + "type": "array", + "items": { + "description": "An Actions variable entry", + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "description": "The name of the variable", + "type": "string" + }, + "value": { + "description": "The value of the variable", + "type": "string" + }, + "visibility": { + "description": "The visibility of the variable. Can be `all`, `private`, or `selected`", + "type": "string", + "enum": [ + "all", + "private", + "selected" + ] + } + } + } + } + }, + "$defs": { + "RepositorySettings": { + "description": "Repository settings", + "allOf": [ + { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the repository." + }, + "description": { + "type": "string", + "description": "A short description of the repository." + }, + "homepage": { + "type": "string", + "description": "A URL with more information about the repository." + }, + "private": { + "type": "boolean", + "description": "Either `true` to make the repository private or `false` to make it public. Default: `false`. \n**Note**: You will get a `422` error if the organization restricts [changing repository visibility](https://docs.github.com/articles/repository-permission-levels-for-an-organization#changing-the-visibility-of-repositories) to organization owners and a non-owner tries to change the value of private.", + "default": false + }, + "visibility": { + "type": "string", + "description": "The visibility of the repository.", + "enum": [ + "public", + "private" + ] + }, + "security_and_analysis": { + "type": "object", + "description": "Specify which security and analysis features to enable or disable for the repository.\n\nTo use this parameter, you must have admin permissions for the repository or be an owner or security manager for the organization that owns the repository. For more information, see \"[Managing security managers in your organization](https://docs.github.com/organizations/managing-peoples-access-to-your-organization-with-roles/managing-security-managers-in-your-organization).\"\n\nFor example, to enable GitHub Advanced Security, use this data in the body of the `PATCH` request:\n`{ \"security_and_analysis\": {\"advanced_security\": { \"status\": \"enabled\" } } }`.\n\nYou can check which security and analysis features are currently enabled by using a `GET /repos/{owner}/{repo}` request.", + "nullable": true, + "properties": { + "advanced_security": { + "type": "object", + "description": "Use the `status` property to enable or disable GitHub Advanced Security for this repository.\nFor more information, see \"[About GitHub Advanced\nSecurity](/github/getting-started-with-github/learning-about-github/about-github-advanced-security).\"\n\nFor standalone Code Scanning or Secret Protection products, this parameter cannot be used.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "code_security": { + "type": "object", + "description": "Use the `status` property to enable or disable GitHub Code Security for this repository.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning for this repository. For more information, see \"[About secret scanning](/code-security/secret-security/about-secret-scanning).\"", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_push_protection": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning push protection for this repository. For more information, see \"[Protecting pushes with secret scanning](/code-security/secret-scanning/protecting-pushes-with-secret-scanning).\"", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_ai_detection": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning AI detection for this repository. For more information, see \"[Responsible detection of generic secrets with AI](https://docs.github.com/code-security/secret-scanning/using-advanced-secret-scanning-and-push-protection-features/generic-secret-detection/responsible-ai-generic-secrets).\"", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_non_provider_patterns": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning non-provider patterns for this repository. For more information, see \"[Supported secret scanning patterns](/code-security/secret-scanning/introduction/supported-secret-scanning-patterns#supported-secrets).\"", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_delegated_alert_dismissal": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning delegated alert dismissal for this repository.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_delegated_bypass": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning delegated bypass for this repository.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_delegated_bypass_options": { + "type": "object", + "description": "Feature options for secret scanning delegated bypass.\nThis object is only honored when `security_and_analysis.secret_scanning_delegated_bypass.status` is set to `enabled`.\nYou can send this object in the same request as `secret_scanning_delegated_bypass`, or update just the options in a separate request.", + "properties": { + "reviewers": { + "type": "array", + "description": "The bypass reviewers for secret scanning delegated bypass.\nIf you omit this field, the existing set of reviewers is unchanged.", + "items": { + "type": "object", + "required": [ + "reviewer_id", + "reviewer_type" + ], + "properties": { + "reviewer_id": { + "type": "integer", + "description": "The ID of the team or role selected as a bypass reviewer" + }, + "reviewer_type": { + "type": "string", + "description": "The type of the bypass reviewer", + "enum": [ + "TEAM", + "ROLE" + ] + }, + "mode": { + "type": "string", + "description": "The bypass mode for the reviewer", + "enum": [ + "ALWAYS", + "EXEMPT" + ], + "default": "ALWAYS" + } + } + } + } + } + } + } + }, + "has_issues": { + "type": "boolean", + "description": "Either `true` to enable issues for this repository or `false` to disable them.", + "default": true + }, + "has_projects": { + "type": "boolean", + "description": "Either `true` to enable projects for this repository or `false` to disable them. **Note:** If you're creating a repository in an organization that has disabled repository projects, the default is `false`, and if you pass `true`, the API returns an error.", + "default": true + }, + "has_wiki": { + "type": "boolean", + "description": "Either `true` to enable the wiki for this repository or `false` to disable it.", + "default": true + }, + "has_pull_requests": { + "type": "boolean", + "description": "Either `true` to allow pull requests for this repository or `false` to prevent pull requests.", + "default": true + }, + "pull_request_creation_policy": { + "type": "string", + "description": "The policy that controls who can create pull requests for this repository: `all` or `collaborators_only`.", + "enum": [ + "all", + "collaborators_only" + ] + }, + "is_template": { + "type": "boolean", + "description": "Either `true` to make this repo available as a template repository or `false` to prevent it.", + "default": false + }, + "default_branch": { + "type": "string", + "description": "Updates the default branch for this repository." + }, + "allow_squash_merge": { + "type": "boolean", + "description": "Either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging.", + "default": true + }, + "allow_merge_commit": { + "type": "boolean", + "description": "Either `true` to allow merging pull requests with a merge commit, or `false` to prevent merging pull requests with merge commits.", + "default": true + }, + "allow_rebase_merge": { + "type": "boolean", + "description": "Either `true` to allow rebase-merging pull requests, or `false` to prevent rebase-merging.", + "default": true + }, + "allow_auto_merge": { + "type": "boolean", + "description": "Either `true` to allow auto-merge on pull requests, or `false` to disallow auto-merge.", + "default": false + }, + "delete_branch_on_merge": { + "type": "boolean", + "description": "Either `true` to allow automatically deleting head branches when pull requests are merged, or `false` to prevent automatic deletion.", + "default": false + }, + "allow_update_branch": { + "type": "boolean", + "description": "Either `true` to always allow a pull request head branch that is behind its base branch to be updated even if it is not required to be up to date before merging, or false otherwise.", + "default": false + }, + "use_squash_pr_title_as_default": { + "type": "boolean", + "description": "Either `true` to allow squash-merge commits to use pull request title, or `false` to use commit message. **This property is closing down. Please use `squash_merge_commit_title` instead.", + "default": false, + "deprecated": true + }, + "squash_merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "COMMIT_OR_PR_TITLE" + ], + "description": "Required when using `squash_merge_commit_message`.\n\nThe default value for a squash merge commit title:\n\n- `PR_TITLE` - default to the pull request's title.\n- `COMMIT_OR_PR_TITLE` - default to the commit's title (if only one commit) or the pull request's title (when more than one commit)." + }, + "squash_merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "COMMIT_MESSAGES", + "BLANK" + ], + "description": "The default value for a squash merge commit message:\n\n- `PR_BODY` - default to the pull request's body.\n- `COMMIT_MESSAGES` - default to the branch's commit messages.\n- `BLANK` - default to a blank commit message." + }, + "merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "MERGE_MESSAGE" + ], + "description": "Required when using `merge_commit_message`.\n\nThe default value for a merge commit title.\n\n- `PR_TITLE` - default to the pull request's title.\n- `MERGE_MESSAGE` - default to the classic title for a merge message (e.g., Merge pull request #123 from branch-name)." + }, + "merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "PR_TITLE", + "BLANK" + ], + "description": "The default value for a merge commit message.\n\n- `PR_TITLE` - default to the pull request's title.\n- `PR_BODY` - default to the pull request's body.\n- `BLANK` - default to a blank commit message." + }, + "archived": { + "type": "boolean", + "description": "Whether to archive this repository. `false` will unarchive a previously archived repository.", + "default": false + }, + "allow_forking": { + "type": "boolean", + "description": "Either `true` to allow private forks, or `false` to prevent private forks.", + "default": false + }, + "web_commit_signoff_required": { + "type": "boolean", + "description": "Either `true` to require contributors to sign off on web-based commits, or `false` to not require contributors to sign off on web-based commits.", + "default": false + } + } + }, + { + "type": "object", + "properties": { + "auto_init": { + "description": "Create an initial commit with empty README. Keep this set to true in most cases since many of the policies below cannot be implemented on bare repos", + "type": "boolean" + }, + "gitignore_template": { + "description": "Desired language or platform [.gitignore template](https://github.com/github/gitignore) to apply. Use the name of the template without the extension. For example, 'Haskell'.", + "type": "string" + }, + "license_template": { + "description": "Choose an [open source license template](https://choosealicense.com/) that best suits your needs, and then use the [license keyword](https://help.github.com/articles/licensing-a-repository/#searching-github-by-license-type) as the `license_template` string. For example, 'mit' or 'mpl-2.0'.", + "type": "string" + }, + "topics": { + "description": "A list of topics to set on the repository", + "type": "array", + "items": { + "type": "string" + } + }, + "security": { + "description": "Settings for Code security and analysis", + "type": "object", + "properties": { + "enableVulnerabilityAlerts": { + "type": "boolean" + }, + "enableAutomatedSecurityFixes": { + "type": "boolean" + } + } + }, + "force_create": { + "description": "Force create the repository", + "type": "boolean" + }, + "template": { + "description": "Name of a template repository to use when creating the repository", + "type": "string" + } + } + } + ] + }, + "LabelSettings": { + "description": "Labels: define labels for Issues and Pull Requests", + "type": "object", + "properties": { + "include": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "color": { + "description": "The hexadecimal color code for the label. If including a `#`, make sure to wrap it with quotes!", + "type": "string" + }, + "description": { + "type": "string" + }, + "oldname": { + "description": "Include the old name to rename an existing label", + "type": "string" + } + } + } + }, + "exclude": { + "description": "Ignore any labels matching these regexes (don't delete them)", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "CollaboratorSettings": { + "description": "A collaborator entry giving a specific user access to a repository.", + "allOf": [ + { + "type": "object", + "properties": { + "permission": { + "type": "string", + "description": "The permission to grant the collaborator. **Only valid on organization-owned repositories.** We accept the following permissions to be set: `pull`, `triage`, `push`, `maintain`, `admin` and you can also specify a custom repository role name, if the owning organization has defined any.", + "default": "push" + } + } + }, + { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "exclude": { + "description": "You can exclude a list of repos for this collaborator and all repos except these repos would have this collaborator", + "type": "array", + "items": { + "type": "string" + } + }, + "include": { + "description": "You can include a list of repos for this collaborator and only those repos would have this collaborator", + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + }, + "TeamSettings": { + "description": "A team entry", + "allOf": [ + { + "type": "object", + "properties": { + "permission": { + "type": "string", + "description": "The permission to grant the team on this repository. We accept the following permissions to be set: `pull`, `triage`, `push`, `maintain`, `admin` and you can also specify a custom repository role name, if the owning organization has defined any. If no permission is specified, the team's `permission` attribute will be used to determine what permission to grant the team on this repository." + } + } + }, + { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "The name (or slug) of the team to grant access to the repo.", + "type": "string" + }, + "privacy": { + "description": "The level of privacy the team should have when it is created by safe-settings. The options are `secret` (visible only to organization owners and members of the team) or `closed` (visible to all members of the organization).", + "type": "string", + "enum": [ + "secret", + "closed" + ] + }, + "external_group": { + "description": "The display name of an external IdP group to link to this team.", + "type": "string" + }, + "exclude": { + "description": "Exclude this team from repos whose names match one of these glob patterns.", + "type": "array", + "items": { + "type": "string" + } + }, + "include": { + "description": "Apply this team only to repos whose names match one of these glob patterns.", + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + }, + "MilestoneSettings": { + "description": "A milestone entry", + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "state": { + "description": "The state of the milestone. Either `open` or `closed`", + "type": "string" + } + } + }, + "BranchSettings": { + "description": "A branch protection rule entry", + "type": "object", + "properties": { + "name": { + "description": "If the name of the branch value is specified as `default`, then the app will create a branch protection rule to apply against the default branch in the repo", + "type": "string" + }, + "protection": { + "type": "object", + "properties": { + "required_status_checks": { + "type": "object", + "description": "Require status checks to pass before merging. Set to `null` to disable.", + "nullable": true, + "properties": { + "strict": { + "type": "boolean", + "description": "Require branches to be up to date before merging." + }, + "contexts": { + "type": "array", + "deprecated": true, + "description": "**Closing down notice**: The list of status checks to require in order to merge into this branch. If any of these checks have recently been set by a particular GitHub App, they will be required to come from that app in future for the branch to merge. Use `checks` instead of `contexts` for more fine-grained control.", + "items": { + "type": "string" + } + }, + "checks": { + "type": "array", + "description": "The list of status checks to require in order to merge into this branch.", + "items": { + "type": "object", + "required": [ + "context" + ], + "properties": { + "context": { + "type": "string", + "description": "The name of the required check" + }, + "app_id": { + "type": "integer", + "description": "The ID of the GitHub App that must provide this check. Omit this field to automatically select the GitHub App that has recently provided this check, or any app if it was not set by a GitHub App. Pass -1 to explicitly allow any app to set the status." + } + } + } + } + }, + "required": [ + "strict", + "contexts" + ] + }, + "enforce_admins": { + "type": "boolean", + "description": "Enforce all configured restrictions for administrators. Set to `true` to enforce required status checks for repository administrators. Set to `null` to disable.", + "nullable": true + }, + "required_pull_request_reviews": { + "type": "object", + "description": "Require at least one approving review on a pull request, before merging. Set to `null` to disable.", + "nullable": true, + "properties": { + "dismissal_restrictions": { + "type": "object", + "description": "Specify which users, teams, and apps can dismiss pull request reviews. Pass an empty `dismissal_restrictions` object to disable. User and team `dismissal_restrictions` are only available for organization-owned repositories. Omit this parameter for personal repositories.", + "properties": { + "users": { + "type": "array", + "description": "The list of user `login`s with dismissal access", + "items": { + "type": "string" + } + }, + "teams": { + "type": "array", + "description": "The list of team `slug`s with dismissal access", + "items": { + "type": "string" + } + }, + "apps": { + "type": "array", + "description": "The list of app `slug`s with dismissal access", + "items": { + "type": "string" + } + } + } + }, + "dismiss_stale_reviews": { + "type": "boolean", + "description": "Set to `true` if you want to automatically dismiss approving reviews when someone pushes a new commit." + }, + "require_code_owner_reviews": { + "type": "boolean", + "description": "Blocks merging pull requests until [code owners](https://docs.github.com/articles/about-code-owners/) review them." + }, + "required_approving_review_count": { + "type": "integer", + "description": "Specify the number of reviewers required to approve pull requests. Use a number between 1 and 6 or 0 to not require reviewers." + }, + "require_last_push_approval": { + "type": "boolean", + "description": "Whether the most recent push must be approved by someone other than the person who pushed it. Default: `false`.", + "default": false + }, + "bypass_pull_request_allowances": { + "type": "object", + "description": "Allow specific users, teams, or apps to bypass pull request requirements.", + "properties": { + "users": { + "type": "array", + "description": "The list of user `login`s allowed to bypass pull request requirements.", + "items": { + "type": "string" + } + }, + "teams": { + "type": "array", + "description": "The list of team `slug`s allowed to bypass pull request requirements.", + "items": { + "type": "string" + } + }, + "apps": { + "type": "array", + "description": "The list of app `slug`s allowed to bypass pull request requirements.", + "items": { + "type": "string" + } + } + } + } + } + }, + "restrictions": { + "type": "object", + "description": "Restrict who can push to the protected branch. User, app, and team `restrictions` are only available for organization-owned repositories. Set to `null` to disable.", + "nullable": true, + "properties": { + "users": { + "type": "array", + "description": "The list of user `login`s with push access", + "items": { + "type": "string" + } + }, + "teams": { + "type": "array", + "description": "The list of team `slug`s with push access", + "items": { + "type": "string" + } + }, + "apps": { + "type": "array", + "description": "The list of app `slug`s with push access", + "items": { + "type": "string" + } + } + }, + "required": [ + "users", + "teams" + ] + }, + "required_linear_history": { + "type": "boolean", + "description": "Enforces a linear commit Git history, which prevents anyone from pushing merge commits to a branch. Set to `true` to enforce a linear commit history. Set to `false` to disable a linear commit Git history. Your repository must allow squash merging or rebase merging before you can enable a linear commit history. Default: `false`. For more information, see \"[Requiring a linear commit history](https://docs.github.com/github/administering-a-repository/requiring-a-linear-commit-history)\" in the GitHub Help documentation." + }, + "allow_force_pushes": { + "type": "boolean", + "description": "Permits force pushes to the protected branch by anyone with write access to the repository. Set to `true` to allow force pushes. Set to `false` or `null` to block force pushes. Default: `false`. For more information, see \"[Enabling force pushes to a protected branch](https://docs.github.com/github/administering-a-repository/enabling-force-pushes-to-a-protected-branch)\" in the GitHub Help documentation.\"", + "nullable": true + }, + "allow_deletions": { + "type": "boolean", + "description": "Allows deletion of the protected branch by anyone with write access to the repository. Set to `false` to prevent deletion of the protected branch. Default: `false`. For more information, see \"[Enabling force pushes to a protected branch](https://docs.github.com/github/administering-a-repository/enabling-force-pushes-to-a-protected-branch)\" in the GitHub Help documentation." + }, + "block_creations": { + "type": "boolean", + "description": "If set to `true`, the `restrictions` branch protection settings which limits who can push will also block pushes which create new branches, unless the push is initiated by a user, team, or app which has the ability to push. Set to `true` to restrict new branch creation. Default: `false`." + }, + "required_conversation_resolution": { + "type": "boolean", + "description": "Requires all conversations on code to be resolved before a pull request can be merged into a branch that matches this rule. Set to `false` to disable. Default: `false`." + }, + "lock_branch": { + "type": "boolean", + "description": "Whether to set the branch as read-only. If this is true, users will not be able to push to the branch. Default: `false`.", + "default": false + }, + "allow_fork_syncing": { + "type": "boolean", + "description": "Whether users can pull changes from upstream when the branch is locked. Set to `true` to allow fork syncing. Set to `false` to prevent fork syncing. Default: `false`.", + "default": false + } + }, + "required": [ + "required_status_checks", + "enforce_admins", + "required_pull_request_reviews", + "restrictions" + ] + } + } + }, + "AutolinkSettings": { + "description": "An autolink reference entry", + "type": "object", + "properties": { + "key_prefix": { + "type": "string", + "description": "This prefix appended by certain characters will generate a link any time it is found in an issue, pull request, or commit." + }, + "url_template": { + "type": "string", + "description": "The URL must contain `` for the reference number. `` matches different characters depending on the value of `is_alphanumeric`." + }, + "is_alphanumeric": { + "type": "boolean", + "default": true, + "description": "Whether this autolink reference matches alphanumeric characters. If true, the `` parameter of the `url_template` matches alphanumeric characters `A-Z` (case insensitive), `0-9`, and `-`. If false, this autolink reference only matches numeric characters." + } + }, + "required": [ + "key_prefix", + "url_template" + ] + }, + "ValidatorSettings": { + "description": "Repository name validation", + "type": "object", + "properties": { + "pattern": { + "type": "string" + } + } + }, + "RulesetSettings": { + "description": "A ruleset entry", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the ruleset." + }, + "target": { + "type": "string", + "description": "The target of the ruleset", + "enum": [ + "branch", + "tag", + "push", + "repository" + ], + "default": "branch" + }, + "enforcement": { + "type": "string", + "description": "The enforcement level of the ruleset. `evaluate` allows admins to test rules before enforcing them. Admins can view insights on the Rule Insights page (`evaluate` is only available with GitHub Enterprise).", + "enum": [ + "disabled", + "active", + "evaluate" + ] + }, + "bypass_actors": { + "type": "array", + "description": "The actors that can bypass the rules in this ruleset", + "items": { + "title": "Repository Ruleset Bypass Actor", + "type": "object", + "description": "An actor that can bypass rules in a ruleset", + "required": [ + "actor_type" + ], + "properties": { + "actor_id": { + "type": "integer", + "nullable": true, + "description": "The ID of the actor that can bypass a ruleset. Required for `Integration`, `RepositoryRole`, `Team`, and `User` actor types. If `actor_type` is `OrganizationAdmin`, `actor_id` is ignored. If `actor_type` is `DeployKey`, this should be null. `OrganizationAdmin` is not applicable for personal repositories." + }, + "actor_type": { + "type": "string", + "enum": [ + "Integration", + "OrganizationAdmin", + "RepositoryRole", + "Team", + "DeployKey", + "User" + ], + "description": "The type of actor that can bypass a ruleset." + }, + "bypass_mode": { + "type": "string", + "description": "When the specified actor can bypass the ruleset. `pull_request` means that an actor can only bypass rules on pull requests. `pull_request` is not applicable for the `DeployKey` actor type. Also, `pull_request` is only applicable to branch rulesets. When `bypass_mode` is `exempt`, rules will not be run for that actor and a bypass audit entry will not be created.", + "enum": [ + "always", + "pull_request", + "exempt" + ], + "default": "always" + }, + "name": { + "type": "string", + "description": "Human-friendly alternative to `actor_id`. The team slug, username, GitHub App slug, or repository role name (resolved using `actor_type`). Cannot be combined with `actor_id`." + } + } + } + }, + "conditions": { + "title": "Organization ruleset conditions", + "type": "object", + "description": "Conditions for an organization ruleset.\nThe branch and tag rulesets conditions object should contain both `repository_name` and `ref_name` properties, or both `repository_id` and `ref_name` properties, or both `repository_property` and `ref_name` properties.\nThe push rulesets conditions object does not require the `ref_name` property.\nFor repository policy rulesets, the conditions object should only contain the `repository_name`, the `repository_id`, or the `repository_property`.", + "oneOf": [ + { + "type": "object", + "title": "repository_name_and_ref_name", + "description": "Conditions to target repositories by name and refs by name", + "allOf": [ + { + "title": "Repository ruleset conditions for ref names", + "type": "object", + "description": "Parameters for a repository ruleset ref name condition", + "properties": { + "ref_name": { + "type": "object", + "properties": { + "include": { + "type": "array", + "description": "Array of ref names or patterns to include. One of these patterns must match for the condition to pass. Also accepts `~DEFAULT_BRANCH` to include the default branch or `~ALL` to include all branches.", + "items": { + "type": "string" + } + }, + "exclude": { + "type": "array", + "description": "Array of ref names or patterns to exclude. The condition will not pass if any of these patterns match.", + "items": { + "type": "string" + } + } + } + } + } + }, + { + "title": "Repository ruleset conditions for repository names", + "type": "object", + "description": "Parameters for a repository name condition", + "properties": { + "repository_name": { + "type": "object", + "properties": { + "include": { + "type": "array", + "description": "Array of repository names or patterns to include. One of these patterns must match for the condition to pass. Also accepts `~ALL` to include all repositories.", + "items": { + "type": "string" + } + }, + "exclude": { + "type": "array", + "description": "Array of repository names or patterns to exclude. The condition will not pass if any of these patterns match.", + "items": { + "type": "string" + } + }, + "protected": { + "type": "boolean", + "description": "Whether renaming of target repositories is prevented." + } + } + } + }, + "required": [ + "repository_name" + ] + } + ] + }, + { + "type": "object", + "title": "repository_id_and_ref_name", + "description": "Conditions to target repositories by id and refs by name", + "allOf": [ + { + "title": "Repository ruleset conditions for ref names", + "type": "object", + "description": "Parameters for a repository ruleset ref name condition", + "properties": { + "ref_name": { + "type": "object", + "properties": { + "include": { + "type": "array", + "description": "Array of ref names or patterns to include. One of these patterns must match for the condition to pass. Also accepts `~DEFAULT_BRANCH` to include the default branch or `~ALL` to include all branches.", + "items": { + "type": "string" + } + }, + "exclude": { + "type": "array", + "description": "Array of ref names or patterns to exclude. The condition will not pass if any of these patterns match.", + "items": { + "type": "string" + } + } + } + } + } + }, + { + "title": "Repository ruleset conditions for repository IDs", + "type": "object", + "description": "Parameters for a repository ID condition", + "properties": { + "repository_id": { + "type": "object", + "properties": { + "repository_ids": { + "type": "array", + "description": "The repository IDs that the ruleset applies to. One of these IDs must match for the condition to pass.", + "items": { + "type": "integer" + } + } + } + } + }, + "required": [ + "repository_id" + ] + } + ] + }, + { + "type": "object", + "title": "repository_property_and_ref_name", + "description": "Conditions to target repositories by property and refs by name", + "allOf": [ + { + "title": "Repository ruleset conditions for ref names", + "type": "object", + "description": "Parameters for a repository ruleset ref name condition", + "properties": { + "ref_name": { + "type": "object", + "properties": { + "include": { + "type": "array", + "description": "Array of ref names or patterns to include. One of these patterns must match for the condition to pass. Also accepts `~DEFAULT_BRANCH` to include the default branch or `~ALL` to include all branches.", + "items": { + "type": "string" + } + }, + "exclude": { + "type": "array", + "description": "Array of ref names or patterns to exclude. The condition will not pass if any of these patterns match.", + "items": { + "type": "string" + } + } + } + } + } + }, + { + "title": "Repository ruleset conditions for repository properties", + "type": "object", + "description": "Parameters for a repository property condition", + "properties": { + "repository_property": { + "type": "object", + "properties": { + "include": { + "type": "array", + "description": "The repository properties and values to include. All of these properties must match for the condition to pass.", + "items": { + "title": "Repository ruleset property targeting definition", + "type": "object", + "description": "Parameters for a targeting a repository property", + "properties": { + "name": { + "type": "string", + "description": "The name of the repository property to target" + }, + "property_values": { + "type": "array", + "description": "The values to match for the repository property", + "items": { + "type": "string" + } + }, + "source": { + "type": "string", + "description": "The source of the repository property. Defaults to 'custom' if not specified.", + "enum": [ + "custom", + "system" + ] + } + }, + "required": [ + "name", + "property_values" + ] + } + }, + "exclude": { + "type": "array", + "description": "The repository properties and values to exclude. The condition will not pass if any of these properties match.", + "items": { + "title": "Repository ruleset property targeting definition", + "type": "object", + "description": "Parameters for a targeting a repository property", + "properties": { + "name": { + "type": "string", + "description": "The name of the repository property to target" + }, + "property_values": { + "type": "array", + "description": "The values to match for the repository property", + "items": { + "type": "string" + } + }, + "source": { + "type": "string", + "description": "The source of the repository property. Defaults to 'custom' if not specified.", + "enum": [ + "custom", + "system" + ] + } + }, + "required": [ + "name", + "property_values" + ] + } + } + } + } + }, + "required": [ + "repository_property" + ] + } + ] + } + ] + }, + "rules": { + "type": "array", + "description": "An array of rules within the ruleset.", + "items": { + "title": "Repository Rule", + "type": "object", + "description": "A repository rule.", + "oneOf": [ + { + "title": "creation", + "description": "Only allow users with bypass permission to create matching refs.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "creation" + ] + } + } + }, + { + "title": "update", + "description": "Only allow users with bypass permission to update matching refs.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "update" + ] + }, + "parameters": { + "type": "object", + "properties": { + "update_allows_fetch_and_merge": { + "type": "boolean", + "description": "Branch can pull changes from its upstream repository" + } + }, + "required": [ + "update_allows_fetch_and_merge" + ] + } + } + }, + { + "title": "deletion", + "description": "Only allow users with bypass permissions to delete matching refs.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "deletion" + ] + } + } + }, + { + "title": "required_linear_history", + "description": "Prevent merge commits from being pushed to matching refs.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "required_linear_history" + ] + } + } + }, + { + "title": "required_deployments", + "description": "Choose which environments must be successfully deployed to before refs can be pushed into a ref that matches this rule.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "required_deployments" + ] + }, + "parameters": { + "type": "object", + "properties": { + "required_deployment_environments": { + "type": "array", + "description": "The environments that must be successfully deployed to before branches can be merged.", + "items": { + "type": "string" + } + } + }, + "required": [ + "required_deployment_environments" + ] + } + } + }, + { + "title": "required_signatures", + "description": "Commits pushed to matching refs must have verified signatures.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "required_signatures" + ] + } + } + }, + { + "title": "pull_request", + "description": "Require all commits be made to a non-target branch and submitted via a pull request before they can be merged.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "pull_request" + ] + }, + "parameters": { + "type": "object", + "properties": { + "allowed_merge_methods": { + "type": "array", + "description": "Array of allowed merge methods. Allowed values include `merge`, `squash`, and `rebase`. At least one option must be enabled.", + "items": { + "type": "string", + "enum": [ + "merge", + "squash", + "rebase" + ] + } + }, + "dismiss_stale_reviews_on_push": { + "type": "boolean", + "description": "New, reviewable commits pushed will dismiss previous pull request review approvals." + }, + "dismissal_restriction": { + "title": "DismissalRestriction", + "description": "Specify people, teams, or apps allowed to dismiss pull request reviews.", + "type": "object", + "properties": { + "allowed_actors": { + "type": "array", + "description": "Specify people, teams, or apps allowed to dismiss pull request reviews.", + "items": { + "title": "Actor", + "description": "An actor allowed to dismiss pull request reviews", + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "ID of the actor that can dismiss reviews." + }, + "type": { + "type": "string", + "description": "The type of the actor", + "enum": [ + "User", + "Team", + "IntegrationInstallation", + "RepositoryRole" + ] + } + }, + "required": [ + "id", + "type" + ] + } + }, + "enabled": { + "type": "boolean", + "description": "Whether to restrict review dismissal to specific actors." + } + }, + "required": [ + "enabled" + ] + }, + "require_code_owner_review": { + "type": "boolean", + "description": "Require an approving review in pull requests that modify files that have a designated code owner." + }, + "require_last_push_approval": { + "type": "boolean", + "description": "Whether the most recent reviewable push must be approved by someone other than the person who pushed it." + }, + "required_approving_review_count": { + "type": "integer", + "description": "The number of approving reviews that are required before a pull request can be merged.", + "minimum": 0, + "maximum": 10 + }, + "required_review_thread_resolution": { + "type": "boolean", + "description": "All conversations on code must be resolved before a pull request can be merged." + }, + "required_reviewers": { + "type": "array", + "description": "> [!NOTE]\n> `required_reviewers` is in beta and subject to change.\n\nA collection of reviewers and associated file patterns. Each reviewer has a list of file patterns which determine the files that reviewer is required to review.", + "items": { + "title": "RequiredReviewerConfiguration", + "description": "A reviewing team, and file patterns describing which files they must approve changes to.", + "type": "object", + "properties": { + "file_patterns": { + "type": "array", + "description": "Array of file patterns. Pull requests which change matching files must be approved by the specified team. File patterns use fnmatch syntax.", + "items": { + "type": "string" + } + }, + "minimum_approvals": { + "type": "integer", + "description": "Minimum number of approvals required from the specified team. If set to zero, the team will be added to the pull request but approval is optional." + }, + "reviewer": { + "title": "Reviewer", + "description": "A required reviewing team", + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "ID of the reviewer which must review changes to matching files." + }, + "type": { + "type": "string", + "description": "The type of the reviewer", + "enum": [ + "Team" + ] + }, + "slug": { + "type": "string", + "description": "Human-friendly alternative to `id`. The slug of the team that must review changes to matching files. Cannot be combined with `id`." + } + }, + "required": [ + "type" + ] + } + }, + "required": [ + "file_patterns", + "minimum_approvals", + "reviewer" + ] + } + } + }, + "required": [ + "dismiss_stale_reviews_on_push", + "require_code_owner_review", + "require_last_push_approval", + "required_approving_review_count", + "required_review_thread_resolution" + ] + } + } + }, + { + "title": "required_status_checks", + "description": "Choose which status checks must pass before the ref is updated. When enabled, commits must first be pushed to another ref where the checks pass.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "required_status_checks" + ] + }, + "parameters": { + "type": "object", + "properties": { + "do_not_enforce_on_create": { + "type": "boolean", + "description": "Allow repositories and branches to be created if a check would otherwise prohibit it." + }, + "required_status_checks": { + "type": "array", + "description": "Status checks that are required.", + "items": { + "title": "StatusCheckConfiguration", + "description": "Required status check", + "type": "object", + "properties": { + "context": { + "type": "string", + "description": "The status check context name that must be present on the commit." + }, + "integration_id": { + "type": "integer", + "description": "The optional integration ID that this status check must originate from." + } + }, + "required": [ + "context" + ] + } + }, + "strict_required_status_checks_policy": { + "type": "boolean", + "description": "Whether pull requests targeting a matching branch must be tested with the latest code. This setting will not take effect unless at least one status check is enabled." + } + }, + "required": [ + "required_status_checks", + "strict_required_status_checks_policy" + ] + } + } + }, + { + "title": "non_fast_forward", + "description": "Prevent users with push access from force pushing to refs.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "non_fast_forward" + ] + } + } + }, + { + "title": "commit_message_pattern", + "description": "Parameters to be used for the commit_message_pattern rule", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "commit_message_pattern" + ] + }, + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "How this rule appears when configuring it." + }, + "negate": { + "type": "boolean", + "description": "If true, the rule will fail if the pattern matches." + }, + "operator": { + "type": "string", + "description": "The operator to use for matching.", + "enum": [ + "starts_with", + "ends_with", + "contains", + "regex" + ] + }, + "pattern": { + "type": "string", + "description": "The pattern to match with." + } + }, + "required": [ + "operator", + "pattern" + ] + } + } + }, + { + "title": "commit_author_email_pattern", + "description": "Parameters to be used for the commit_author_email_pattern rule", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "commit_author_email_pattern" + ] + }, + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "How this rule appears when configuring it." + }, + "negate": { + "type": "boolean", + "description": "If true, the rule will fail if the pattern matches." + }, + "operator": { + "type": "string", + "description": "The operator to use for matching.", + "enum": [ + "starts_with", + "ends_with", + "contains", + "regex" + ] + }, + "pattern": { + "type": "string", + "description": "The pattern to match with." + } + }, + "required": [ + "operator", + "pattern" + ] + } + } + }, + { + "title": "committer_email_pattern", + "description": "Parameters to be used for the committer_email_pattern rule", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "committer_email_pattern" + ] + }, + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "How this rule appears when configuring it." + }, + "negate": { + "type": "boolean", + "description": "If true, the rule will fail if the pattern matches." + }, + "operator": { + "type": "string", + "description": "The operator to use for matching.", + "enum": [ + "starts_with", + "ends_with", + "contains", + "regex" + ] + }, + "pattern": { + "type": "string", + "description": "The pattern to match with." + } + }, + "required": [ + "operator", + "pattern" + ] + } + } + }, + { + "title": "branch_name_pattern", + "description": "Parameters to be used for the branch_name_pattern rule", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "branch_name_pattern" + ] + }, + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "How this rule appears when configuring it." + }, + "negate": { + "type": "boolean", + "description": "If true, the rule will fail if the pattern matches." + }, + "operator": { + "type": "string", + "description": "The operator to use for matching.", + "enum": [ + "starts_with", + "ends_with", + "contains", + "regex" + ] + }, + "pattern": { + "type": "string", + "description": "The pattern to match with." + } + }, + "required": [ + "operator", + "pattern" + ] + } + } + }, + { + "title": "tag_name_pattern", + "description": "Parameters to be used for the tag_name_pattern rule", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "tag_name_pattern" + ] + }, + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "How this rule appears when configuring it." + }, + "negate": { + "type": "boolean", + "description": "If true, the rule will fail if the pattern matches." + }, + "operator": { + "type": "string", + "description": "The operator to use for matching.", + "enum": [ + "starts_with", + "ends_with", + "contains", + "regex" + ] + }, + "pattern": { + "type": "string", + "description": "The pattern to match with." + } + }, + "required": [ + "operator", + "pattern" + ] + } + } + }, + { + "title": "file_path_restriction", + "description": "Prevent commits that include changes in specified file and folder paths from being pushed to the commit graph. This includes absolute paths that contain file names.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "file_path_restriction" + ] + }, + "parameters": { + "type": "object", + "properties": { + "restricted_file_paths": { + "type": "array", + "description": "The file paths that are restricted from being pushed to the commit graph.", + "items": { + "type": "string" + } + } + }, + "required": [ + "restricted_file_paths" + ] + } + } + }, + { + "title": "max_file_path_length", + "description": "Prevent commits that include file paths that exceed the specified character limit from being pushed to the commit graph.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "max_file_path_length" + ] + }, + "parameters": { + "type": "object", + "properties": { + "max_file_path_length": { + "type": "integer", + "description": "The maximum amount of characters allowed in file paths.", + "minimum": 1, + "maximum": 32767 + } + }, + "required": [ + "max_file_path_length" + ] + } + } + }, + { + "title": "file_extension_restriction", + "description": "Prevent commits that include files with specified file extensions from being pushed to the commit graph.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "file_extension_restriction" + ] + }, + "parameters": { + "type": "object", + "properties": { + "restricted_file_extensions": { + "type": "array", + "description": "The file extensions that are restricted from being pushed to the commit graph.", + "items": { + "type": "string" + } + } + }, + "required": [ + "restricted_file_extensions" + ] + } + } + }, + { + "title": "max_file_size", + "description": "Prevent commits with individual files that exceed the specified limit from being pushed to the commit graph.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "max_file_size" + ] + }, + "parameters": { + "type": "object", + "properties": { + "max_file_size": { + "type": "integer", + "description": "The maximum file size allowed in megabytes. This limit does not apply to Git Large File Storage (Git LFS).", + "minimum": 1, + "maximum": 100 + } + }, + "required": [ + "max_file_size" + ] + } + } + }, + { + "title": "workflows", + "description": "Require all changes made to a targeted branch to pass the specified workflows before they can be merged.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "workflows" + ] + }, + "parameters": { + "type": "object", + "properties": { + "do_not_enforce_on_create": { + "type": "boolean", + "description": "Allow repositories and branches to be created if a check would otherwise prohibit it." + }, + "workflows": { + "type": "array", + "description": "Workflows that must pass for this rule to pass.", + "items": { + "title": "WorkflowFileReference", + "description": "A workflow that must run for this rule to pass", + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "The path to the workflow file" + }, + "ref": { + "type": "string", + "description": "The ref (branch or tag) of the workflow file to use" + }, + "repository_id": { + "type": "integer", + "description": "The ID of the repository where the workflow is defined" + }, + "sha": { + "type": "string", + "description": "The commit SHA of the workflow file to use" + } + }, + "required": [ + "path", + "repository_id" + ] + } + } + }, + "required": [ + "workflows" + ] + } + } + }, + { + "title": "code_scanning", + "description": "Choose which tools must provide code scanning results before the reference is updated. When configured, code scanning must be enabled and have results for both the commit and the reference being updated.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "code_scanning" + ] + }, + "parameters": { + "type": "object", + "properties": { + "code_scanning_tools": { + "type": "array", + "description": "Tools that must provide code scanning results for this rule to pass.", + "items": { + "title": "CodeScanningTool", + "description": "A tool that must provide code scanning results for this rule to pass.", + "type": "object", + "properties": { + "alerts_threshold": { + "type": "string", + "description": "The severity level at which code scanning results that raise alerts block a reference update. For more information on alert severity levels, see \"[About code scanning alerts](https://docs.github.com/code-security/code-scanning/managing-code-scanning-alerts/about-code-scanning-alerts#about-alert-severity-and-security-severity-levels).\"", + "enum": [ + "none", + "errors", + "errors_and_warnings", + "all" + ] + }, + "security_alerts_threshold": { + "type": "string", + "description": "The severity level at which code scanning results that raise security alerts block a reference update. For more information on security severity levels, see \"[About code scanning alerts](https://docs.github.com/code-security/code-scanning/managing-code-scanning-alerts/about-code-scanning-alerts#about-alert-severity-and-security-severity-levels).\"", + "enum": [ + "none", + "critical", + "high_or_higher", + "medium_or_higher", + "all" + ] + }, + "tool": { + "type": "string", + "description": "The name of a code scanning tool" + } + }, + "required": [ + "alerts_threshold", + "security_alerts_threshold", + "tool" + ] + } + } + }, + "required": [ + "code_scanning_tools" + ] + } + } + }, + { + "title": "copilot_code_review", + "description": "Request Copilot code review for new pull requests automatically if the author has access to Copilot code review and their premium requests quota has not reached the limit.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "copilot_code_review" + ] + }, + "parameters": { + "type": "object", + "properties": { + "review_draft_pull_requests": { + "type": "boolean", + "description": "Copilot automatically reviews draft pull requests before they are marked as ready for review." + }, + "review_on_push": { + "type": "boolean", + "description": "Copilot automatically reviews each new push to the pull request." + } + } + } + } + } + ] + } + } + }, + "required": [ + "name", + "enforcement" + ] + }, + "EnvironmentsSettings": { + "description": "A deployment environment configuration entry", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "The name of the deployment environment", + "type": "string" + }, + "wait_timer": { + "description": "The amount of time to delay a job after the job is initially triggered (in minutes)", + "type": "integer" + }, + "reviewers": { + "description": "The people or teams that may review jobs that reference the environment", + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "description": "The type of reviewer (`User` or `Team`)", + "type": "string" + }, + "id": { + "description": "The id of the user or team who can review the deployment", + "type": "integer" + } + } + } + }, + "deployment_branch_policy": { + "description": "The type of deployment branch policy for this environment", + "type": "object", + "properties": { + "protected_branches": { + "description": "Whether only protected branches can be deployed to this environment", + "type": "boolean" + }, + "custom_branch_policies": { + "description": "Whether only branches that match the specified name patterns can deploy to this environment", + "type": "boolean" + } + } + }, + "prevent_self_review": { + "description": "Whether or not a user who created the job is prevented from approving their own job", + "type": "boolean" + } + } + }, + "CustomPropertiesSettings": { + "description": "A custom property entry", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "VariablesSettings": { + "description": "An Actions variable entry", + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "description": "The name of the variable", + "type": "string" + }, + "value": { + "description": "The value of the variable", + "type": "string" + }, + "visibility": { + "description": "The visibility of the variable. Can be `all`, `private`, or `selected`", + "type": "string", + "enum": [ + "all", + "private", + "selected" + ] + } + } + }, + "custom_repository_roles": { + "description": "Org-level custom repository roles. Only valid in the org-level settings.yml.", + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "base_role", + "permissions" + ], + "properties": { + "name": { + "type": "string", + "description": "The name of the custom role." + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "A short description of the role." + }, + "base_role": { + "type": "string", + "enum": [ + "read", + "triage", + "write", + "maintain" + ], + "description": "The system role from which this role inherits permissions." + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Additional fine-grained permissions included in this role." + } + } + } + }, + "app_installations": { + "description": "Manage which repositories a GitHub App installation can access. The target is a GitHub App installation rather than a repository. Repo selection follows the config hierarchy: org-level settings.yml selects all repos in the org; suborgs/*.yml selects repos by the suborg's targeting criteria; repos/*.yml adds the specific repo. Requires safe-settings to be installed on the enterprise with 'Enterprise organization installations' permission.", + "type": "array", + "items": { + "type": "object", + "required": [ + "app_slug" + ], + "additionalProperties": false, + "properties": { + "app_slug": { + "type": "string", + "description": "The slug of the GitHub App installation to manage." + } + } + } + }, + "additive_plugins": { + "description": "List of plugins to run in additive mode. In additive mode the plugin will only add and update entries; it will never call remove(), so items that exist on GitHub but are absent from the YAML are preserved. Supported plugins: labels, collaborators, teams, milestones, autolinks, environments, custom_properties, variables, rulesets, custom_repository_roles, app_installations. Declare only in settings.yml (org level) to keep behavior consistent across all repos.", + "type": "array", + "items": { + "type": "string", + "enum": [ + "labels", + "collaborators", + "teams", + "milestones", + "autolinks", + "environments", + "custom_properties", + "variables", + "rulesets", + "custom_repository_roles", + "app_installations" + ] + } + }, + "disable_plugins": { + "description": "List of plugins to disable at this configuration layer. Each entry is either a plugin name (string shorthand, equivalent to target: all) or an object {plugin, target}. target=self disables the plugin at this layer only; target=children disables it at all lower layers; target=all disables it at this layer and all lower layers. Cascade is union-only; lower layers cannot re-enable a disabled plugin.", + "type": "array", + "items": { + "oneOf": [ + { + "type": "string", + "enum": [ + "repository", + "labels", + "collaborators", + "teams", + "milestones", + "branches", + "autolinks", + "validator", + "rulesets", + "environments", + "custom_properties", + "custom_repository_roles", + "variables", + "archive", + "app_installations" + ] + }, + { + "type": "object", + "required": [ + "plugin" + ], + "additionalProperties": false, + "properties": { + "plugin": { + "type": "string", + "enum": [ + "repository", + "labels", + "collaborators", + "teams", + "milestones", + "branches", + "autolinks", + "validator", + "rulesets", + "environments", + "custom_properties", + "custom_repository_roles", + "variables", + "archive", + "app_installations" + ] + }, + "target": { + "type": "string", + "enum": [ + "self", + "children", + "all" + ], + "default": "all" + } + } + } + ] + } } } } \ No newline at end of file diff --git a/schema/dereferenced/suborgs.json b/schema/dereferenced/suborgs.json new file mode 100644 index 000000000..6e7bb3254 --- /dev/null +++ b/schema/dereferenced/suborgs.json @@ -0,0 +1,2879 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Safe-settings suborg-level configuration", + "description": "Schema for .github/suborgs/*.yml — suborg-level safe-settings configuration. Defines which repos belong to the suborg and what settings to apply.", + "type": "object", + "properties": { + "suborgrepos": { + "type": "array", + "description": "Glob patterns matching repository names. Repos whose names match any pattern are included in this suborg.", + "items": { + "type": "string" + } + }, + "suborgteams": { + "type": "array", + "description": "Team slugs. Repos that belong to any of these teams are included in this suborg.", + "items": { + "type": "string" + } + }, + "suborgproperties": { + "type": "array", + "description": "Custom property filters. Repos with matching custom property values are included in this suborg.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "The name of the custom property" + }, + "values": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Accepted values for this property" + } + } + } + }, + "repositories": { + "description": "Repository settings. Use force_create to create the repository if it does not exist, and template to specify a template repository to use when creating it.", + "allOf": [ + { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the repository." + }, + "description": { + "type": "string", + "description": "A short description of the repository." + }, + "homepage": { + "type": "string", + "description": "A URL with more information about the repository." + }, + "private": { + "type": "boolean", + "description": "Either `true` to make the repository private or `false` to make it public. Default: `false`. \n**Note**: You will get a `422` error if the organization restricts [changing repository visibility](https://docs.github.com/articles/repository-permission-levels-for-an-organization#changing-the-visibility-of-repositories) to organization owners and a non-owner tries to change the value of private.", + "default": false + }, + "visibility": { + "type": "string", + "description": "The visibility of the repository.", + "enum": [ + "public", + "private" + ] + }, + "security_and_analysis": { + "type": "object", + "description": "Specify which security and analysis features to enable or disable for the repository.\n\nTo use this parameter, you must have admin permissions for the repository or be an owner or security manager for the organization that owns the repository. For more information, see \"[Managing security managers in your organization](https://docs.github.com/organizations/managing-peoples-access-to-your-organization-with-roles/managing-security-managers-in-your-organization).\"\n\nFor example, to enable GitHub Advanced Security, use this data in the body of the `PATCH` request:\n`{ \"security_and_analysis\": {\"advanced_security\": { \"status\": \"enabled\" } } }`.\n\nYou can check which security and analysis features are currently enabled by using a `GET /repos/{owner}/{repo}` request.", + "nullable": true, + "properties": { + "advanced_security": { + "type": "object", + "description": "Use the `status` property to enable or disable GitHub Advanced Security for this repository.\nFor more information, see \"[About GitHub Advanced\nSecurity](/github/getting-started-with-github/learning-about-github/about-github-advanced-security).\"\n\nFor standalone Code Scanning or Secret Protection products, this parameter cannot be used.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "code_security": { + "type": "object", + "description": "Use the `status` property to enable or disable GitHub Code Security for this repository.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning for this repository. For more information, see \"[About secret scanning](/code-security/secret-security/about-secret-scanning).\"", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_push_protection": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning push protection for this repository. For more information, see \"[Protecting pushes with secret scanning](/code-security/secret-scanning/protecting-pushes-with-secret-scanning).\"", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_ai_detection": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning AI detection for this repository. For more information, see \"[Responsible detection of generic secrets with AI](https://docs.github.com/code-security/secret-scanning/using-advanced-secret-scanning-and-push-protection-features/generic-secret-detection/responsible-ai-generic-secrets).\"", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_non_provider_patterns": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning non-provider patterns for this repository. For more information, see \"[Supported secret scanning patterns](/code-security/secret-scanning/introduction/supported-secret-scanning-patterns#supported-secrets).\"", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_delegated_alert_dismissal": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning delegated alert dismissal for this repository.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_delegated_bypass": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning delegated bypass for this repository.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_delegated_bypass_options": { + "type": "object", + "description": "Feature options for secret scanning delegated bypass.\nThis object is only honored when `security_and_analysis.secret_scanning_delegated_bypass.status` is set to `enabled`.\nYou can send this object in the same request as `secret_scanning_delegated_bypass`, or update just the options in a separate request.", + "properties": { + "reviewers": { + "type": "array", + "description": "The bypass reviewers for secret scanning delegated bypass.\nIf you omit this field, the existing set of reviewers is unchanged.", + "items": { + "type": "object", + "required": [ + "reviewer_id", + "reviewer_type" + ], + "properties": { + "reviewer_id": { + "type": "integer", + "description": "The ID of the team or role selected as a bypass reviewer" + }, + "reviewer_type": { + "type": "string", + "description": "The type of the bypass reviewer", + "enum": [ + "TEAM", + "ROLE" + ] + }, + "mode": { + "type": "string", + "description": "The bypass mode for the reviewer", + "enum": [ + "ALWAYS", + "EXEMPT" + ], + "default": "ALWAYS" + } + } + } + } + } + } + } + }, + "has_issues": { + "type": "boolean", + "description": "Either `true` to enable issues for this repository or `false` to disable them.", + "default": true + }, + "has_projects": { + "type": "boolean", + "description": "Either `true` to enable projects for this repository or `false` to disable them. **Note:** If you're creating a repository in an organization that has disabled repository projects, the default is `false`, and if you pass `true`, the API returns an error.", + "default": true + }, + "has_wiki": { + "type": "boolean", + "description": "Either `true` to enable the wiki for this repository or `false` to disable it.", + "default": true + }, + "has_pull_requests": { + "type": "boolean", + "description": "Either `true` to allow pull requests for this repository or `false` to prevent pull requests.", + "default": true + }, + "pull_request_creation_policy": { + "type": "string", + "description": "The policy that controls who can create pull requests for this repository: `all` or `collaborators_only`.", + "enum": [ + "all", + "collaborators_only" + ] + }, + "is_template": { + "type": "boolean", + "description": "Either `true` to make this repo available as a template repository or `false` to prevent it.", + "default": false + }, + "default_branch": { + "type": "string", + "description": "Updates the default branch for this repository." + }, + "allow_squash_merge": { + "type": "boolean", + "description": "Either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging.", + "default": true + }, + "allow_merge_commit": { + "type": "boolean", + "description": "Either `true` to allow merging pull requests with a merge commit, or `false` to prevent merging pull requests with merge commits.", + "default": true + }, + "allow_rebase_merge": { + "type": "boolean", + "description": "Either `true` to allow rebase-merging pull requests, or `false` to prevent rebase-merging.", + "default": true + }, + "allow_auto_merge": { + "type": "boolean", + "description": "Either `true` to allow auto-merge on pull requests, or `false` to disallow auto-merge.", + "default": false + }, + "delete_branch_on_merge": { + "type": "boolean", + "description": "Either `true` to allow automatically deleting head branches when pull requests are merged, or `false` to prevent automatic deletion.", + "default": false + }, + "allow_update_branch": { + "type": "boolean", + "description": "Either `true` to always allow a pull request head branch that is behind its base branch to be updated even if it is not required to be up to date before merging, or false otherwise.", + "default": false + }, + "use_squash_pr_title_as_default": { + "type": "boolean", + "description": "Either `true` to allow squash-merge commits to use pull request title, or `false` to use commit message. **This property is closing down. Please use `squash_merge_commit_title` instead.", + "default": false, + "deprecated": true + }, + "squash_merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "COMMIT_OR_PR_TITLE" + ], + "description": "Required when using `squash_merge_commit_message`.\n\nThe default value for a squash merge commit title:\n\n- `PR_TITLE` - default to the pull request's title.\n- `COMMIT_OR_PR_TITLE` - default to the commit's title (if only one commit) or the pull request's title (when more than one commit)." + }, + "squash_merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "COMMIT_MESSAGES", + "BLANK" + ], + "description": "The default value for a squash merge commit message:\n\n- `PR_BODY` - default to the pull request's body.\n- `COMMIT_MESSAGES` - default to the branch's commit messages.\n- `BLANK` - default to a blank commit message." + }, + "merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "MERGE_MESSAGE" + ], + "description": "Required when using `merge_commit_message`.\n\nThe default value for a merge commit title.\n\n- `PR_TITLE` - default to the pull request's title.\n- `MERGE_MESSAGE` - default to the classic title for a merge message (e.g., Merge pull request #123 from branch-name)." + }, + "merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "PR_TITLE", + "BLANK" + ], + "description": "The default value for a merge commit message.\n\n- `PR_TITLE` - default to the pull request's title.\n- `PR_BODY` - default to the pull request's body.\n- `BLANK` - default to a blank commit message." + }, + "archived": { + "type": "boolean", + "description": "Whether to archive this repository. `false` will unarchive a previously archived repository.", + "default": false + }, + "allow_forking": { + "type": "boolean", + "description": "Either `true` to allow private forks, or `false` to prevent private forks.", + "default": false + }, + "web_commit_signoff_required": { + "type": "boolean", + "description": "Either `true` to require contributors to sign off on web-based commits, or `false` to not require contributors to sign off on web-based commits.", + "default": false + } + } + }, + { + "type": "object", + "properties": { + "auto_init": { + "description": "Create an initial commit with empty README. Keep this set to true in most cases since many of the policies below cannot be implemented on bare repos", + "type": "boolean" + }, + "gitignore_template": { + "description": "Desired language or platform [.gitignore template](https://github.com/github/gitignore) to apply. Use the name of the template without the extension. For example, 'Haskell'.", + "type": "string" + }, + "license_template": { + "description": "Choose an [open source license template](https://choosealicense.com/) that best suits your needs, and then use the [license keyword](https://help.github.com/articles/licensing-a-repository/#searching-github-by-license-type) as the `license_template` string. For example, 'mit' or 'mpl-2.0'.", + "type": "string" + }, + "topics": { + "description": "A list of topics to set on the repository", + "type": "array", + "items": { + "type": "string" + } + }, + "security": { + "description": "Settings for Code security and analysis", + "type": "object", + "properties": { + "enableVulnerabilityAlerts": { + "type": "boolean" + }, + "enableAutomatedSecurityFixes": { + "type": "boolean" + } + } + }, + "force_create": { + "description": "If true, create the repository if it does not already exist.", + "type": "boolean" + }, + "template": { + "description": "Name of a template repository to use when creating a new repository.", + "type": "string" + } + } + } + ] + }, + "labels": { + "description": "Labels: define labels for Issues and Pull Requests", + "type": "object", + "properties": { + "include": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "color": { + "description": "The hexadecimal color code for the label. If including a `#`, make sure to wrap it with quotes!", + "type": "string" + }, + "description": { + "type": "string" + }, + "oldname": { + "description": "Include the old name to rename an existing label", + "type": "string" + } + } + } + }, + "exclude": { + "description": "Ignore any labels matching these regexes (don't delete them)", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "collaborators": { + "description": "Collaborators: give specific users access to any repository.", + "type": "array", + "items": { + "description": "A collaborator entry giving a specific user access to a repository.", + "allOf": [ + { + "type": "object", + "properties": { + "permission": { + "type": "string", + "description": "The permission to grant the collaborator. **Only valid on organization-owned repositories.** We accept the following permissions to be set: `pull`, `triage`, `push`, `maintain`, `admin` and you can also specify a custom repository role name, if the owning organization has defined any.", + "default": "push" + } + } + }, + { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "exclude": { + "description": "You can exclude a list of repos for this collaborator and all repos except these repos would have this collaborator", + "type": "array", + "items": { + "type": "string" + } + }, + "include": { + "description": "You can include a list of repos for this collaborator and only those repos would have this collaborator", + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + } + }, + "teams": { + "description": "Teams", + "type": "array", + "items": { + "description": "A team entry", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the team." + }, + "description": { + "type": "string", + "description": "The description of the team." + }, + "maintainers": { + "type": "array", + "description": "List GitHub usernames for organization members who will become team maintainers.", + "items": { + "type": "string" + } + }, + "repo_names": { + "type": "array", + "description": "The full name (e.g., \"organization-name/repository-name\") of repositories to add the team to.", + "items": { + "type": "string" + } + }, + "privacy": { + "type": "string", + "description": "The level of privacy this team should have. The options are: \n**For a non-nested team:** \n * `secret` - only visible to organization owners and members of this team. \n * `closed` - visible to all members of this organization. \nDefault: `secret` \n**For a parent or child team:** \n * `closed` - visible to all members of this organization. \nDefault for child team: `closed`", + "enum": [ + "secret", + "closed" + ] + }, + "notification_setting": { + "type": "string", + "description": "The notification setting the team has chosen. The options are: \n * `notifications_enabled` - team members receive notifications when the team is @mentioned. \n * `notifications_disabled` - no one receives notifications. \nDefault: `notifications_enabled`", + "enum": [ + "notifications_enabled", + "notifications_disabled" + ] + }, + "parent_team_id": { + "type": "integer", + "description": "The ID of a team to set as the parent team." + }, + "parent_team_slug": { + "type": "string", + "description": "The slug of a team to set as the parent team. Ignored when `parent_team_id` is also provided." + } + }, + "required": [ + "name" + ] + } + }, + "milestones": { + "description": "Milestones: define milestones for Issues and Pull Requests", + "type": "array", + "items": { + "description": "A milestone entry", + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "state": { + "description": "The state of the milestone. Either `open` or `closed`", + "type": "string" + } + } + } + }, + "branches": { + "description": "Branch protection rules", + "type": "array", + "items": { + "description": "A branch protection rule entry", + "type": "object", + "properties": { + "name": { + "description": "If the name of the branch value is specified as `default`, then the app will create a branch protection rule to apply against the default branch in the repo", + "type": "string" + }, + "protection": { + "type": "object", + "properties": { + "required_status_checks": { + "type": "object", + "description": "Require status checks to pass before merging. Set to `null` to disable.", + "nullable": true, + "properties": { + "strict": { + "type": "boolean", + "description": "Require branches to be up to date before merging." + }, + "contexts": { + "type": "array", + "deprecated": true, + "description": "**Closing down notice**: The list of status checks to require in order to merge into this branch. If any of these checks have recently been set by a particular GitHub App, they will be required to come from that app in future for the branch to merge. Use `checks` instead of `contexts` for more fine-grained control.", + "items": { + "type": "string" + } + }, + "checks": { + "type": "array", + "description": "The list of status checks to require in order to merge into this branch.", + "items": { + "type": "object", + "required": [ + "context" + ], + "properties": { + "context": { + "type": "string", + "description": "The name of the required check" + }, + "app_id": { + "type": "integer", + "description": "The ID of the GitHub App that must provide this check. Omit this field to automatically select the GitHub App that has recently provided this check, or any app if it was not set by a GitHub App. Pass -1 to explicitly allow any app to set the status." + } + } + } + } + }, + "required": [ + "strict", + "contexts" + ] + }, + "enforce_admins": { + "type": "boolean", + "description": "Enforce all configured restrictions for administrators. Set to `true` to enforce required status checks for repository administrators. Set to `null` to disable.", + "nullable": true + }, + "required_pull_request_reviews": { + "type": "object", + "description": "Require at least one approving review on a pull request, before merging. Set to `null` to disable.", + "nullable": true, + "properties": { + "dismissal_restrictions": { + "type": "object", + "description": "Specify which users, teams, and apps can dismiss pull request reviews. Pass an empty `dismissal_restrictions` object to disable. User and team `dismissal_restrictions` are only available for organization-owned repositories. Omit this parameter for personal repositories.", + "properties": { + "users": { + "type": "array", + "description": "The list of user `login`s with dismissal access", + "items": { + "type": "string" + } + }, + "teams": { + "type": "array", + "description": "The list of team `slug`s with dismissal access", + "items": { + "type": "string" + } + }, + "apps": { + "type": "array", + "description": "The list of app `slug`s with dismissal access", + "items": { + "type": "string" + } + } + } + }, + "dismiss_stale_reviews": { + "type": "boolean", + "description": "Set to `true` if you want to automatically dismiss approving reviews when someone pushes a new commit." + }, + "require_code_owner_reviews": { + "type": "boolean", + "description": "Blocks merging pull requests until [code owners](https://docs.github.com/articles/about-code-owners/) review them." + }, + "required_approving_review_count": { + "type": "integer", + "description": "Specify the number of reviewers required to approve pull requests. Use a number between 1 and 6 or 0 to not require reviewers." + }, + "require_last_push_approval": { + "type": "boolean", + "description": "Whether the most recent push must be approved by someone other than the person who pushed it. Default: `false`.", + "default": false + }, + "bypass_pull_request_allowances": { + "type": "object", + "description": "Allow specific users, teams, or apps to bypass pull request requirements.", + "properties": { + "users": { + "type": "array", + "description": "The list of user `login`s allowed to bypass pull request requirements.", + "items": { + "type": "string" + } + }, + "teams": { + "type": "array", + "description": "The list of team `slug`s allowed to bypass pull request requirements.", + "items": { + "type": "string" + } + }, + "apps": { + "type": "array", + "description": "The list of app `slug`s allowed to bypass pull request requirements.", + "items": { + "type": "string" + } + } + } + } + } + }, + "restrictions": { + "type": "object", + "description": "Restrict who can push to the protected branch. User, app, and team `restrictions` are only available for organization-owned repositories. Set to `null` to disable.", + "nullable": true, + "properties": { + "users": { + "type": "array", + "description": "The list of user `login`s with push access", + "items": { + "type": "string" + } + }, + "teams": { + "type": "array", + "description": "The list of team `slug`s with push access", + "items": { + "type": "string" + } + }, + "apps": { + "type": "array", + "description": "The list of app `slug`s with push access", + "items": { + "type": "string" + } + } + }, + "required": [ + "users", + "teams" + ] + }, + "required_linear_history": { + "type": "boolean", + "description": "Enforces a linear commit Git history, which prevents anyone from pushing merge commits to a branch. Set to `true` to enforce a linear commit history. Set to `false` to disable a linear commit Git history. Your repository must allow squash merging or rebase merging before you can enable a linear commit history. Default: `false`. For more information, see \"[Requiring a linear commit history](https://docs.github.com/github/administering-a-repository/requiring-a-linear-commit-history)\" in the GitHub Help documentation." + }, + "allow_force_pushes": { + "type": "boolean", + "description": "Permits force pushes to the protected branch by anyone with write access to the repository. Set to `true` to allow force pushes. Set to `false` or `null` to block force pushes. Default: `false`. For more information, see \"[Enabling force pushes to a protected branch](https://docs.github.com/github/administering-a-repository/enabling-force-pushes-to-a-protected-branch)\" in the GitHub Help documentation.\"", + "nullable": true + }, + "allow_deletions": { + "type": "boolean", + "description": "Allows deletion of the protected branch by anyone with write access to the repository. Set to `false` to prevent deletion of the protected branch. Default: `false`. For more information, see \"[Enabling force pushes to a protected branch](https://docs.github.com/github/administering-a-repository/enabling-force-pushes-to-a-protected-branch)\" in the GitHub Help documentation." + }, + "block_creations": { + "type": "boolean", + "description": "If set to `true`, the `restrictions` branch protection settings which limits who can push will also block pushes which create new branches, unless the push is initiated by a user, team, or app which has the ability to push. Set to `true` to restrict new branch creation. Default: `false`." + }, + "required_conversation_resolution": { + "type": "boolean", + "description": "Requires all conversations on code to be resolved before a pull request can be merged into a branch that matches this rule. Set to `false` to disable. Default: `false`." + }, + "lock_branch": { + "type": "boolean", + "description": "Whether to set the branch as read-only. If this is true, users will not be able to push to the branch. Default: `false`.", + "default": false + }, + "allow_fork_syncing": { + "type": "boolean", + "description": "Whether users can pull changes from upstream when the branch is locked. Set to `true` to allow fork syncing. Set to `false` to prevent fork syncing. Default: `false`.", + "default": false + } + }, + "required": [ + "required_status_checks", + "enforce_admins", + "required_pull_request_reviews", + "restrictions" + ] + } + } + } + }, + "autolinks": { + "description": "Autolinks", + "type": "array", + "items": { + "description": "An autolink reference entry", + "type": "object", + "properties": { + "key_prefix": { + "type": "string", + "description": "This prefix appended by certain characters will generate a link any time it is found in an issue, pull request, or commit." + }, + "url_template": { + "type": "string", + "description": "The URL must contain `` for the reference number. `` matches different characters depending on the value of `is_alphanumeric`." + }, + "is_alphanumeric": { + "type": "boolean", + "default": true, + "description": "Whether this autolink reference matches alphanumeric characters. If true, the `` parameter of the `url_template` matches alphanumeric characters `A-Z` (case insensitive), `0-9`, and `-`. If false, this autolink reference only matches numeric characters." + } + }, + "required": [ + "key_prefix", + "url_template" + ] + } + }, + "validator": { + "description": "Repository name validation", + "type": "object", + "properties": { + "pattern": { + "type": "string" + } + } + }, + "environments": { + "description": "Deployment environments", + "type": "array", + "items": { + "description": "A deployment environment configuration entry", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "The name of the deployment environment", + "type": "string" + }, + "wait_timer": { + "description": "The amount of time to delay a job after the job is initially triggered (in minutes)", + "type": "integer" + }, + "reviewers": { + "description": "The people or teams that may review jobs that reference the environment", + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "description": "The type of reviewer (`User` or `Team`)", + "type": "string" + }, + "id": { + "description": "The id of the user or team who can review the deployment", + "type": "integer" + } + } + } + }, + "deployment_branch_policy": { + "description": "The type of deployment branch policy for this environment", + "type": "object", + "properties": { + "protected_branches": { + "description": "Whether only protected branches can be deployed to this environment", + "type": "boolean" + }, + "custom_branch_policies": { + "description": "Whether only branches that match the specified name patterns can deploy to this environment", + "type": "boolean" + } + } + }, + "prevent_self_review": { + "description": "Whether or not a user who created the job is prevented from approving their own job", + "type": "boolean" + } + } + } + }, + "custom_properties": { + "description": "Custom properties", + "type": "array", + "items": { + "description": "A custom property entry", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + }, + "variables": { + "description": "Repository or org-level Actions variables", + "type": "array", + "items": { + "description": "An Actions variable entry", + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "description": "The name of the variable", + "type": "string" + }, + "value": { + "description": "The value of the variable", + "type": "string" + }, + "visibility": { + "description": "The visibility of the variable. Can be `all`, `private`, or `selected`", + "type": "string", + "enum": [ + "all", + "private", + "selected" + ] + } + } + } + } + }, + "$defs": { + "RepositorySettings": { + "description": "Repository settings. Use force_create to create the repository if it does not exist, and template to specify a template repository to use when creating it.", + "allOf": [ + { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the repository." + }, + "description": { + "type": "string", + "description": "A short description of the repository." + }, + "homepage": { + "type": "string", + "description": "A URL with more information about the repository." + }, + "private": { + "type": "boolean", + "description": "Either `true` to make the repository private or `false` to make it public. Default: `false`. \n**Note**: You will get a `422` error if the organization restricts [changing repository visibility](https://docs.github.com/articles/repository-permission-levels-for-an-organization#changing-the-visibility-of-repositories) to organization owners and a non-owner tries to change the value of private.", + "default": false + }, + "visibility": { + "type": "string", + "description": "The visibility of the repository.", + "enum": [ + "public", + "private" + ] + }, + "security_and_analysis": { + "type": "object", + "description": "Specify which security and analysis features to enable or disable for the repository.\n\nTo use this parameter, you must have admin permissions for the repository or be an owner or security manager for the organization that owns the repository. For more information, see \"[Managing security managers in your organization](https://docs.github.com/organizations/managing-peoples-access-to-your-organization-with-roles/managing-security-managers-in-your-organization).\"\n\nFor example, to enable GitHub Advanced Security, use this data in the body of the `PATCH` request:\n`{ \"security_and_analysis\": {\"advanced_security\": { \"status\": \"enabled\" } } }`.\n\nYou can check which security and analysis features are currently enabled by using a `GET /repos/{owner}/{repo}` request.", + "nullable": true, + "properties": { + "advanced_security": { + "type": "object", + "description": "Use the `status` property to enable or disable GitHub Advanced Security for this repository.\nFor more information, see \"[About GitHub Advanced\nSecurity](/github/getting-started-with-github/learning-about-github/about-github-advanced-security).\"\n\nFor standalone Code Scanning or Secret Protection products, this parameter cannot be used.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "code_security": { + "type": "object", + "description": "Use the `status` property to enable or disable GitHub Code Security for this repository.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning for this repository. For more information, see \"[About secret scanning](/code-security/secret-security/about-secret-scanning).\"", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_push_protection": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning push protection for this repository. For more information, see \"[Protecting pushes with secret scanning](/code-security/secret-scanning/protecting-pushes-with-secret-scanning).\"", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_ai_detection": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning AI detection for this repository. For more information, see \"[Responsible detection of generic secrets with AI](https://docs.github.com/code-security/secret-scanning/using-advanced-secret-scanning-and-push-protection-features/generic-secret-detection/responsible-ai-generic-secrets).\"", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_non_provider_patterns": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning non-provider patterns for this repository. For more information, see \"[Supported secret scanning patterns](/code-security/secret-scanning/introduction/supported-secret-scanning-patterns#supported-secrets).\"", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_delegated_alert_dismissal": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning delegated alert dismissal for this repository.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_delegated_bypass": { + "type": "object", + "description": "Use the `status` property to enable or disable secret scanning delegated bypass for this repository.", + "properties": { + "status": { + "type": "string", + "description": "Can be `enabled` or `disabled`." + } + } + }, + "secret_scanning_delegated_bypass_options": { + "type": "object", + "description": "Feature options for secret scanning delegated bypass.\nThis object is only honored when `security_and_analysis.secret_scanning_delegated_bypass.status` is set to `enabled`.\nYou can send this object in the same request as `secret_scanning_delegated_bypass`, or update just the options in a separate request.", + "properties": { + "reviewers": { + "type": "array", + "description": "The bypass reviewers for secret scanning delegated bypass.\nIf you omit this field, the existing set of reviewers is unchanged.", + "items": { + "type": "object", + "required": [ + "reviewer_id", + "reviewer_type" + ], + "properties": { + "reviewer_id": { + "type": "integer", + "description": "The ID of the team or role selected as a bypass reviewer" + }, + "reviewer_type": { + "type": "string", + "description": "The type of the bypass reviewer", + "enum": [ + "TEAM", + "ROLE" + ] + }, + "mode": { + "type": "string", + "description": "The bypass mode for the reviewer", + "enum": [ + "ALWAYS", + "EXEMPT" + ], + "default": "ALWAYS" + } + } + } + } + } + } + } + }, + "has_issues": { + "type": "boolean", + "description": "Either `true` to enable issues for this repository or `false` to disable them.", + "default": true + }, + "has_projects": { + "type": "boolean", + "description": "Either `true` to enable projects for this repository or `false` to disable them. **Note:** If you're creating a repository in an organization that has disabled repository projects, the default is `false`, and if you pass `true`, the API returns an error.", + "default": true + }, + "has_wiki": { + "type": "boolean", + "description": "Either `true` to enable the wiki for this repository or `false` to disable it.", + "default": true + }, + "has_pull_requests": { + "type": "boolean", + "description": "Either `true` to allow pull requests for this repository or `false` to prevent pull requests.", + "default": true + }, + "pull_request_creation_policy": { + "type": "string", + "description": "The policy that controls who can create pull requests for this repository: `all` or `collaborators_only`.", + "enum": [ + "all", + "collaborators_only" + ] + }, + "is_template": { + "type": "boolean", + "description": "Either `true` to make this repo available as a template repository or `false` to prevent it.", + "default": false + }, + "default_branch": { + "type": "string", + "description": "Updates the default branch for this repository." + }, + "allow_squash_merge": { + "type": "boolean", + "description": "Either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging.", + "default": true + }, + "allow_merge_commit": { + "type": "boolean", + "description": "Either `true` to allow merging pull requests with a merge commit, or `false` to prevent merging pull requests with merge commits.", + "default": true + }, + "allow_rebase_merge": { + "type": "boolean", + "description": "Either `true` to allow rebase-merging pull requests, or `false` to prevent rebase-merging.", + "default": true + }, + "allow_auto_merge": { + "type": "boolean", + "description": "Either `true` to allow auto-merge on pull requests, or `false` to disallow auto-merge.", + "default": false + }, + "delete_branch_on_merge": { + "type": "boolean", + "description": "Either `true` to allow automatically deleting head branches when pull requests are merged, or `false` to prevent automatic deletion.", + "default": false + }, + "allow_update_branch": { + "type": "boolean", + "description": "Either `true` to always allow a pull request head branch that is behind its base branch to be updated even if it is not required to be up to date before merging, or false otherwise.", + "default": false + }, + "use_squash_pr_title_as_default": { + "type": "boolean", + "description": "Either `true` to allow squash-merge commits to use pull request title, or `false` to use commit message. **This property is closing down. Please use `squash_merge_commit_title` instead.", + "default": false, + "deprecated": true + }, + "squash_merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "COMMIT_OR_PR_TITLE" + ], + "description": "Required when using `squash_merge_commit_message`.\n\nThe default value for a squash merge commit title:\n\n- `PR_TITLE` - default to the pull request's title.\n- `COMMIT_OR_PR_TITLE` - default to the commit's title (if only one commit) or the pull request's title (when more than one commit)." + }, + "squash_merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "COMMIT_MESSAGES", + "BLANK" + ], + "description": "The default value for a squash merge commit message:\n\n- `PR_BODY` - default to the pull request's body.\n- `COMMIT_MESSAGES` - default to the branch's commit messages.\n- `BLANK` - default to a blank commit message." + }, + "merge_commit_title": { + "type": "string", + "enum": [ + "PR_TITLE", + "MERGE_MESSAGE" + ], + "description": "Required when using `merge_commit_message`.\n\nThe default value for a merge commit title.\n\n- `PR_TITLE` - default to the pull request's title.\n- `MERGE_MESSAGE` - default to the classic title for a merge message (e.g., Merge pull request #123 from branch-name)." + }, + "merge_commit_message": { + "type": "string", + "enum": [ + "PR_BODY", + "PR_TITLE", + "BLANK" + ], + "description": "The default value for a merge commit message.\n\n- `PR_TITLE` - default to the pull request's title.\n- `PR_BODY` - default to the pull request's body.\n- `BLANK` - default to a blank commit message." + }, + "archived": { + "type": "boolean", + "description": "Whether to archive this repository. `false` will unarchive a previously archived repository.", + "default": false + }, + "allow_forking": { + "type": "boolean", + "description": "Either `true` to allow private forks, or `false` to prevent private forks.", + "default": false + }, + "web_commit_signoff_required": { + "type": "boolean", + "description": "Either `true` to require contributors to sign off on web-based commits, or `false` to not require contributors to sign off on web-based commits.", + "default": false + } + } + }, + { + "type": "object", + "properties": { + "auto_init": { + "description": "Create an initial commit with empty README. Keep this set to true in most cases since many of the policies below cannot be implemented on bare repos", + "type": "boolean" + }, + "gitignore_template": { + "description": "Desired language or platform [.gitignore template](https://github.com/github/gitignore) to apply. Use the name of the template without the extension. For example, 'Haskell'.", + "type": "string" + }, + "license_template": { + "description": "Choose an [open source license template](https://choosealicense.com/) that best suits your needs, and then use the [license keyword](https://help.github.com/articles/licensing-a-repository/#searching-github-by-license-type) as the `license_template` string. For example, 'mit' or 'mpl-2.0'.", + "type": "string" + }, + "topics": { + "description": "A list of topics to set on the repository", + "type": "array", + "items": { + "type": "string" + } + }, + "security": { + "description": "Settings for Code security and analysis", + "type": "object", + "properties": { + "enableVulnerabilityAlerts": { + "type": "boolean" + }, + "enableAutomatedSecurityFixes": { + "type": "boolean" + } + } + }, + "force_create": { + "description": "If true, create the repository if it does not already exist.", + "type": "boolean" + }, + "template": { + "description": "Name of a template repository to use when creating a new repository.", + "type": "string" + } + } + } + ] + }, + "LabelSettings": { + "description": "Labels: define labels for Issues and Pull Requests", + "type": "object", + "properties": { + "include": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "color": { + "description": "The hexadecimal color code for the label. If including a `#`, make sure to wrap it with quotes!", + "type": "string" + }, + "description": { + "type": "string" + }, + "oldname": { + "description": "Include the old name to rename an existing label", + "type": "string" + } + } + } + }, + "exclude": { + "description": "Ignore any labels matching these regexes (don't delete them)", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "CollaboratorSettings": { + "description": "A collaborator entry giving a specific user access to a repository.", + "allOf": [ + { + "type": "object", + "properties": { + "permission": { + "type": "string", + "description": "The permission to grant the collaborator. **Only valid on organization-owned repositories.** We accept the following permissions to be set: `pull`, `triage`, `push`, `maintain`, `admin` and you can also specify a custom repository role name, if the owning organization has defined any.", + "default": "push" + } + } + }, + { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "exclude": { + "description": "You can exclude a list of repos for this collaborator and all repos except these repos would have this collaborator", + "type": "array", + "items": { + "type": "string" + } + }, + "include": { + "description": "You can include a list of repos for this collaborator and only those repos would have this collaborator", + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + }, + "TeamSettings": { + "description": "A team entry", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the team." + }, + "description": { + "type": "string", + "description": "The description of the team." + }, + "maintainers": { + "type": "array", + "description": "List GitHub usernames for organization members who will become team maintainers.", + "items": { + "type": "string" + } + }, + "repo_names": { + "type": "array", + "description": "The full name (e.g., \"organization-name/repository-name\") of repositories to add the team to.", + "items": { + "type": "string" + } + }, + "privacy": { + "type": "string", + "description": "The level of privacy this team should have. The options are: \n**For a non-nested team:** \n * `secret` - only visible to organization owners and members of this team. \n * `closed` - visible to all members of this organization. \nDefault: `secret` \n**For a parent or child team:** \n * `closed` - visible to all members of this organization. \nDefault for child team: `closed`", + "enum": [ + "secret", + "closed" + ] + }, + "notification_setting": { + "type": "string", + "description": "The notification setting the team has chosen. The options are: \n * `notifications_enabled` - team members receive notifications when the team is @mentioned. \n * `notifications_disabled` - no one receives notifications. \nDefault: `notifications_enabled`", + "enum": [ + "notifications_enabled", + "notifications_disabled" + ] + }, + "parent_team_id": { + "type": "integer", + "description": "The ID of a team to set as the parent team." + }, + "parent_team_slug": { + "type": "string", + "description": "The slug of a team to set as the parent team. Ignored when `parent_team_id` is also provided." + } + }, + "required": [ + "name" + ] + }, + "MilestoneSettings": { + "description": "A milestone entry", + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "state": { + "description": "The state of the milestone. Either `open` or `closed`", + "type": "string" + } + } + }, + "BranchSettings": { + "description": "A branch protection rule entry", + "type": "object", + "properties": { + "name": { + "description": "If the name of the branch value is specified as `default`, then the app will create a branch protection rule to apply against the default branch in the repo", + "type": "string" + }, + "protection": { + "type": "object", + "properties": { + "required_status_checks": { + "type": "object", + "description": "Require status checks to pass before merging. Set to `null` to disable.", + "nullable": true, + "properties": { + "strict": { + "type": "boolean", + "description": "Require branches to be up to date before merging." + }, + "contexts": { + "type": "array", + "deprecated": true, + "description": "**Closing down notice**: The list of status checks to require in order to merge into this branch. If any of these checks have recently been set by a particular GitHub App, they will be required to come from that app in future for the branch to merge. Use `checks` instead of `contexts` for more fine-grained control.", + "items": { + "type": "string" + } + }, + "checks": { + "type": "array", + "description": "The list of status checks to require in order to merge into this branch.", + "items": { + "type": "object", + "required": [ + "context" + ], + "properties": { + "context": { + "type": "string", + "description": "The name of the required check" + }, + "app_id": { + "type": "integer", + "description": "The ID of the GitHub App that must provide this check. Omit this field to automatically select the GitHub App that has recently provided this check, or any app if it was not set by a GitHub App. Pass -1 to explicitly allow any app to set the status." + } + } + } + } + }, + "required": [ + "strict", + "contexts" + ] + }, + "enforce_admins": { + "type": "boolean", + "description": "Enforce all configured restrictions for administrators. Set to `true` to enforce required status checks for repository administrators. Set to `null` to disable.", + "nullable": true + }, + "required_pull_request_reviews": { + "type": "object", + "description": "Require at least one approving review on a pull request, before merging. Set to `null` to disable.", + "nullable": true, + "properties": { + "dismissal_restrictions": { + "type": "object", + "description": "Specify which users, teams, and apps can dismiss pull request reviews. Pass an empty `dismissal_restrictions` object to disable. User and team `dismissal_restrictions` are only available for organization-owned repositories. Omit this parameter for personal repositories.", + "properties": { + "users": { + "type": "array", + "description": "The list of user `login`s with dismissal access", + "items": { + "type": "string" + } + }, + "teams": { + "type": "array", + "description": "The list of team `slug`s with dismissal access", + "items": { + "type": "string" + } + }, + "apps": { + "type": "array", + "description": "The list of app `slug`s with dismissal access", + "items": { + "type": "string" + } + } + } + }, + "dismiss_stale_reviews": { + "type": "boolean", + "description": "Set to `true` if you want to automatically dismiss approving reviews when someone pushes a new commit." + }, + "require_code_owner_reviews": { + "type": "boolean", + "description": "Blocks merging pull requests until [code owners](https://docs.github.com/articles/about-code-owners/) review them." + }, + "required_approving_review_count": { + "type": "integer", + "description": "Specify the number of reviewers required to approve pull requests. Use a number between 1 and 6 or 0 to not require reviewers." + }, + "require_last_push_approval": { + "type": "boolean", + "description": "Whether the most recent push must be approved by someone other than the person who pushed it. Default: `false`.", + "default": false + }, + "bypass_pull_request_allowances": { + "type": "object", + "description": "Allow specific users, teams, or apps to bypass pull request requirements.", + "properties": { + "users": { + "type": "array", + "description": "The list of user `login`s allowed to bypass pull request requirements.", + "items": { + "type": "string" + } + }, + "teams": { + "type": "array", + "description": "The list of team `slug`s allowed to bypass pull request requirements.", + "items": { + "type": "string" + } + }, + "apps": { + "type": "array", + "description": "The list of app `slug`s allowed to bypass pull request requirements.", + "items": { + "type": "string" + } + } + } + } + } + }, + "restrictions": { + "type": "object", + "description": "Restrict who can push to the protected branch. User, app, and team `restrictions` are only available for organization-owned repositories. Set to `null` to disable.", + "nullable": true, + "properties": { + "users": { + "type": "array", + "description": "The list of user `login`s with push access", + "items": { + "type": "string" + } + }, + "teams": { + "type": "array", + "description": "The list of team `slug`s with push access", + "items": { + "type": "string" + } + }, + "apps": { + "type": "array", + "description": "The list of app `slug`s with push access", + "items": { + "type": "string" + } + } + }, + "required": [ + "users", + "teams" + ] + }, + "required_linear_history": { + "type": "boolean", + "description": "Enforces a linear commit Git history, which prevents anyone from pushing merge commits to a branch. Set to `true` to enforce a linear commit history. Set to `false` to disable a linear commit Git history. Your repository must allow squash merging or rebase merging before you can enable a linear commit history. Default: `false`. For more information, see \"[Requiring a linear commit history](https://docs.github.com/github/administering-a-repository/requiring-a-linear-commit-history)\" in the GitHub Help documentation." + }, + "allow_force_pushes": { + "type": "boolean", + "description": "Permits force pushes to the protected branch by anyone with write access to the repository. Set to `true` to allow force pushes. Set to `false` or `null` to block force pushes. Default: `false`. For more information, see \"[Enabling force pushes to a protected branch](https://docs.github.com/github/administering-a-repository/enabling-force-pushes-to-a-protected-branch)\" in the GitHub Help documentation.\"", + "nullable": true + }, + "allow_deletions": { + "type": "boolean", + "description": "Allows deletion of the protected branch by anyone with write access to the repository. Set to `false` to prevent deletion of the protected branch. Default: `false`. For more information, see \"[Enabling force pushes to a protected branch](https://docs.github.com/github/administering-a-repository/enabling-force-pushes-to-a-protected-branch)\" in the GitHub Help documentation." + }, + "block_creations": { + "type": "boolean", + "description": "If set to `true`, the `restrictions` branch protection settings which limits who can push will also block pushes which create new branches, unless the push is initiated by a user, team, or app which has the ability to push. Set to `true` to restrict new branch creation. Default: `false`." + }, + "required_conversation_resolution": { + "type": "boolean", + "description": "Requires all conversations on code to be resolved before a pull request can be merged into a branch that matches this rule. Set to `false` to disable. Default: `false`." + }, + "lock_branch": { + "type": "boolean", + "description": "Whether to set the branch as read-only. If this is true, users will not be able to push to the branch. Default: `false`.", + "default": false + }, + "allow_fork_syncing": { + "type": "boolean", + "description": "Whether users can pull changes from upstream when the branch is locked. Set to `true` to allow fork syncing. Set to `false` to prevent fork syncing. Default: `false`.", + "default": false + } + }, + "required": [ + "required_status_checks", + "enforce_admins", + "required_pull_request_reviews", + "restrictions" + ] + } + } + }, + "AutolinkSettings": { + "description": "An autolink reference entry", + "type": "object", + "properties": { + "key_prefix": { + "type": "string", + "description": "This prefix appended by certain characters will generate a link any time it is found in an issue, pull request, or commit." + }, + "url_template": { + "type": "string", + "description": "The URL must contain `` for the reference number. `` matches different characters depending on the value of `is_alphanumeric`." + }, + "is_alphanumeric": { + "type": "boolean", + "default": true, + "description": "Whether this autolink reference matches alphanumeric characters. If true, the `` parameter of the `url_template` matches alphanumeric characters `A-Z` (case insensitive), `0-9`, and `-`. If false, this autolink reference only matches numeric characters." + } + }, + "required": [ + "key_prefix", + "url_template" + ] + }, + "ValidatorSettings": { + "description": "Repository name validation", + "type": "object", + "properties": { + "pattern": { + "type": "string" + } + } + }, + "RulesetSettings": { + "description": "A ruleset entry", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the ruleset." + }, + "target": { + "type": "string", + "description": "The target of the ruleset", + "enum": [ + "branch", + "tag", + "push", + "repository" + ], + "default": "branch" + }, + "enforcement": { + "type": "string", + "description": "The enforcement level of the ruleset. `evaluate` allows admins to test rules before enforcing them. Admins can view insights on the Rule Insights page (`evaluate` is only available with GitHub Enterprise).", + "enum": [ + "disabled", + "active", + "evaluate" + ] + }, + "bypass_actors": { + "type": "array", + "description": "The actors that can bypass the rules in this ruleset", + "items": { + "title": "Repository Ruleset Bypass Actor", + "type": "object", + "description": "An actor that can bypass rules in a ruleset", + "required": [ + "actor_type" + ], + "properties": { + "actor_id": { + "type": "integer", + "nullable": true, + "description": "The ID of the actor that can bypass a ruleset. Required for `Integration`, `RepositoryRole`, `Team`, and `User` actor types. If `actor_type` is `OrganizationAdmin`, `actor_id` is ignored. If `actor_type` is `DeployKey`, this should be null. `OrganizationAdmin` is not applicable for personal repositories." + }, + "actor_type": { + "type": "string", + "enum": [ + "Integration", + "OrganizationAdmin", + "RepositoryRole", + "Team", + "DeployKey", + "User" + ], + "description": "The type of actor that can bypass a ruleset." + }, + "bypass_mode": { + "type": "string", + "description": "When the specified actor can bypass the ruleset. `pull_request` means that an actor can only bypass rules on pull requests. `pull_request` is not applicable for the `DeployKey` actor type. Also, `pull_request` is only applicable to branch rulesets. When `bypass_mode` is `exempt`, rules will not be run for that actor and a bypass audit entry will not be created.", + "enum": [ + "always", + "pull_request", + "exempt" + ], + "default": "always" + }, + "name": { + "type": "string", + "description": "Human-friendly alternative to `actor_id`. The team slug, username, GitHub App slug, or repository role name (resolved using `actor_type`). Cannot be combined with `actor_id`." + } + } + } + }, + "conditions": { + "title": "Organization ruleset conditions", + "type": "object", + "description": "Conditions for an organization ruleset.\nThe branch and tag rulesets conditions object should contain both `repository_name` and `ref_name` properties, or both `repository_id` and `ref_name` properties, or both `repository_property` and `ref_name` properties.\nThe push rulesets conditions object does not require the `ref_name` property.\nFor repository policy rulesets, the conditions object should only contain the `repository_name`, the `repository_id`, or the `repository_property`.", + "oneOf": [ + { + "type": "object", + "title": "repository_name_and_ref_name", + "description": "Conditions to target repositories by name and refs by name", + "allOf": [ + { + "title": "Repository ruleset conditions for ref names", + "type": "object", + "description": "Parameters for a repository ruleset ref name condition", + "properties": { + "ref_name": { + "type": "object", + "properties": { + "include": { + "type": "array", + "description": "Array of ref names or patterns to include. One of these patterns must match for the condition to pass. Also accepts `~DEFAULT_BRANCH` to include the default branch or `~ALL` to include all branches.", + "items": { + "type": "string" + } + }, + "exclude": { + "type": "array", + "description": "Array of ref names or patterns to exclude. The condition will not pass if any of these patterns match.", + "items": { + "type": "string" + } + } + } + } + } + }, + { + "title": "Repository ruleset conditions for repository names", + "type": "object", + "description": "Parameters for a repository name condition", + "properties": { + "repository_name": { + "type": "object", + "properties": { + "include": { + "type": "array", + "description": "Array of repository names or patterns to include. One of these patterns must match for the condition to pass. Also accepts `~ALL` to include all repositories.", + "items": { + "type": "string" + } + }, + "exclude": { + "type": "array", + "description": "Array of repository names or patterns to exclude. The condition will not pass if any of these patterns match.", + "items": { + "type": "string" + } + }, + "protected": { + "type": "boolean", + "description": "Whether renaming of target repositories is prevented." + } + } + } + }, + "required": [ + "repository_name" + ] + } + ] + }, + { + "type": "object", + "title": "repository_id_and_ref_name", + "description": "Conditions to target repositories by id and refs by name", + "allOf": [ + { + "title": "Repository ruleset conditions for ref names", + "type": "object", + "description": "Parameters for a repository ruleset ref name condition", + "properties": { + "ref_name": { + "type": "object", + "properties": { + "include": { + "type": "array", + "description": "Array of ref names or patterns to include. One of these patterns must match for the condition to pass. Also accepts `~DEFAULT_BRANCH` to include the default branch or `~ALL` to include all branches.", + "items": { + "type": "string" + } + }, + "exclude": { + "type": "array", + "description": "Array of ref names or patterns to exclude. The condition will not pass if any of these patterns match.", + "items": { + "type": "string" + } + } + } + } + } + }, + { + "title": "Repository ruleset conditions for repository IDs", + "type": "object", + "description": "Parameters for a repository ID condition", + "properties": { + "repository_id": { + "type": "object", + "properties": { + "repository_ids": { + "type": "array", + "description": "The repository IDs that the ruleset applies to. One of these IDs must match for the condition to pass.", + "items": { + "type": "integer" + } + } + } + } + }, + "required": [ + "repository_id" + ] + } + ] + }, + { + "type": "object", + "title": "repository_property_and_ref_name", + "description": "Conditions to target repositories by property and refs by name", + "allOf": [ + { + "title": "Repository ruleset conditions for ref names", + "type": "object", + "description": "Parameters for a repository ruleset ref name condition", + "properties": { + "ref_name": { + "type": "object", + "properties": { + "include": { + "type": "array", + "description": "Array of ref names or patterns to include. One of these patterns must match for the condition to pass. Also accepts `~DEFAULT_BRANCH` to include the default branch or `~ALL` to include all branches.", + "items": { + "type": "string" + } + }, + "exclude": { + "type": "array", + "description": "Array of ref names or patterns to exclude. The condition will not pass if any of these patterns match.", + "items": { + "type": "string" + } + } + } + } + } + }, + { + "title": "Repository ruleset conditions for repository properties", + "type": "object", + "description": "Parameters for a repository property condition", + "properties": { + "repository_property": { + "type": "object", + "properties": { + "include": { + "type": "array", + "description": "The repository properties and values to include. All of these properties must match for the condition to pass.", + "items": { + "title": "Repository ruleset property targeting definition", + "type": "object", + "description": "Parameters for a targeting a repository property", + "properties": { + "name": { + "type": "string", + "description": "The name of the repository property to target" + }, + "property_values": { + "type": "array", + "description": "The values to match for the repository property", + "items": { + "type": "string" + } + }, + "source": { + "type": "string", + "description": "The source of the repository property. Defaults to 'custom' if not specified.", + "enum": [ + "custom", + "system" + ] + } + }, + "required": [ + "name", + "property_values" + ] + } + }, + "exclude": { + "type": "array", + "description": "The repository properties and values to exclude. The condition will not pass if any of these properties match.", + "items": { + "title": "Repository ruleset property targeting definition", + "type": "object", + "description": "Parameters for a targeting a repository property", + "properties": { + "name": { + "type": "string", + "description": "The name of the repository property to target" + }, + "property_values": { + "type": "array", + "description": "The values to match for the repository property", + "items": { + "type": "string" + } + }, + "source": { + "type": "string", + "description": "The source of the repository property. Defaults to 'custom' if not specified.", + "enum": [ + "custom", + "system" + ] + } + }, + "required": [ + "name", + "property_values" + ] + } + } + } + } + }, + "required": [ + "repository_property" + ] + } + ] + } + ] + }, + "rules": { + "type": "array", + "description": "An array of rules within the ruleset.", + "items": { + "title": "Repository Rule", + "type": "object", + "description": "A repository rule.", + "oneOf": [ + { + "title": "creation", + "description": "Only allow users with bypass permission to create matching refs.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "creation" + ] + } + } + }, + { + "title": "update", + "description": "Only allow users with bypass permission to update matching refs.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "update" + ] + }, + "parameters": { + "type": "object", + "properties": { + "update_allows_fetch_and_merge": { + "type": "boolean", + "description": "Branch can pull changes from its upstream repository" + } + }, + "required": [ + "update_allows_fetch_and_merge" + ] + } + } + }, + { + "title": "deletion", + "description": "Only allow users with bypass permissions to delete matching refs.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "deletion" + ] + } + } + }, + { + "title": "required_linear_history", + "description": "Prevent merge commits from being pushed to matching refs.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "required_linear_history" + ] + } + } + }, + { + "title": "required_deployments", + "description": "Choose which environments must be successfully deployed to before refs can be pushed into a ref that matches this rule.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "required_deployments" + ] + }, + "parameters": { + "type": "object", + "properties": { + "required_deployment_environments": { + "type": "array", + "description": "The environments that must be successfully deployed to before branches can be merged.", + "items": { + "type": "string" + } + } + }, + "required": [ + "required_deployment_environments" + ] + } + } + }, + { + "title": "required_signatures", + "description": "Commits pushed to matching refs must have verified signatures.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "required_signatures" + ] + } + } + }, + { + "title": "pull_request", + "description": "Require all commits be made to a non-target branch and submitted via a pull request before they can be merged.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "pull_request" + ] + }, + "parameters": { + "type": "object", + "properties": { + "allowed_merge_methods": { + "type": "array", + "description": "Array of allowed merge methods. Allowed values include `merge`, `squash`, and `rebase`. At least one option must be enabled.", + "items": { + "type": "string", + "enum": [ + "merge", + "squash", + "rebase" + ] + } + }, + "dismiss_stale_reviews_on_push": { + "type": "boolean", + "description": "New, reviewable commits pushed will dismiss previous pull request review approvals." + }, + "dismissal_restriction": { + "title": "DismissalRestriction", + "description": "Specify people, teams, or apps allowed to dismiss pull request reviews.", + "type": "object", + "properties": { + "allowed_actors": { + "type": "array", + "description": "Specify people, teams, or apps allowed to dismiss pull request reviews.", + "items": { + "title": "Actor", + "description": "An actor allowed to dismiss pull request reviews", + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "ID of the actor that can dismiss reviews." + }, + "type": { + "type": "string", + "description": "The type of the actor", + "enum": [ + "User", + "Team", + "IntegrationInstallation", + "RepositoryRole" + ] + } + }, + "required": [ + "id", + "type" + ] + } + }, + "enabled": { + "type": "boolean", + "description": "Whether to restrict review dismissal to specific actors." + } + }, + "required": [ + "enabled" + ] + }, + "require_code_owner_review": { + "type": "boolean", + "description": "Require an approving review in pull requests that modify files that have a designated code owner." + }, + "require_last_push_approval": { + "type": "boolean", + "description": "Whether the most recent reviewable push must be approved by someone other than the person who pushed it." + }, + "required_approving_review_count": { + "type": "integer", + "description": "The number of approving reviews that are required before a pull request can be merged.", + "minimum": 0, + "maximum": 10 + }, + "required_review_thread_resolution": { + "type": "boolean", + "description": "All conversations on code must be resolved before a pull request can be merged." + }, + "required_reviewers": { + "type": "array", + "description": "> [!NOTE]\n> `required_reviewers` is in beta and subject to change.\n\nA collection of reviewers and associated file patterns. Each reviewer has a list of file patterns which determine the files that reviewer is required to review.", + "items": { + "title": "RequiredReviewerConfiguration", + "description": "A reviewing team, and file patterns describing which files they must approve changes to.", + "type": "object", + "properties": { + "file_patterns": { + "type": "array", + "description": "Array of file patterns. Pull requests which change matching files must be approved by the specified team. File patterns use fnmatch syntax.", + "items": { + "type": "string" + } + }, + "minimum_approvals": { + "type": "integer", + "description": "Minimum number of approvals required from the specified team. If set to zero, the team will be added to the pull request but approval is optional." + }, + "reviewer": { + "title": "Reviewer", + "description": "A required reviewing team", + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "ID of the reviewer which must review changes to matching files." + }, + "type": { + "type": "string", + "description": "The type of the reviewer", + "enum": [ + "Team" + ] + }, + "slug": { + "type": "string", + "description": "Human-friendly alternative to `id`. The slug of the team that must review changes to matching files. Cannot be combined with `id`." + } + }, + "required": [ + "type" + ] + } + }, + "required": [ + "file_patterns", + "minimum_approvals", + "reviewer" + ] + } + } + }, + "required": [ + "dismiss_stale_reviews_on_push", + "require_code_owner_review", + "require_last_push_approval", + "required_approving_review_count", + "required_review_thread_resolution" + ] + } + } + }, + { + "title": "required_status_checks", + "description": "Choose which status checks must pass before the ref is updated. When enabled, commits must first be pushed to another ref where the checks pass.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "required_status_checks" + ] + }, + "parameters": { + "type": "object", + "properties": { + "do_not_enforce_on_create": { + "type": "boolean", + "description": "Allow repositories and branches to be created if a check would otherwise prohibit it." + }, + "required_status_checks": { + "type": "array", + "description": "Status checks that are required.", + "items": { + "title": "StatusCheckConfiguration", + "description": "Required status check", + "type": "object", + "properties": { + "context": { + "type": "string", + "description": "The status check context name that must be present on the commit." + }, + "integration_id": { + "type": "integer", + "description": "The optional integration ID that this status check must originate from." + } + }, + "required": [ + "context" + ] + } + }, + "strict_required_status_checks_policy": { + "type": "boolean", + "description": "Whether pull requests targeting a matching branch must be tested with the latest code. This setting will not take effect unless at least one status check is enabled." + } + }, + "required": [ + "required_status_checks", + "strict_required_status_checks_policy" + ] + } + } + }, + { + "title": "non_fast_forward", + "description": "Prevent users with push access from force pushing to refs.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "non_fast_forward" + ] + } + } + }, + { + "title": "commit_message_pattern", + "description": "Parameters to be used for the commit_message_pattern rule", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "commit_message_pattern" + ] + }, + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "How this rule appears when configuring it." + }, + "negate": { + "type": "boolean", + "description": "If true, the rule will fail if the pattern matches." + }, + "operator": { + "type": "string", + "description": "The operator to use for matching.", + "enum": [ + "starts_with", + "ends_with", + "contains", + "regex" + ] + }, + "pattern": { + "type": "string", + "description": "The pattern to match with." + } + }, + "required": [ + "operator", + "pattern" + ] + } + } + }, + { + "title": "commit_author_email_pattern", + "description": "Parameters to be used for the commit_author_email_pattern rule", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "commit_author_email_pattern" + ] + }, + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "How this rule appears when configuring it." + }, + "negate": { + "type": "boolean", + "description": "If true, the rule will fail if the pattern matches." + }, + "operator": { + "type": "string", + "description": "The operator to use for matching.", + "enum": [ + "starts_with", + "ends_with", + "contains", + "regex" + ] + }, + "pattern": { + "type": "string", + "description": "The pattern to match with." + } + }, + "required": [ + "operator", + "pattern" + ] + } + } + }, + { + "title": "committer_email_pattern", + "description": "Parameters to be used for the committer_email_pattern rule", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "committer_email_pattern" + ] + }, + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "How this rule appears when configuring it." + }, + "negate": { + "type": "boolean", + "description": "If true, the rule will fail if the pattern matches." + }, + "operator": { + "type": "string", + "description": "The operator to use for matching.", + "enum": [ + "starts_with", + "ends_with", + "contains", + "regex" + ] + }, + "pattern": { + "type": "string", + "description": "The pattern to match with." + } + }, + "required": [ + "operator", + "pattern" + ] + } + } + }, + { + "title": "branch_name_pattern", + "description": "Parameters to be used for the branch_name_pattern rule", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "branch_name_pattern" + ] + }, + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "How this rule appears when configuring it." + }, + "negate": { + "type": "boolean", + "description": "If true, the rule will fail if the pattern matches." + }, + "operator": { + "type": "string", + "description": "The operator to use for matching.", + "enum": [ + "starts_with", + "ends_with", + "contains", + "regex" + ] + }, + "pattern": { + "type": "string", + "description": "The pattern to match with." + } + }, + "required": [ + "operator", + "pattern" + ] + } + } + }, + { + "title": "tag_name_pattern", + "description": "Parameters to be used for the tag_name_pattern rule", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "tag_name_pattern" + ] + }, + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "How this rule appears when configuring it." + }, + "negate": { + "type": "boolean", + "description": "If true, the rule will fail if the pattern matches." + }, + "operator": { + "type": "string", + "description": "The operator to use for matching.", + "enum": [ + "starts_with", + "ends_with", + "contains", + "regex" + ] + }, + "pattern": { + "type": "string", + "description": "The pattern to match with." + } + }, + "required": [ + "operator", + "pattern" + ] + } + } + }, + { + "title": "file_path_restriction", + "description": "Prevent commits that include changes in specified file and folder paths from being pushed to the commit graph. This includes absolute paths that contain file names.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "file_path_restriction" + ] + }, + "parameters": { + "type": "object", + "properties": { + "restricted_file_paths": { + "type": "array", + "description": "The file paths that are restricted from being pushed to the commit graph.", + "items": { + "type": "string" + } + } + }, + "required": [ + "restricted_file_paths" + ] + } + } + }, + { + "title": "max_file_path_length", + "description": "Prevent commits that include file paths that exceed the specified character limit from being pushed to the commit graph.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "max_file_path_length" + ] + }, + "parameters": { + "type": "object", + "properties": { + "max_file_path_length": { + "type": "integer", + "description": "The maximum amount of characters allowed in file paths.", + "minimum": 1, + "maximum": 32767 + } + }, + "required": [ + "max_file_path_length" + ] + } + } + }, + { + "title": "file_extension_restriction", + "description": "Prevent commits that include files with specified file extensions from being pushed to the commit graph.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "file_extension_restriction" + ] + }, + "parameters": { + "type": "object", + "properties": { + "restricted_file_extensions": { + "type": "array", + "description": "The file extensions that are restricted from being pushed to the commit graph.", + "items": { + "type": "string" + } + } + }, + "required": [ + "restricted_file_extensions" + ] + } + } + }, + { + "title": "max_file_size", + "description": "Prevent commits with individual files that exceed the specified limit from being pushed to the commit graph.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "max_file_size" + ] + }, + "parameters": { + "type": "object", + "properties": { + "max_file_size": { + "type": "integer", + "description": "The maximum file size allowed in megabytes. This limit does not apply to Git Large File Storage (Git LFS).", + "minimum": 1, + "maximum": 100 + } + }, + "required": [ + "max_file_size" + ] + } + } + }, + { + "title": "workflows", + "description": "Require all changes made to a targeted branch to pass the specified workflows before they can be merged.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "workflows" + ] + }, + "parameters": { + "type": "object", + "properties": { + "do_not_enforce_on_create": { + "type": "boolean", + "description": "Allow repositories and branches to be created if a check would otherwise prohibit it." + }, + "workflows": { + "type": "array", + "description": "Workflows that must pass for this rule to pass.", + "items": { + "title": "WorkflowFileReference", + "description": "A workflow that must run for this rule to pass", + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "The path to the workflow file" + }, + "ref": { + "type": "string", + "description": "The ref (branch or tag) of the workflow file to use" + }, + "repository_id": { + "type": "integer", + "description": "The ID of the repository where the workflow is defined" + }, + "sha": { + "type": "string", + "description": "The commit SHA of the workflow file to use" + } + }, + "required": [ + "path", + "repository_id" + ] + } + } + }, + "required": [ + "workflows" + ] + } + } + }, + { + "title": "code_scanning", + "description": "Choose which tools must provide code scanning results before the reference is updated. When configured, code scanning must be enabled and have results for both the commit and the reference being updated.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "code_scanning" + ] + }, + "parameters": { + "type": "object", + "properties": { + "code_scanning_tools": { + "type": "array", + "description": "Tools that must provide code scanning results for this rule to pass.", + "items": { + "title": "CodeScanningTool", + "description": "A tool that must provide code scanning results for this rule to pass.", + "type": "object", + "properties": { + "alerts_threshold": { + "type": "string", + "description": "The severity level at which code scanning results that raise alerts block a reference update. For more information on alert severity levels, see \"[About code scanning alerts](https://docs.github.com/code-security/code-scanning/managing-code-scanning-alerts/about-code-scanning-alerts#about-alert-severity-and-security-severity-levels).\"", + "enum": [ + "none", + "errors", + "errors_and_warnings", + "all" + ] + }, + "security_alerts_threshold": { + "type": "string", + "description": "The severity level at which code scanning results that raise security alerts block a reference update. For more information on security severity levels, see \"[About code scanning alerts](https://docs.github.com/code-security/code-scanning/managing-code-scanning-alerts/about-code-scanning-alerts#about-alert-severity-and-security-severity-levels).\"", + "enum": [ + "none", + "critical", + "high_or_higher", + "medium_or_higher", + "all" + ] + }, + "tool": { + "type": "string", + "description": "The name of a code scanning tool" + } + }, + "required": [ + "alerts_threshold", + "security_alerts_threshold", + "tool" + ] + } + } + }, + "required": [ + "code_scanning_tools" + ] + } + } + }, + { + "title": "copilot_code_review", + "description": "Request Copilot code review for new pull requests automatically if the author has access to Copilot code review and their premium requests quota has not reached the limit.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "copilot_code_review" + ] + }, + "parameters": { + "type": "object", + "properties": { + "review_draft_pull_requests": { + "type": "boolean", + "description": "Copilot automatically reviews draft pull requests before they are marked as ready for review." + }, + "review_on_push": { + "type": "boolean", + "description": "Copilot automatically reviews each new push to the pull request." + } + } + } + } + } + ] + } + } + }, + "required": [ + "name", + "enforcement" + ] + }, + "EnvironmentsSettings": { + "description": "A deployment environment configuration entry", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "The name of the deployment environment", + "type": "string" + }, + "wait_timer": { + "description": "The amount of time to delay a job after the job is initially triggered (in minutes)", + "type": "integer" + }, + "reviewers": { + "description": "The people or teams that may review jobs that reference the environment", + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "description": "The type of reviewer (`User` or `Team`)", + "type": "string" + }, + "id": { + "description": "The id of the user or team who can review the deployment", + "type": "integer" + } + } + } + }, + "deployment_branch_policy": { + "description": "The type of deployment branch policy for this environment", + "type": "object", + "properties": { + "protected_branches": { + "description": "Whether only protected branches can be deployed to this environment", + "type": "boolean" + }, + "custom_branch_policies": { + "description": "Whether only branches that match the specified name patterns can deploy to this environment", + "type": "boolean" + } + } + }, + "prevent_self_review": { + "description": "Whether or not a user who created the job is prevented from approving their own job", + "type": "boolean" + } + } + }, + "CustomPropertiesSettings": { + "description": "A custom property entry", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "VariablesSettings": { + "description": "An Actions variable entry", + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "description": "The name of the variable", + "type": "string" + }, + "value": { + "description": "The value of the variable", + "type": "string" + }, + "visibility": { + "description": "The visibility of the variable. Can be `all`, `private`, or `selected`", + "type": "string", + "enum": [ + "all", + "private", + "selected" + ] + } + } + } + } +} \ No newline at end of file diff --git a/schema/repos.json b/schema/repos.json new file mode 100644 index 000000000..3a7c51301 --- /dev/null +++ b/schema/repos.json @@ -0,0 +1,324 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Safe-settings repo-level configuration", + "description": "Schema for .github/repos/{repo-name}.yml — repo-level safe-settings override configuration. Settings here are merged on top of org-level and suborg-level settings for the specific repository.", + "type": "object", + "properties": { + "repositories": { + "$ref": "#/$defs/RepositorySettings" + }, + "labels": { + "$ref": "#/$defs/LabelSettings" + }, + "collaborators": { + "description": "Collaborators: give specific users access to any repository.", + "type": "array", + "items": { + "$ref": "#/$defs/CollaboratorSettings" + } + }, + "teams": { + "description": "Teams", + "type": "array", + "items": { + "$ref": "#/$defs/TeamSettings" + } + }, + "milestones": { + "description": "Milestones: define milestones for Issues and Pull Requests", + "type": "array", + "items": { + "$ref": "#/$defs/MilestoneSettings" + } + }, + "branches": { + "description": "Branch protection rules", + "type": "array", + "items": { + "$ref": "#/$defs/BranchSettings" + } + }, + "autolinks": { + "description": "Autolinks", + "type": "array", + "items": { + "$ref": "#/$defs/AutolinkSettings" + } + }, + "validator": { + "$ref": "#/$defs/ValidatorSettings" + }, + "environments": { + "description": "Deployment environments", + "type": "array", + "items": { + "$ref": "#/$defs/EnvironmentsSettings" + } + }, + "custom_properties": { + "description": "Custom properties", + "type": "array", + "items": { + "$ref": "#/$defs/CustomPropertiesSettings" + } + }, + "variables": { + "description": "Repository or org-level Actions variables", + "type": "array", + "items": { + "$ref": "#/$defs/VariablesSettings" + } + } + }, + "$defs": { + "RepositorySettings": { + "description": "Repository settings", + "allOf": [ + { + "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2026-03-10.json#/paths/~1repos~1{owner}~1{repo}/patch/requestBody/content/application~1json/schema" + }, + { + "type": "object", + "properties": { + "auto_init": { + "description": "Create an initial commit with empty README. Keep this set to true in most cases since many of the policies below cannot be implemented on bare repos", + "type": "boolean" + }, + "gitignore_template": { + "description": "Desired language or platform [.gitignore template](https://github.com/github/gitignore) to apply. Use the name of the template without the extension. For example, 'Haskell'.", + "type": "string" + }, + "license_template": { + "description": "Choose an [open source license template](https://choosealicense.com/) that best suits your needs, and then use the [license keyword](https://help.github.com/articles/licensing-a-repository/#searching-github-by-license-type) as the `license_template` string. For example, 'mit' or 'mpl-2.0'.", + "type": "string" + }, + "topics": { + "description": "A list of topics to set on the repository", + "type": "array", + "items": { + "type": "string" + } + }, + "security": { + "description": "Settings for Code security and analysis", + "type": "object", + "properties": { + "enableVulnerabilityAlerts": { + "type": "boolean" + }, + "enableAutomatedSecurityFixes": { + "type": "boolean" + } + } + }, + "force_create": { + "description": "If true, create the repository if it does not already exist.", + "type": "boolean" + }, + "template": { + "description": "Name of a template repository to use when creating a new repository.", + "type": "string" + } + } + } + ] + }, + "LabelSettings": { + "description": "Labels: define labels for Issues and Pull Requests", + "type": "object", + "properties": { + "include": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "color": { + "description": "The hexadecimal color code for the label. If including a `#`, make sure to wrap it with quotes!", + "type": "string" + }, + "description": { + "type": "string" + }, + "oldname": { + "description": "Include the old name to rename an existing label", + "type": "string" + } + } + } + }, + "exclude": { + "description": "Ignore any labels matching these regexes (don't delete them)", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "CollaboratorSettings": { + "description": "A collaborator entry giving a specific user access to a repository.", + "allOf": [ + { + "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2026-03-10.json#/paths/~1repos~1{owner}~1{repo}~1collaborators~1{username}/put/requestBody/content/application~1json/schema" + }, + { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "exclude": { + "description": "You can exclude a list of repos for this collaborator and all repos except these repos would have this collaborator", + "type": "array", + "items": { + "type": "string" + } + }, + "include": { + "description": "You can include a list of repos for this collaborator and only those repos would have this collaborator", + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + }, + "TeamSettings": { + "description": "A team entry", + "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2026-03-10.json#/paths/~1orgs~1{org}~1teams/post/requestBody/content/application~1json/schema" + }, + "MilestoneSettings": { + "description": "A milestone entry", + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "state": { + "description": "The state of the milestone. Either `open` or `closed`", + "type": "string" + } + } + }, + "BranchSettings": { + "description": "A branch protection rule entry", + "type": "object", + "properties": { + "name": { + "description": "If the name of the branch value is specified as `default`, then the app will create a branch protection rule to apply against the default branch in the repo", + "type": "string" + }, + "protection": { + "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2026-03-10.json#/paths/~1repos~1{owner}~1{repo}~1branches~1{branch}~1protection/put/requestBody/content/application~1json/schema" + } + } + }, + "AutolinkSettings": { + "description": "An autolink reference entry", + "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2026-03-10.json#/paths/~1repos~1{owner}~1{repo}~1autolinks/post/requestBody/content/application~1json/schema" + }, + "ValidatorSettings": { + "description": "Repository name validation", + "type": "object", + "properties": { + "pattern": { + "type": "string" + } + } + }, + "RulesetSettings": { + "description": "A ruleset entry", + "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2026-03-10.json#/paths/~1orgs~1{org}~1rulesets/post/requestBody/content/application~1json/schema" + }, + "EnvironmentsSettings": { + "description": "A deployment environment configuration entry", + "type": "object", + "required": ["name"], + "properties": { + "name": { + "description": "The name of the deployment environment", + "type": "string" + }, + "wait_timer": { + "description": "The amount of time to delay a job after the job is initially triggered (in minutes)", + "type": "integer" + }, + "reviewers": { + "description": "The people or teams that may review jobs that reference the environment", + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "description": "The type of reviewer (`User` or `Team`)", + "type": "string" + }, + "id": { + "description": "The id of the user or team who can review the deployment", + "type": "integer" + } + } + } + }, + "deployment_branch_policy": { + "description": "The type of deployment branch policy for this environment", + "type": "object", + "properties": { + "protected_branches": { + "description": "Whether only protected branches can be deployed to this environment", + "type": "boolean" + }, + "custom_branch_policies": { + "description": "Whether only branches that match the specified name patterns can deploy to this environment", + "type": "boolean" + } + } + }, + "prevent_self_review": { + "description": "Whether or not a user who created the job is prevented from approving their own job", + "type": "boolean" + } + } + }, + "CustomPropertiesSettings": { + "description": "A custom property entry", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "VariablesSettings": { + "description": "An Actions variable entry", + "type": "object", + "required": ["name", "value"], + "properties": { + "name": { + "description": "The name of the variable", + "type": "string" + }, + "value": { + "description": "The value of the variable", + "type": "string" + }, + "visibility": { + "description": "The visibility of the variable. Can be `all`, `private`, or `selected`", + "type": "string", + "enum": ["all", "private", "selected"] + } + } + } + } +} diff --git a/schema/settings.json b/schema/settings.json index 4d390b38f..71eec48a2 100644 --- a/schema/settings.json +++ b/schema/settings.json @@ -1,11 +1,88 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Safe-settings org-level configuration", + "description": "Schema for .github/settings.yml — org-level safe-settings configuration", "type": "object", "properties": { "repositories": { + "$ref": "#/$defs/RepositorySettings" + }, + "labels": { + "$ref": "#/$defs/LabelSettings" + }, + "collaborators": { + "description": "Collaborators: give specific users access to any repository.", + "type": "array", + "items": { + "$ref": "#/$defs/CollaboratorSettings" + } + }, + "teams": { + "description": "Teams", + "type": "array", + "items": { + "$ref": "#/$defs/TeamSettings" + } + }, + "milestones": { + "description": "Milestones: define milestones for Issues and Pull Requests", + "type": "array", + "items": { + "$ref": "#/$defs/MilestoneSettings" + } + }, + "branches": { + "description": "Branch protection rules", + "type": "array", + "items": { + "$ref": "#/$defs/BranchSettings" + } + }, + "autolinks": { + "description": "Autolinks", + "type": "array", + "items": { + "$ref": "#/$defs/AutolinkSettings" + } + }, + "validator": { + "$ref": "#/$defs/ValidatorSettings" + }, + "rulesets": { + "description": "Rulesets. Org-level only — rulesets defined here apply to the organization and are NOT inherited by suborg or repo override files.", + "type": "array", + "items": { + "$ref": "#/$defs/RulesetSettings" + } + }, + "environments": { + "description": "Deployment environments", + "type": "array", + "items": { + "$ref": "#/$defs/EnvironmentsSettings" + } + }, + "custom_properties": { + "description": "Custom properties", + "type": "array", + "items": { + "$ref": "#/$defs/CustomPropertiesSettings" + } + }, + "variables": { + "description": "Repository or org-level Actions variables", + "type": "array", + "items": { + "$ref": "#/$defs/VariablesSettings" + } + } + }, + "$defs": { + "RepositorySettings": { + "description": "Repository settings", "allOf": [ { - "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2022-11-28.json#/paths/~1repos~1{owner}~1{repo}/patch/requestBody/content/application~1json/schema" + "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2026-03-10.json#/paths/~1repos~1{owner}~1{repo}/patch/requestBody/content/application~1json/schema" }, { "type": "object", @@ -40,12 +117,20 @@ "type": "boolean" } } + }, + "force_create": { + "description": "Force create the repository", + "type": "boolean" + }, + "template": { + "description": "Name of a template repository to use when creating the repository", + "type": "string" } } } ] }, - "labels": { + "LabelSettings": { "description": "Labels: define labels for Issues and Pull Requests", "type": "object", "properties": { @@ -80,117 +165,354 @@ } } }, - "milestones": { - "description": "Milestones: define milestones for Issues and Pull Requests", - "type": "array", - "items": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "state": { - "description": "The state of the milestone. Either `open` or `closed`", - "type": "string" + "CollaboratorSettings": { + "description": "A collaborator entry giving a specific user access to a repository.", + "allOf": [ + { + "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2026-03-10.json#/paths/~1repos~1{owner}~1{repo}~1collaborators~1{username}/put/requestBody/content/application~1json/schema" + }, + { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "exclude": { + "description": "You can exclude a list of repos for this collaborator and all repos except these repos would have this collaborator", + "type": "array", + "items": { + "type": "string" + } + }, + "include": { + "description": "You can include a list of repos for this collaborator and only those repos would have this collaborator", + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + }, + "TeamSettings": { + "description": "A team entry", + "allOf": [ + { + "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2026-03-10.json#/paths/~1orgs~1{org}~1teams~1{team_slug}~1repos~1{owner}~1{repo}/put/requestBody/content/application~1json/schema" + }, + { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "The name (or slug) of the team to grant access to the repo.", + "type": "string" + }, + "privacy": { + "description": "The level of privacy the team should have when it is created by safe-settings. The options are `secret` (visible only to organization owners and members of the team) or `closed` (visible to all members of the organization).", + "type": "string", + "enum": [ + "secret", + "closed" + ] + }, + "external_group": { + "description": "The display name of an external IdP group to link to this team.", + "type": "string" + }, + "exclude": { + "description": "Exclude this team from repos whose names match one of these glob patterns.", + "type": "array", + "items": { + "type": "string" + } + }, + "include": { + "description": "Apply this team only to repos whose names match one of these glob patterns.", + "type": "array", + "items": { + "type": "string" + } + } } } + ] + }, + "MilestoneSettings": { + "description": "A milestone entry", + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "state": { + "description": "The state of the milestone. Either `open` or `closed`", + "type": "string" + } } }, - "collaborators": { - "description": "Collaborators: give specific users access to any repository.", - "type": "array", - "items": { - "allOf": [ - { - "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2022-11-28.json#/paths/~1repos~1{owner}~1{repo}~1collaborators~1{username}/put/requestBody/content/application~1json/schema" - }, - { + "BranchSettings": { + "description": "A branch protection rule entry", + "type": "object", + "properties": { + "name": { + "description": "If the name of the branch value is specified as `default`, then the app will create a branch protection rule to apply against the default branch in the repo", + "type": "string" + }, + "protection": { + "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2026-03-10.json#/paths/~1repos~1{owner}~1{repo}~1branches~1{branch}~1protection/put/requestBody/content/application~1json/schema" + } + } + }, + "AutolinkSettings": { + "description": "An autolink reference entry", + "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2026-03-10.json#/paths/~1repos~1{owner}~1{repo}~1autolinks/post/requestBody/content/application~1json/schema" + }, + "ValidatorSettings": { + "description": "Repository name validation", + "type": "object", + "properties": { + "pattern": { + "type": "string" + } + } + }, + "RulesetSettings": { + "description": "A ruleset entry", + "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2026-03-10.json#/paths/~1orgs~1{org}~1rulesets/post/requestBody/content/application~1json/schema" + }, + "EnvironmentsSettings": { + "description": "A deployment environment configuration entry", + "type": "object", + "required": ["name"], + "properties": { + "name": { + "description": "The name of the deployment environment", + "type": "string" + }, + "wait_timer": { + "description": "The amount of time to delay a job after the job is initially triggered (in minutes)", + "type": "integer" + }, + "reviewers": { + "description": "The people or teams that may review jobs that reference the environment", + "type": "array", + "items": { "type": "object", "properties": { - "username": { + "type": { + "description": "The type of reviewer (`User` or `Team`)", "type": "string" }, - "exclude": { - "description": "You can exclude a list of repos for this collaborator and all repos except these repos would have this collaborator", - "type": "array", - "items": { - "type": "string" - } - }, - "include": { - "description": "You can include a list of repos for this collaborator and only those repos would have this collaborator", - "type": "array", - "items": { - "type": "string" - } + "id": { + "description": "The id of the user or team who can review the deployment", + "type": "integer" } } } - ] + }, + "deployment_branch_policy": { + "description": "The type of deployment branch policy for this environment", + "type": "object", + "properties": { + "protected_branches": { + "description": "Whether only protected branches can be deployed to this environment", + "type": "boolean" + }, + "custom_branch_policies": { + "description": "Whether only branches that match the specified name patterns can deploy to this environment", + "type": "boolean" + } + } + }, + "prevent_self_review": { + "description": "Whether or not a user who created the job is prevented from approving their own job", + "type": "boolean" + } } }, - "teams": { - "description": "Teams", - "type": "array", - "items": { - "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2022-11-28.json#/paths/~1orgs~1{org}~1teams/post/requestBody/content/application~1json/schema" + "CustomPropertiesSettings": { + "description": "A custom property entry", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } } }, - "branches": { - "description": "Branch protection rules", + "VariablesSettings": { + "description": "An Actions variable entry", + "type": "object", + "required": ["name", "value"], + "properties": { + "name": { + "description": "The name of the variable", + "type": "string" + }, + "value": { + "description": "The value of the variable", + "type": "string" + }, + "visibility": { + "description": "The visibility of the variable. Can be `all`, `private`, or `selected`", + "type": "string", + "enum": ["all", "private", "selected"] + } + } + }, + "custom_repository_roles": { + "description": "Org-level custom repository roles. Only valid in the org-level settings.yml.", "type": "array", "items": { + "type": "object", + "required": [ + "name", + "base_role", + "permissions" + ], "properties": { "name": { - "description": "If the name of the branch value is specified as `default`, then the app will create a branch protection rule to apply against the default branch in the repo", - "type": "string" + "type": "string", + "description": "The name of the custom role." + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "A short description of the role." + }, + "base_role": { + "type": "string", + "enum": [ + "read", + "triage", + "write", + "maintain" + ], + "description": "The system role from which this role inherits permissions." }, - "protection": { - "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2022-11-28.json#/paths/~1repos~1{owner}~1{repo}~1branches~1{branch}~1protection/put/requestBody/content/application~1json/schema" + "permissions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Additional fine-grained permissions included in this role." } } } }, - "custom_properties": { - "description": "Custom properties", + "app_installations": { + "description": "Manage which repositories a GitHub App installation can access. The target is a GitHub App installation rather than a repository. Repo selection follows the config hierarchy: org-level settings.yml selects all repos in the org; suborgs/*.yml selects repos by the suborg's targeting criteria; repos/*.yml adds the specific repo. Requires safe-settings to be installed on the enterprise with 'Enterprise organization installations' permission.", "type": "array", "items": { "type": "object", + "required": [ + "app_slug" + ], + "additionalProperties": false, "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" + "app_slug": { + "type": "string", + "description": "The slug of the GitHub App installation to manage." } } } }, - "autolinks": { - "description": "Autolinks", + "additive_plugins": { + "description": "List of plugins to run in additive mode. In additive mode the plugin will only add and update entries; it will never call remove(), so items that exist on GitHub but are absent from the YAML are preserved. Supported plugins: labels, collaborators, teams, milestones, autolinks, environments, custom_properties, variables, rulesets, custom_repository_roles, app_installations. Declare only in settings.yml (org level) to keep behavior consistent across all repos.", "type": "array", "items": { - "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2022-11-28.json#/paths/~1repos~1{owner}~1{repo}~1autolinks/post/requestBody/content/application~1json/schema" - } - }, - "validator": { - "description": "Repository name validation", - "type": "object", - "properties": { - "pattern": { - "type": "string" - } + "type": "string", + "enum": [ + "labels", + "collaborators", + "teams", + "milestones", + "autolinks", + "environments", + "custom_properties", + "variables", + "rulesets", + "custom_repository_roles", + "app_installations" + ] } }, - "rulesets": { - "description": "Rulesets", + "disable_plugins": { + "description": "List of plugins to disable at this configuration layer. Each entry is either a plugin name (string shorthand, equivalent to target: all) or an object {plugin, target}. target=self disables the plugin at this layer only; target=children disables it at all lower layers; target=all disables it at this layer and all lower layers. Cascade is union-only; lower layers cannot re-enable a disabled plugin.", "type": "array", "items": { - "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2022-11-28.json#/paths/~1orgs~1{org}~1rulesets/post/requestBody/content/application~1json/schema" + "oneOf": [ + { + "type": "string", + "enum": [ + "repository", + "labels", + "collaborators", + "teams", + "milestones", + "branches", + "autolinks", + "validator", + "rulesets", + "environments", + "custom_properties", + "custom_repository_roles", + "variables", + "archive", + "app_installations" + ] + }, + { + "type": "object", + "required": [ + "plugin" + ], + "additionalProperties": false, + "properties": { + "plugin": { + "type": "string", + "enum": [ + "repository", + "labels", + "collaborators", + "teams", + "milestones", + "branches", + "autolinks", + "validator", + "rulesets", + "environments", + "custom_properties", + "custom_repository_roles", + "variables", + "archive", + "app_installations" + ] + }, + "target": { + "type": "string", + "enum": [ + "self", + "children", + "all" + ], + "default": "all" + } + } + } + ] } } } -} +} \ No newline at end of file diff --git a/schema/suborgs.json b/schema/suborgs.json new file mode 100644 index 000000000..3a3c79def --- /dev/null +++ b/schema/suborgs.json @@ -0,0 +1,358 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Safe-settings suborg-level configuration", + "description": "Schema for .github/suborgs/*.yml — suborg-level safe-settings configuration. Defines which repos belong to the suborg and what settings to apply.", + "type": "object", + "properties": { + "suborgrepos": { + "type": "array", + "description": "Glob patterns matching repository names. Repos whose names match any pattern are included in this suborg.", + "items": { + "type": "string" + } + }, + "suborgteams": { + "type": "array", + "description": "Team slugs. Repos that belong to any of these teams are included in this suborg.", + "items": { + "type": "string" + } + }, + "suborgproperties": { + "type": "array", + "description": "Custom property filters. Repos with matching custom property values are included in this suborg.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "The name of the custom property" + }, + "values": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Accepted values for this property" + } + } + } + }, + "repositories": { + "$ref": "#/$defs/RepositorySettings" + }, + "labels": { + "$ref": "#/$defs/LabelSettings" + }, + "collaborators": { + "description": "Collaborators: give specific users access to any repository.", + "type": "array", + "items": { + "$ref": "#/$defs/CollaboratorSettings" + } + }, + "teams": { + "description": "Teams", + "type": "array", + "items": { + "$ref": "#/$defs/TeamSettings" + } + }, + "milestones": { + "description": "Milestones: define milestones for Issues and Pull Requests", + "type": "array", + "items": { + "$ref": "#/$defs/MilestoneSettings" + } + }, + "branches": { + "description": "Branch protection rules", + "type": "array", + "items": { + "$ref": "#/$defs/BranchSettings" + } + }, + "autolinks": { + "description": "Autolinks", + "type": "array", + "items": { + "$ref": "#/$defs/AutolinkSettings" + } + }, + "validator": { + "$ref": "#/$defs/ValidatorSettings" + }, + "environments": { + "description": "Deployment environments", + "type": "array", + "items": { + "$ref": "#/$defs/EnvironmentsSettings" + } + }, + "custom_properties": { + "description": "Custom properties", + "type": "array", + "items": { + "$ref": "#/$defs/CustomPropertiesSettings" + } + }, + "variables": { + "description": "Repository or org-level Actions variables", + "type": "array", + "items": { + "$ref": "#/$defs/VariablesSettings" + } + } + }, + "$defs": { + "RepositorySettings": { + "description": "Repository settings. Use force_create to create the repository if it does not exist, and template to specify a template repository to use when creating it.", + "allOf": [ + { + "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2026-03-10.json#/paths/~1repos~1{owner}~1{repo}/patch/requestBody/content/application~1json/schema" + }, + { + "type": "object", + "properties": { + "auto_init": { + "description": "Create an initial commit with empty README. Keep this set to true in most cases since many of the policies below cannot be implemented on bare repos", + "type": "boolean" + }, + "gitignore_template": { + "description": "Desired language or platform [.gitignore template](https://github.com/github/gitignore) to apply. Use the name of the template without the extension. For example, 'Haskell'.", + "type": "string" + }, + "license_template": { + "description": "Choose an [open source license template](https://choosealicense.com/) that best suits your needs, and then use the [license keyword](https://help.github.com/articles/licensing-a-repository/#searching-github-by-license-type) as the `license_template` string. For example, 'mit' or 'mpl-2.0'.", + "type": "string" + }, + "topics": { + "description": "A list of topics to set on the repository", + "type": "array", + "items": { + "type": "string" + } + }, + "security": { + "description": "Settings for Code security and analysis", + "type": "object", + "properties": { + "enableVulnerabilityAlerts": { + "type": "boolean" + }, + "enableAutomatedSecurityFixes": { + "type": "boolean" + } + } + }, + "force_create": { + "description": "If true, create the repository if it does not already exist.", + "type": "boolean" + }, + "template": { + "description": "Name of a template repository to use when creating a new repository.", + "type": "string" + } + } + } + ] + }, + "LabelSettings": { + "description": "Labels: define labels for Issues and Pull Requests", + "type": "object", + "properties": { + "include": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "color": { + "description": "The hexadecimal color code for the label. If including a `#`, make sure to wrap it with quotes!", + "type": "string" + }, + "description": { + "type": "string" + }, + "oldname": { + "description": "Include the old name to rename an existing label", + "type": "string" + } + } + } + }, + "exclude": { + "description": "Ignore any labels matching these regexes (don't delete them)", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "CollaboratorSettings": { + "description": "A collaborator entry giving a specific user access to a repository.", + "allOf": [ + { + "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2026-03-10.json#/paths/~1repos~1{owner}~1{repo}~1collaborators~1{username}/put/requestBody/content/application~1json/schema" + }, + { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "exclude": { + "description": "You can exclude a list of repos for this collaborator and all repos except these repos would have this collaborator", + "type": "array", + "items": { + "type": "string" + } + }, + "include": { + "description": "You can include a list of repos for this collaborator and only those repos would have this collaborator", + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + }, + "TeamSettings": { + "description": "A team entry", + "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2026-03-10.json#/paths/~1orgs~1{org}~1teams/post/requestBody/content/application~1json/schema" + }, + "MilestoneSettings": { + "description": "A milestone entry", + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "state": { + "description": "The state of the milestone. Either `open` or `closed`", + "type": "string" + } + } + }, + "BranchSettings": { + "description": "A branch protection rule entry", + "type": "object", + "properties": { + "name": { + "description": "If the name of the branch value is specified as `default`, then the app will create a branch protection rule to apply against the default branch in the repo", + "type": "string" + }, + "protection": { + "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2026-03-10.json#/paths/~1repos~1{owner}~1{repo}~1branches~1{branch}~1protection/put/requestBody/content/application~1json/schema" + } + } + }, + "AutolinkSettings": { + "description": "An autolink reference entry", + "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2026-03-10.json#/paths/~1repos~1{owner}~1{repo}~1autolinks/post/requestBody/content/application~1json/schema" + }, + "ValidatorSettings": { + "description": "Repository name validation", + "type": "object", + "properties": { + "pattern": { + "type": "string" + } + } + }, + "RulesetSettings": { + "description": "A ruleset entry", + "$ref": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2026-03-10.json#/paths/~1orgs~1{org}~1rulesets/post/requestBody/content/application~1json/schema" + }, + "EnvironmentsSettings": { + "description": "A deployment environment configuration entry", + "type": "object", + "required": ["name"], + "properties": { + "name": { + "description": "The name of the deployment environment", + "type": "string" + }, + "wait_timer": { + "description": "The amount of time to delay a job after the job is initially triggered (in minutes)", + "type": "integer" + }, + "reviewers": { + "description": "The people or teams that may review jobs that reference the environment", + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "description": "The type of reviewer (`User` or `Team`)", + "type": "string" + }, + "id": { + "description": "The id of the user or team who can review the deployment", + "type": "integer" + } + } + } + }, + "deployment_branch_policy": { + "description": "The type of deployment branch policy for this environment", + "type": "object", + "properties": { + "protected_branches": { + "description": "Whether only protected branches can be deployed to this environment", + "type": "boolean" + }, + "custom_branch_policies": { + "description": "Whether only branches that match the specified name patterns can deploy to this environment", + "type": "boolean" + } + } + }, + "prevent_self_review": { + "description": "Whether or not a user who created the job is prevented from approving their own job", + "type": "boolean" + } + } + }, + "CustomPropertiesSettings": { + "description": "A custom property entry", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "VariablesSettings": { + "description": "An Actions variable entry", + "type": "object", + "required": ["name", "value"], + "properties": { + "name": { + "description": "The name of the variable", + "type": "string" + }, + "value": { + "description": "The value of the variable", + "type": "string" + }, + "visibility": { + "description": "The visibility of the variable. Can be `all`, `private`, or `selected`", + "type": "string", + "enum": ["all", "private", "selected"] + } + } + } + } +} diff --git a/script/build-schema b/script/build-schema index 7611d089e..951f3bc76 100755 --- a/script/build-schema +++ b/script/build-schema @@ -1,13 +1,100 @@ #!/usr/bin/env node const $RefParser = require('@apidevtools/json-schema-ref-parser') -const fs = require('node:fs/promises'); +const fs = require('node:fs/promises') +const path = require('node:path') -(async () => { - const schema = await fs.readFile('schema/settings.json', 'utf-8').then(JSON.parse) +// Safe-settings lets users reference ruleset bypass actors and required +// reviewers by name instead of numeric id. GitHub's published schema only +// knows about the id fields, so after dereferencing we inject the optional +// name-based alternatives: +// - bypass_actors[].name -> alternative to actor_id (team slug, username, +// GitHub App slug, or repository role name; resolved via actor_type) +// - reviewer.slug -> alternative to reviewer.id (team slug) +const augmentRulesetAliases = (node) => { + if (Array.isArray(node)) { + node.forEach(augmentRulesetAliases) + return + } + if (!node || typeof node !== 'object') return - await $RefParser.dereference(schema) + const props = node.properties + if (props && typeof props === 'object') { + // Bypass actor object: has both actor_type and actor_id. + if (props.actor_type && props.actor_id && !props.name) { + props.name = { + type: 'string', + description: 'Human-friendly alternative to `actor_id`. The team slug, username, GitHub App slug, or repository role name (resolved using `actor_type`). Cannot be combined with `actor_id`.' + } + } + // Reviewer object: has id and a type enum restricted to Team. + if (props.id && props.type && Array.isArray(props.type.enum) && + props.type.enum.length === 1 && props.type.enum[0] === 'Team' && !props.slug) { + props.slug = { + type: 'string', + description: 'Human-friendly alternative to `id`. The slug of the team that must review changes to matching files. Cannot be combined with `id`.' + } + if (Array.isArray(node.required)) { + node.required = node.required.filter(r => r !== 'id') + } + } + } + for (const value of Object.values(node)) { + if (value && typeof value === 'object') augmentRulesetAliases(value) + } +} + +const schemas = [ + { src: 'schema/settings.json', dest: 'schema/dereferenced/settings.json' }, + { src: 'schema/suborgs.json', dest: 'schema/dereferenced/suborgs.json' }, + { src: 'schema/repos.json', dest: 'schema/dereferenced/repos.json' } +] + +;(async () => { await fs.mkdir('schema/dereferenced', { recursive: true }) - await fs.writeFile('schema/dereferenced/settings.json', JSON.stringify(schema, null, 2)) + + // Find the GitHub API spec URL from the schema files. + // The URL in the schema files is the single source of truth for which version is used. + const specUrlPattern = /(https:\/\/raw\.githubusercontent\.com\/github\/rest-api-description[^#"]+api\.github\.com[^#"]*\.json)/ + let specUrl + for (const { src } of schemas) { + const match = (await fs.readFile(src, 'utf8')).match(specUrlPattern) + if (match) { specUrl = match[1]; break } + } + + if (!specUrl) { + console.error('Could not find GitHub API spec URL in schema files.') + process.exit(1) + } + + // Pre-dereference the spec so all internal $refs (e.g. #/components/schemas/...) + // are resolved before it is used as an external reference target. + console.log(`Fetching ${specUrl} ...`) + const resolvedSpec = await $RefParser.dereference(specUrl) + + const githubApiResolver = { + order: 1, + canRead: /\/descriptions\/api\.github\.com\/api\.github\.com/, + read: () => resolvedSpec + } + + let hasErrors = false + for (const { src, dest } of schemas) { + try { + const dereferenced = await $RefParser.dereference(path.resolve(src), { + resolve: { githubApi: githubApiResolver } + }) + // Inject name-based aliases (bypass_actors[].name, reviewer.slug) that + // safe-settings supports but the published GitHub schema does not. + augmentRulesetAliases(dereferenced) + await fs.writeFile(dest, JSON.stringify(dereferenced, null, 2)) + console.log(`Dereferenced ${src} → ${dest}`) + } catch (err) { + console.error(`Error dereferencing ${src}: ${err.message}`) + hasErrors = true + } + } + + if (hasErrors) process.exit(1) })().catch(console.error) diff --git a/smoke-test.js b/smoke-test.js new file mode 100644 index 000000000..30ef1a643 --- /dev/null +++ b/smoke-test.js @@ -0,0 +1,3311 @@ +#!/usr/bin/env node + +/** + * Smoke Test for safe-settings + * + * Usage: + * 1. Ensure `.env` is configured with GH_ORG, APP_ID, PRIVATE_KEY, WEBHOOK_PROXY_URL, etc. + * 2. Set GH_TOKEN env var to a fine-grained PAT with org admin + repo permissions. + * This is required for drift-remediation tests (Phases 2 & 3) so that + * changes appear as a human (not Bot) and trigger safe-settings webhooks. + * 3. Run: `node smoke-test.js` + * Add --interactive to pause after each phase for manual validation. + * Set SMOKE_VERBOSE=1 for live safe-settings logs. + * Optional (Phase 16 — ruleset name resolution): set SMOKE_NR_USER to a + * username and/or SMOKE_NR_APP_SLUG to an installed GitHub App slug to also + * exercise User and Integration bypass-actor name resolution. + * + * Auth: + * - Octokit (GitHub App): APP_ID + PRIVATE_KEY from .env — used for most operations. + * - gh CLI (user PAT): GH_TOKEN env var — used for drift tests only. + */ + +const { execSync, spawn } = require('child_process') +const fs = require('fs') +const path = require('path') +const readline = require('readline') + +// ─── Configuration ─────────────────────────────────────────────────────────── + +function loadEnv () { + const envPath = path.join(__dirname, '.env') + if (!fs.existsSync(envPath)) throw new Error('.env file not found') + const lines = fs.readFileSync(envPath, 'utf8').split('\n') + let currentKey = null + let currentValue = '' + let inMultiline = false + + for (const line of lines) { + if (inMultiline) { + currentValue += '\n' + line + if (line.includes('"') || line.includes("'")) { + const val = currentValue.replace(/^["']|["']$/g, '') + // Like dotenv: .env values don't override existing env vars + if (!(currentKey in process.env)) process.env[currentKey] = val + inMultiline = false + } + continue + } + const trimmed = line.trim() + if (!trimmed || trimmed.startsWith('#')) continue + const eqIdx = trimmed.indexOf('=') + if (eqIdx === -1) continue + currentKey = trimmed.slice(0, eqIdx).trim() + currentValue = trimmed.slice(eqIdx + 1).trim() + if ((currentValue.startsWith('"') && !currentValue.endsWith('"')) || + (currentValue.startsWith("'") && !currentValue.endsWith("'"))) { + inMultiline = true + continue + } + const val = currentValue.replace(/^["']|["']$/g, '') + if (!(currentKey in process.env)) process.env[currentKey] = val + } +} + +loadEnv() + +const ORG = process.env.GH_ORG || 'decyjphr-emu' +const ADMIN_REPO = process.env.ADMIN_REPO || 'admin' +const CONFIG_PATH = process.env.CONFIG_PATH || '.github' +const APP_ID = process.env.APP_ID +const PRIVATE_KEY = (process.env.PRIVATE_KEY || '').replace(/\\n/g, '\n') + +const TEST_REPOS = ['test', 'demo-repo-service1', 'demo-repo-service2', 'combined-settings-repo', 'smoke-team-filter'] +const TEST_TEAMS = ['AD-GRP-PAYMENTS-PLATFORM-OWNERS', 'awesometeam-a-approvers', 'jefeish-edj-test'] +// Teams exercised by the team include/exclude filter phase (Phase 18). +const SMOKE_FILTER_REPO = 'smoke-team-filter' +const SMOKE_FILTER_TEAMS = ['smoke-filter-included', 'smoke-filter-excluded', 'smoke-filter-nomatch'] + +// Principals created on demand for the ruleset name-resolution phase (Phase 16) +const SMOKE_NR_TEAM = 'safe-settings-smoke-nr-team' +const SMOKE_NR_ROLE = 'safe-settings-smoke-nr-role' + +const POLL_INTERVAL_MS = 5000 +const MAX_POLL_MS = 120000 +const WEBHOOK_SETTLE_MS = 15000 + +// Fine-grained PAT for drift tests (must appear as a human, not Bot) +const GH_TOKEN = process.env.GH_TOKEN || '' + +// ─── App installation plugin config (Phase 17) ─────────────────────────────── +// Enterprise slug — required for the app_installations plugin, which manages +// GitHub App installation repository access via the Enterprise organization +// installations API. Read from GH_ENTERPRISE (same var safe-settings uses). +const GH_ENTERPRISE = process.env.GH_ENTERPRISE || '' +// One or more enterprise-level GitHub App slugs to exercise the plugin. +// ONE app is sufficient for full lifecycle coverage; providing a second app +// (comma-separated) additionally verifies that changes to one app's +// installation do not affect another's. Pre-create these as enterprise GitHub +// Apps and install them on the org (creation requires a human). +// SMOKE_APP_SLUGS=app-one,app-two (or singular SMOKE_APP_SLUG=app-one) +const APP_SLUGS = (process.env.SMOKE_APP_SLUGS || process.env.SMOKE_APP_SLUG || '') + .split(',').map(s => s.trim()).filter(Boolean) +// Dedicated repos created/managed by the app_installations phase. +const APP_TEST_REPOS = ['smoke-app-repo-1', 'smoke-app-repo-2', 'smoke-app-repo-3'] +// Enterprise org installations API version (matches lib/appOctokitClient.js). +const APPS_API_VERSION = '2026-03-10' + +// Interactive mode: pause after each phase for manual validation +// Accepts --interactive flag or bare positional "interactive" word. +const INTERACTIVE = process.argv.includes('--interactive') || process.argv.slice(2).includes('interactive') + +// Phase filter: supports single, comma-separated, or range values. +// --phase 3 → only phase 3 +// --phase 1,2,3 → phases 1, 2, and 3 +// --phase 1-3 → phases 1 through 3 +// npm run smoke-test:phase -- 1-3 interactive +const PHASE_ARG_IDX = process.argv.indexOf('--phase') +const _parsePhaseSet = (raw) => { + if (!raw) return null + const nums = new Set() + for (const part of raw.split(',')) { + const range = part.match(/^(\d+)-(\d+)$/) + if (range) { + const lo = parseInt(range[1], 10) + const hi = parseInt(range[2], 10) + for (let i = lo; i <= hi; i++) nums.add(i) + } else if (/^\d+$/.test(part.trim())) { + nums.add(parseInt(part.trim(), 10)) + } + } + return nums.size > 0 ? nums : null +} +const ONLY_PHASES = PHASE_ARG_IDX !== -1 + ? _parsePhaseSet(process.argv[PHASE_ARG_IDX + 1]) + : (() => { + // Accept bare positional phase spec (e.g. "3" or "1-3" or "1,2,3") + const positional = process.argv.slice(2).find(a => !a.startsWith('--') && /^[\d,\-]+$/.test(a) && !/^-\d/.test(a)) + return positional !== undefined ? _parsePhaseSet(positional) : null + })() + +class InteractiveExit extends Error { + constructor (action) { + super(`interactive:${action}`) + this.action = action + } +} + +// ─── Octokit client (initialized in main) ──────────────────────────────────── + +let octokit = null +// Enterprise-installation-authenticated client (Phase 17). Null when the app +// is not installed on the enterprise or GH_ENTERPRISE is unset. +let entOctokit = null +// Snapshot of each managed app's installation state, captured at the start of +// Phase 17 and restored during teardown so shared apps are left untouched. +const appInstallSnapshot = {} + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +let passCount = 0 +let failCount = 0 +const failures = [] + +function log (msg) { console.log(`\x1b[36m[smoke]\x1b[0m ${msg}`) } +function logPass (msg) { passCount++; console.log(`\x1b[32m ✓ ${msg}\x1b[0m`) } +function logFail (msg) { failCount++; failures.push(msg); console.log(`\x1b[31m ✗ ${msg}\x1b[0m`) } +function logPhase (msg) { console.log(`\n\x1b[35m═══ ${msg} ═══\x1b[0m`) } + +function assert (condition, msg) { + if (condition) logPass(msg) + else logFail(msg) + return condition +} + +function sleep (ms) { return new Promise(resolve => setTimeout(resolve, ms)) } + +async function poll (fn, { timeout = MAX_POLL_MS, interval = POLL_INTERVAL_MS, desc = 'condition' } = {}) { + const start = Date.now() + while (Date.now() - start < timeout) { + const result = await fn() + if (result) return result + await sleep(interval) + } + log(` ⚠ Timed out waiting for ${desc}`) + return null +} + +// ─── Interactive mode ───────────────────────────────────────────────────────── + +let skipNext = false + +async function pause (phaseName) { + return new Promise((resolve) => { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }) + process.stdout.write( + `\n\x1b[33m[interactive] "${phaseName}" complete.\x1b[0m\n` + + ` \x1b[90mPress Enter to continue, 's' skip next, 'q' quit+teardown, 'a' abort: \x1b[0m` + ) + rl.once('line', (answer) => { + const input = answer.trim().toLowerCase() + if (input === 's') resolve('skip') + else if (input === 'q') resolve('quit') + else if (input === 'a') resolve('abort') + else resolve('continue') + rl.close() + }) + rl.once('close', () => resolve('continue')) + }) +} + +async function runPhase (label, fn) { + if (skipNext) { + log(`\x1b[33m[interactive] Skipping ${label}\x1b[0m`) + skipNext = false + return 'skipped' + } + await fn() + if (!INTERACTIVE) return 'continue' + const action = await pause(label) + if (action === 'skip') skipNext = true + return action +} + +async function confirmMerge (owner, repo, prNumber) { + return new Promise((resolve) => { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }) + process.stdout.write( + `\n\x1b[33m[interactive] PR #${prNumber} is ready to merge.\x1b[0m\n` + + ` \x1b[90mPress Enter to merge, 'c' to close PR, 'q' quit+teardown, 'a' abort: \x1b[0m` + ) + rl.once('line', (answer) => { + const input = answer.trim().toLowerCase() + if (input === 'c') resolve('close') + else if (input === 'q') resolve('quit') + else if (input === 'a') resolve('abort') + else resolve('merge') + rl.close() + }) + rl.once('close', () => resolve('merge')) + }) +} + +async function safeMerge (owner, repo, prNumber) { + if (INTERACTIVE) { + const action = await confirmMerge(owner, repo, prNumber) + if (action !== 'merge') { + try { await octokit.rest.pulls.update({ owner, repo, pull_number: prNumber, state: 'closed' }) } catch { /* ok */ } + log(`\x1b[33m[interactive] PR #${prNumber} closed.\x1b[0m`) + if (action === 'quit' || action === 'abort') throw new InteractiveExit(action) + return false + } + } + log('Merging PR...') + await mergePR(owner, repo, prNumber) + return true +} + +// ─── GitHub API helpers ────────────────────────────────────────────────────── + +async function getDefaultBranch () { + const { data } = await octokit.rest.repos.get({ owner: ORG, repo: ADMIN_REPO }) + return data.default_branch || 'main' +} + +async function createOrUpdateFile (owner, repo, filePath, content, branch, message) { + const b64 = Buffer.from(content).toString('base64') + let sha = null + try { + const { data } = await octokit.rest.repos.getContent({ owner, repo, path: filePath, ref: branch }) + sha = data.sha + } catch { /* file doesn't exist */ } + const params = { owner, repo, path: filePath, message, content: b64, branch } + if (sha) params.sha = sha + return (await octokit.rest.repos.createOrUpdateFileContents(params)).data +} + +async function deleteFile (owner, repo, filePath, branch, message) { + try { + const { data } = await octokit.rest.repos.getContent({ owner, repo, path: filePath, ref: branch }) + await octokit.rest.repos.deleteFile({ owner, repo, path: filePath, message, sha: data.sha, branch }) + } catch { /* file doesn't exist */ } +} + +async function cleanDirectory (owner, repo, dirPath) { + const branch = await getDefaultBranch() + try { + const { data } = await octokit.rest.repos.getContent({ owner, repo, path: dirPath, ref: branch }) + if (Array.isArray(data)) { + for (const file of data) { + if (file.type === 'file') { + await deleteFile(owner, repo, file.path, branch, `Clean up ${file.path}`) + } + } + } + } catch { /* directory doesn't exist */ } +} + +async function createBranch (owner, repo, branchName) { + const defaultBranch = await getDefaultBranch() + const { data: ref } = await octokit.rest.git.getRef({ owner, repo, ref: `heads/${defaultBranch}` }) + await octokit.rest.git.createRef({ owner, repo, ref: `refs/heads/${branchName}`, sha: ref.object.sha }) +} + +async function deleteBranch (owner, repo, branch) { + try { await octokit.rest.git.deleteRef({ owner, repo, ref: `heads/${branch}` }) } catch { /* ok */ } +} + +async function createPR (owner, repo, title, head, base) { + const { data } = await octokit.rest.pulls.create({ owner, repo, title, head, base, body: `Smoke test: ${title}` }) + log(` Created PR #${data.number}`) + return data +} + +async function mergePR (owner, repo, prNumber) { + return (await octokit.rest.pulls.merge({ owner, repo, pull_number: prNumber, merge_method: 'merge' })).data +} + +async function deleteRepo (owner, repo) { + try { await octokit.rest.repos.delete({ owner, repo }) } catch { /* ok */ } +} + +async function deleteTeam (org, teamSlug) { + try { await octokit.rest.teams.deleteInOrg({ org, team_slug: teamSlug }) } catch { /* ok */ } +} + +async function ensureTeam (org, name) { + try { + const { data } = await octokit.rest.teams.getByName({ org, team_slug: name }) + return data + } catch { /* team doesn't exist yet */ } + try { + const { data } = await octokit.rest.teams.create({ org, name, privacy: 'closed' }) + return data + } catch { return null } +} + +async function getCustomRepositoryRole (org, name) { + try { + const { data } = await octokit.request('GET /orgs/{org}/custom-repository-roles', { org }) + return (data.custom_roles || []).find(role => role.name === name) || null + } catch { return null } +} + +async function createCustomRepositoryRole (org, name, description) { + const existing = await getCustomRepositoryRole(org, name) + if (existing) return existing + return (await octokit.request('POST /orgs/{org}/custom-repository-roles', { + org, + name, + description, + base_role: 'read', + permissions: ['delete_alerts_code_scanning'] + })).data +} + +async function deleteCustomRepositoryRole (org, name) { + const role = await getCustomRepositoryRole(org, name) + if (!role) return + await octokit.request('DELETE /orgs/{org}/custom-repository-roles/{role_id}', { org, role_id: role.id }) +} + +async function getOrgRuleset (org, name) { + try { + const { data: rulesets } = await octokit.request('GET /orgs/{org}/rulesets', { org }) + return rulesets.find(ruleset => ruleset.name === name) || null + } catch { return null } +} + +async function getRepoRuleset (owner, repo, name) { + try { + const { data: rulesets } = await octokit.request('GET /repos/{owner}/{repo}/rulesets', { owner, repo }) + return rulesets.find(ruleset => ruleset.name === name) || null + } catch { return null } +} + +async function getRepoRulesetDetails (owner, repo, rulesetId) { + try { + const { data } = await octokit.request('GET /repos/{owner}/{repo}/rulesets/{ruleset_id}', { owner, repo, ruleset_id: rulesetId }) + return data + } catch { return null } +} + +async function setRepoCustomProperty (owner, repo, propertyName, value) { + await octokit.request('PATCH /repos/{owner}/{repo}/properties/values', { + owner, + repo, + properties: [ + { property_name: propertyName, value } + ] + }) +} + +async function createOrgRuleset (org, name) { + const existing = await getOrgRuleset(org, name) + if (existing) return existing + return (await octokit.request('POST /orgs/{org}/rulesets', { + org, + name, + target: 'repository', + source_type: 'Organization', + source: org, + enforcement: 'disabled', + conditions: { + repository_property: { + exclude: [], + include: [ + { name: 'visibility', source: 'system', property_values: ['private'] } + ] + } + }, + rules: [{ type: 'repository_delete' }] + })).data +} + +async function deleteOrgRuleset (org, name) { + const ruleset = await getOrgRuleset(org, name) + if (!ruleset) return + await octokit.request('DELETE /orgs/{org}/rulesets/{ruleset_id}', { org, ruleset_id: ruleset.id }) +} + +async function waitForCheckRun (owner, repo, sha, { timeout = MAX_POLL_MS } = {}) { + return poll(async () => { + const { data } = await octokit.rest.checks.listForRef({ owner, repo, ref: sha }) + const cr = data.check_runs.find(c => c.name === 'Safe-setting validator') + return (cr && cr.status === 'completed') ? cr : null + }, { timeout, desc: 'check run to complete' }) +} + +// Ensure a repository exists in the org (create it directly if missing). +async function ensureRepo (name) { + try { + await octokit.rest.repos.get({ owner: ORG, repo: name }) + return + } catch { /* needs creation */ } + try { + await octokit.rest.repos.createInOrg({ org: ORG, name, private: true, auto_init: true }) + await poll(async () => { + try { await octokit.rest.repos.get({ owner: ORG, repo: name }); return true } catch { return null } + }, { desc: `repo ${name} to be created`, timeout: 30000 }) + } catch (e) { log(` Could not create repo ${name}: ${e.message}`) } +} + +// ─── Enterprise organization installations API helpers (Phase 17) ──────────── + +async function listOrgAppInstallations () { + const options = entOctokit.request.endpoint.merge( + 'GET /enterprises/{enterprise}/apps/organizations/{org}/installations', + { enterprise: GH_ENTERPRISE, org: ORG, headers: { 'X-GitHub-Api-Version': APPS_API_VERSION } } + ) + return entOctokit.paginate(options) +} + +async function getAppInstallation (appSlug) { + const installations = await listOrgAppInstallations() + return installations.find(i => i.app_slug === appSlug) || null +} + +async function listInstallationRepoNames (installationId) { + const options = entOctokit.request.endpoint.merge( + 'GET /enterprises/{enterprise}/apps/organizations/{org}/installations/{installation_id}/repositories', + { enterprise: GH_ENTERPRISE, org: ORG, installation_id: installationId, headers: { 'X-GitHub-Api-Version': APPS_API_VERSION } } + ) + const repos = await entOctokit.paginate(options) + return repos.map(r => r.name) +} + +async function setInstallationSelection (installationId, selection, repositories) { + const params = { + enterprise: GH_ENTERPRISE, + org: ORG, + installation_id: installationId, + repository_selection: selection, + headers: { 'X-GitHub-Api-Version': APPS_API_VERSION } + } + if (selection === 'selected') params.repositories = repositories || [] + await entOctokit.request('PATCH /enterprises/{enterprise}/apps/organizations/{org}/installations/{installation_id}/repositories', params) +} + +async function addInstallationRepos (installationId, repositoryNames) { + await entOctokit.request('PATCH /enterprises/{enterprise}/apps/organizations/{org}/installations/{installation_id}/repositories/add', { + enterprise: GH_ENTERPRISE, + org: ORG, + installation_id: installationId, + repositories: repositoryNames, + headers: { 'X-GitHub-Api-Version': APPS_API_VERSION } + }) +} + +// ─── Safe-settings process management ──────────────────────────────────────── + +let ssProcess = null + +function startSafeSettings () { + log('Starting safe-settings...') + ssProcess = spawn('npm', ['start'], { + cwd: __dirname, + env: process.env, + stdio: ['ignore', 'pipe', 'pipe'] + }) + ssProcess.stdout.on('data', (d) => { if (process.env.SMOKE_VERBOSE) process.stdout.write(d) }) + ssProcess.stderr.on('data', (d) => { if (process.env.SMOKE_VERBOSE) process.stderr.write(d) }) + ssProcess.on('exit', (code) => { log(`safe-settings exited with code ${code}`) }) +} + +function stopSafeSettings () { + if (ssProcess) { + log('Stopping safe-settings...') + ssProcess.kill('SIGTERM') + ssProcess = null + } +} + +// ─── YAML Configs ──────────────────────────────────────────────────────────── + +const REPO_TEST_YML = `repository: + name: test + description: Demo repository created via safe-settings + private: true + auto_init: true + force_create: true + has_issues: true + has_projects: false + has_wiki: false + delete_branch_on_merge: true + allow_squash_merge: true + allow_merge_commit: false + allow_rebase_merge: true + +teams: + - name: expert-services-developers + permission: push + +custom_properties: + - property_name: ent-ownership + value: expert-services + - property_name: ent-supervisory-org + value: expert-services + +rulesets: +- name: synk + target: branch + enforcement: disabled + bypass_actors: + - actor_id: 1 + actor_type: OrganizationAdmin + bypass_mode: pull_request + + conditions: + ref_name: + include: ["~DEFAULT_BRANCH"] + exclude: ["refs/heads/oldmaster"] + + rules: + - type: creation + - type: update + - type: deletion + - type: required_linear_history + - type: required_signatures + - type: pull_request + parameters: + dismiss_stale_reviews_on_push: true + require_code_owner_review: true + require_last_push_approval: true + required_approving_review_count: 2 + required_review_thread_resolution: true + + - type: commit_message_pattern + parameters: + name: test commit_message_pattern + negate: true + operator: starts_with + pattern: skip* + + - type: commit_author_email_pattern + parameters: + name: test commit_author_email_pattern + negate: false + operator: regex + pattern: "^.*@example.com$" + + - type: committer_email_pattern + parameters: + name: test committer_email_pattern + negate: false + operator: regex + pattern: "^.*@example.com$" + + - type: branch_name_pattern + parameters: + name: test branch_name_pattern + negate: false + operator: regex + pattern: ".*\\\\/.*" + +- name: Prevent merges when new SONAR alerts are introduced + target: branch + enforcement: active + conditions: + ref_name: + include: + - "~DEFAULT_BRANCH" + exclude: [] + bypass_actors: + - actor_type: OrganizationAdmin + bypass_mode: always + rules: + - type: code_scanning + parameters: + code_scanning_tools: + - tool: Sonar + alerts_threshold: none + security_alerts_threshold: medium_or_higher +` + +const REPO_TEST_OTHER_OWNERSHIP_YML = REPO_TEST_YML.replace( + ' - property_name: ent-ownership\n value: expert-services', + ' - property_name: ent-ownership\n value: other-services' +) + +const REPO_DEMO_SERVICE1_YML = `# Safe-Settings Configuration +repository: + name: demo-repo-service1 + description: "Repository 2 sample" + visibility: private + default_branch: main + homepage: "" + auto_init: true + force_create: true + delete_branch_on_merge: true + archived: false + topics: + - topic1 + - topic2 + +teams: + - name: AD-GRP-PAYMENTS-PLATFORM-OWNERS + permission: admin + - name: awesometeam-a-approvers + permission: push + - name: expert-services-developers + permission: push + +branches: + - name: main + protection: + required_status_checks: + strict: true + contexts: [] + required_pull_request_reviews: + required_approving_review_count: 2 + dismiss_stale_reviews: false + require_code_owner_reviews: true + require_last_push_approval: false + bypass_pull_request_allowances: + apps: [] + users: [] + teams: [] + dismissal_restrictions: + users: [] + teams: [] + enforce_admins: true + restrictions: + apps: [] + users: [] + teams: [] + + - name: develop + protection: + required_status_checks: + strict: true + contexts: [] + required_pull_request_reviews: + required_approving_review_count: 1 + dismiss_stale_reviews: false + require_code_owner_reviews: true + require_last_push_approval: false + bypass_pull_request_allowances: + apps: [] + users: [] + teams: [] + dismissal_restrictions: + users: [] + teams: [] + enforce_admins: true + restrictions: + apps: [] + users: [] + teams: [] +` + +const SUBORG_EXPERT_SERVICES_YML = `suborgteams: + - expert-services-developers + +rulesets: + - name: Protect release and production branches + target: branch + enforcement: active + conditions: + ref_name: + include: + - refs/heads/release/* + - refs/heads/production + exclude: [] + bypass_actors: + - actor_type: OrganizationAdmin + bypass_mode: always + rules: + - type: creation + - type: pull_request + parameters: + required_approving_review_count: 1 + dismiss_stale_reviews_on_push: false + require_code_owner_review: false + require_last_push_approval: false + required_review_thread_resolution: false + allowed_merge_methods: + - merge + - squash + - rebase + required_reviewers: + - minimum_approvals: 1 + file_patterns: + - "*.js" + reviewer: + id: 11721733 + type: Team +` + +const SUBORG_EXPERT_SERVICES_PROPERTY_YML = SUBORG_EXPERT_SERVICES_YML.replace( + 'suborgteams:\n - expert-services-developers', + 'suborgproperties:\n - ent-ownership: expert-services' +) + +// Suborg config that narrows targeting to only the 'test' repo via +// suborgrepos. Used to test that repos dropping out of suborg targeting +// (due to targeting rule changes in the suborg.yml) have their +// suborg-applied rulesets removed. +const SUBORG_EXPERT_SERVICES_NARROW_YML = SUBORG_EXPERT_SERVICES_YML.replace( + 'suborgteams:\n - expert-services-developers', + 'suborgrepos:\n - test' +) + +const REPO_DEMO_SERVICE1_ARCHIVED_YML = `# Safe-Settings Configuration +repository: + name: demo-repo-service1 + description: "Repository 2 sample" + visibility: private + default_branch: main + homepage: "" + auto_init: true + force_create: true + delete_branch_on_merge: true + archived: true +` + +const REPO_DEMO_SERVICE2_YML = `# Safe-Settings Configuration +repository: + name: demo-repo-service2 + description: "Repository 2 sample" + visibility: private + default_branch: main + homepage: "" + auto_init: true + force_create: true + delete_branch_on_merge: true + archived: false + topics: + - topic1 + - topic2 + +teams: + - name: expert-services-developers + permission: push +` + +const REPO_DEMO_SERVICE2_EXTERNAL_GROUP_YML = `# Safe-Settings Configuration +repository: + name: demo-repo-service2 + description: "Repository 2 sample" + visibility: private + default_branch: main + homepage: "" + auto_init: true + force_create: true + delete_branch_on_merge: true + archived: false + topics: + - topic1 + - topic2 + +teams: + - name: expert-services-developers + permission: push + - name: jefeish-edj-test + permission: push + external_group: jefeish-edj-test +` + +const REPO_DEMO_SERVICE2_NO_EXTERNAL_GROUP_YML = `# Safe-Settings Configuration +repository: + name: demo-repo-service2 + description: "Repository 2 sample" + visibility: private + default_branch: main + homepage: "" + auto_init: true + force_create: true + delete_branch_on_merge: true + archived: false + topics: + - topic1 + - topic2 + +teams: + - name: expert-services-developers + permission: push +` + +const SETTINGS_YML_ORG = `# Org-level safe-settings configuration + +rulesets: + - name: test + target: repository + source_type: Organization + source: ${ORG} + enforcement: disabled + conditions: + repository_property: + exclude: [] + include: + - name: visibility + source: system + property_values: + - internal + rules: + - type: repository_delete + +custom_repository_roles: + - name: security-engineer + description: Can contribute code and manage the security pipeline + base_role: maintain + permissions: + - delete_alerts_code_scanning +` + +// Phase 10a: settings.yml that disables custom_repository_roles at org-self, +// and tries to add a NEW role ("disabled-role"). The new role must NOT be created. +const SETTINGS_YML_DISABLE_CRR = `# Org-level settings with disable_plugins (custom_repository_roles) + +disable_plugins: + - plugin: custom_repository_roles + target: self + +rulesets: + - name: test + target: repository + source_type: Organization + source: ${ORG} + enforcement: disabled + conditions: + repository_property: + exclude: [] + include: + - name: visibility + source: system + property_values: + - internal + rules: + - type: repository_delete + +custom_repository_roles: + - name: security-engineer + description: Can contribute code and manage the security pipeline + base_role: maintain + permissions: + - delete_alerts_code_scanning + - name: disabled-role + description: This role MUST NOT be created (custom_repository_roles disabled) + base_role: read + permissions: + - delete_alerts_code_scanning +` + +// Phase 10b: settings.yml with invalid disable_plugins entry — should fail validation +const SETTINGS_YML_INVALID_DISABLE = `# Org-level settings with invalid disable_plugins + +disable_plugins: + - not-a-real-plugin +` + +// Phase 11a: settings.yml with additive_plugins for labels and custom_properties. +// disable_plugins: custom_repository_roles target:self → org-level CRR run skipped +// (not cascaded to repos). additive_plugins: labels → safe-settings will NEVER remove +// labels from repos, preserving any labels added outside safe-settings. +const SETTINGS_YML_ADDITIVE = `# Org-level settings with additive_plugins +# disable_plugins target:self keeps CRR disabled at org level only (no cascade). +# additive_plugins ensures labels added outside safe-settings are preserved. + +disable_plugins: + - plugin: custom_repository_roles + target: self + +additive_plugins: + - labels + - custom_properties + +labels: + - name: safe-settings-base + color: '0075ca' + description: Baseline label applied by safe-settings policy + +rulesets: + - name: test + target: repository + source_type: Organization + source: ${ORG} + enforcement: disabled + conditions: + repository_property: + exclude: [] + include: + - name: visibility + source: system + property_values: + - internal + rules: + - type: repository_delete + +custom_repository_roles: + - name: security-engineer + description: Can contribute code and manage the security pipeline + base_role: maintain + permissions: + - delete_alerts_code_scanning +` + +// Phase 11b trigger: same as SETTINGS_YML_ADDITIVE with a comment bump so the +// push event fires and safe-settings re-processes all repos. +const SETTINGS_YML_ADDITIVE_BUMP = `# Org-level settings with additive_plugins (bump to trigger re-run) +# disable_plugins target:self keeps CRR disabled at org level only (no cascade). +# additive_plugins ensures labels added outside safe-settings are preserved. + +disable_plugins: + - plugin: custom_repository_roles + target: self + +additive_plugins: + - labels + - custom_properties + +labels: + - name: safe-settings-base + color: '0075ca' + description: Baseline label applied by safe-settings policy + +rulesets: + - name: test + target: repository + source_type: Organization + source: ${ORG} + enforcement: disabled + conditions: + repository_property: + exclude: [] + include: + - name: visibility + source: system + property_values: + - internal + rules: + - type: repository_delete + +custom_repository_roles: + - name: security-engineer + description: Can contribute code and manage the security pipeline + base_role: maintain + permissions: + - delete_alerts_code_scanning +` + +// Phase 11c: same labels policy but WITHOUT additive_plugins — used to confirm +// that without additive mode safe-settings DOES remove the external label. +const SETTINGS_YML_NO_ADDITIVE = `# Org-level settings WITHOUT additive_plugins (for contrast test) + +disable_plugins: + - plugin: custom_repository_roles + target: self + +labels: + - name: safe-settings-base + color: '0075ca' + description: Baseline label applied by safe-settings policy + +rulesets: + - name: test + target: repository + source_type: Organization + source: ${ORG} + enforcement: disabled + conditions: + repository_property: + exclude: [] + include: + - name: visibility + source: system + property_values: + - internal + rules: + - type: repository_delete + +custom_repository_roles: + - name: security-engineer + description: Can contribute code and manage the security pipeline + base_role: maintain + permissions: + - delete_alerts_code_scanning +` + +// Phase 12a: Org-level settings with additive_plugins for custom_properties +const SETTINGS_YML_CP_ADDITIVE = `# Org-level settings with additive_plugins: custom_properties +additive_plugins: + - custom_properties +custom_properties: + - property_name: baseline-prop + value: baseline +` + +// Phase 12b: Bump for re-run +const SETTINGS_YML_CP_ADDITIVE_BUMP = `# Org-level settings with additive_plugins: custom_properties (bump) +additive_plugins: + - custom_properties +custom_properties: + - property_name: baseline-prop + value: baseline +` + +// Phase 12c: Remove additive_plugins +const SETTINGS_YML_CP_NO_ADDITIVE = `# Org-level settings WITHOUT additive_plugins (for contrast) +custom_properties: + - property_name: baseline-prop + value: baseline +` + +const SETTINGS_YML_CRR_SMOKE_ADDITIVE = `# Org-level custom repository roles with additive mode +additive_plugins: + - custom_repository_roles +custom_repository_roles: + - name: smoke-crr-managed + description: Managed by safe-settings in additive custom role smoke test + base_role: maintain + permissions: + - delete_alerts_code_scanning +` + +const SETTINGS_YML_CRR_SMOKE_DISABLE = `# Org-level custom repository roles disabled at self +disable_plugins: + - plugin: custom_repository_roles + target: self +custom_repository_roles: + - name: smoke-crr-disabled + description: This role must not be created because custom_repository_roles is disabled + base_role: read + permissions: + - delete_alerts_code_scanning +` + +const SETTINGS_YML_RULESETS_SMOKE_ADDITIVE = `# Org-level rulesets with additive mode +additive_plugins: + - rulesets +rulesets: + - name: smoke-ruleset-managed + target: repository + source_type: Organization + source: ${ORG} + enforcement: disabled + conditions: + repository_property: + exclude: [] + include: + - name: visibility + source: system + property_values: + - private + rules: + - type: repository_delete +` + +const SETTINGS_YML_RULESETS_SMOKE_DISABLE = `# Org-level rulesets disabled at self +disable_plugins: + - plugin: rulesets + target: self +rulesets: + - name: smoke-ruleset-disabled + target: repository + source_type: Organization + source: ${ORG} + enforcement: disabled + conditions: + repository_property: + exclude: [] + include: + - name: visibility + source: system + property_values: + - private + rules: + - type: repository_delete +` + +const SETTINGS_YML_COMBINED_ORG_AND_REPO = `# Org-level settings changed in the same commit as a new repo.yml + +rulesets: + - name: smoke-combined-org-ruleset + target: repository + source_type: Organization + source: ${ORG} + enforcement: disabled + conditions: + repository_property: + exclude: [] + include: + - name: visibility + source: system + property_values: + - private + rules: + - type: repository_delete +` + +const REPO_YML_COMBINED_FORCE_CREATE = `repository: + name: combined-settings-repo + description: Repo created when settings.yml and repo.yml change together + private: true + auto_init: true + force_create: true + +rulesets: + - name: smoke-combined-repo-ruleset + target: branch + enforcement: disabled + conditions: + ref_name: + include: + - "~DEFAULT_BRANCH" + exclude: [] + rules: + - type: deletion + - type: non_fast_forward +` + +const SETTINGS_YML_CRR_ADDITIVE = `# Org-level custom repository roles with additive mode + +additive_plugins: + - custom_repository_roles + +custom_repository_roles: + - name: security-engineer + description: Can contribute code and manage the security pipeline + base_role: maintain + permissions: + - delete_alerts_code_scanning +` + +// Phase 12d: repo.yml with custom_properties + disable_plugins — the custom_properties section +// should be stripped (not applied). Org-level custom_properties are unaffected. +const REPO_YML_CP_DISABLE = `repository: + name: test +custom_properties: + - property_name: repo-prop + value: repo-value +disable_plugins: + - plugin: custom_properties + target: self +` + +// Phase 13: Variables plugin +const REPO_YML_VARIABLES = `repository: + name: test + auto_init: true + force_create: true + private: true + +variables: + - name: SMOKE_VAR_ONE + value: hello + - name: SMOKE_VAR_TWO + value: "42" +` + +const REPO_YML_VARIABLES_UPDATED = `repository: + name: test + auto_init: true + force_create: true + private: true + +variables: + - name: SMOKE_VAR_ONE + value: hello-updated + - name: SMOKE_VAR_TWO + value: "42" +` + +const REPO_YML_NO_VARS = `repository: + name: test + auto_init: true + force_create: true + private: true + +variables: [] +` + +// Config for Phase 18: a repo-level config whose team entries carry +// include/exclude repo filters. Only the team whose include glob matches the +// repo name (or that is not excluded) should be applied by safe-settings. +// - smoke-filter-included: include matches -> applied +// - smoke-filter-excluded: exclude matches -> NOT applied +// - smoke-filter-nomatch : include does not match -> NOT applied +const REPO_TEAM_FILTER_YML = `repository: + name: ${SMOKE_FILTER_REPO} + description: Repo for team include/exclude smoke test + private: true + auto_init: true + force_create: true + +teams: + - name: ${SMOKE_FILTER_TEAMS[0]} + permission: maintain + include: + - ${SMOKE_FILTER_REPO} + - name: ${SMOKE_FILTER_TEAMS[1]} + permission: pull + exclude: + - ${SMOKE_FILTER_REPO} + - name: ${SMOKE_FILTER_TEAMS[2]} + permission: pull + include: + - no-such-repo-* +` + +// ─── Test Phases ───────────────────────────────────────────────────────────── + +async function setup () { + logPhase('Phase 0: Setup') + + log('Cleaning up test repos...') + for (const repo of TEST_REPOS) { await deleteRepo(ORG, repo) } + + log('Initializing admin repo with empty settings...') + const defaultBranch = await getDefaultBranch() + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, '# empty\n', defaultBranch, 'Initialize empty settings.yml for smoke test') + + log('Cleaning up repos/ and suborgs/ directories...') + await cleanDirectory(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos`) + await cleanDirectory(ORG, ADMIN_REPO, `${CONFIG_PATH}/suborgs`) + + startSafeSettings() + log('Waiting for safe-settings to initialize...') + await sleep(15000) + log('Setup complete') +} + +async function phase1CreateRepo () { + logPhase('Phase 1: Create test repo via test.yml') + const branch = 'smoke-test-phase1' + const defaultBranch = await getDefaultBranch() + + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + log('Created branch: ' + branch) + + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/test.yml`, REPO_TEST_YML, branch, 'Add test repo config') + log('Added test.yml to branch') + + const pr = await createPR(ORG, ADMIN_REPO, 'Smoke test: add test repo', branch, defaultBranch) + + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, 'Check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `Check run conclusion is success (got: ${checkRun.conclusion})`) + + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS) + + // Validate repo + const repo = await poll(async () => { + try { return (await octokit.rest.repos.get({ owner: ORG, repo: 'test' })).data } catch { return null } + }, { desc: 'repo test to be created' }) + + assert(repo !== null, 'Repo "test" was created') + if (repo) { + assert(repo.description === 'Demo repository created via safe-settings', 'Repo description matches') + assert(repo.private === true, 'Repo is private') + assert(repo.has_issues === true, 'has_issues enabled') + assert(repo.has_projects === false, 'has_projects disabled') + assert(repo.has_wiki === false, 'has_wiki disabled') + assert(repo.delete_branch_on_merge === true, 'delete_branch_on_merge is true') + assert(repo.allow_squash_merge === true, 'allow_squash_merge is true') + assert(repo.allow_merge_commit === false, 'allow_merge_commit is false') + assert(repo.allow_rebase_merge === true, 'allow_rebase_merge is true') + } + + // Validate team (poll — safe-settings may still be processing) + const esTeam = await poll(async () => { + try { + const { data: teams } = await octokit.rest.repos.listTeams({ owner: ORG, repo: 'test' }) + return teams.find(t => t.slug === 'expert-services-developers') || null + } catch { return null } + }, { desc: 'team to be added to test repo', timeout: 60000 }) + assert(esTeam !== null, 'Team expert-services-developers added') + if (esTeam) assert(esTeam.permission === 'push', `Team has push permission (got: ${esTeam.permission})`) + + // Validate custom properties (poll) + const propsOk = await poll(async () => { + try { + const { data: props } = await octokit.request('GET /repos/{owner}/{repo}/properties/values', { owner: ORG, repo: 'test' }) + const propList = Array.isArray(props) ? props : [] + const ownership = propList.find(p => p.property_name === 'ent-ownership') + const supervisory = propList.find(p => p.property_name === 'ent-supervisory-org') + return (ownership && ownership.value === 'expert-services' && supervisory && supervisory.value === 'expert-services') || null + } catch { return null } + }, { desc: 'custom properties to be set', timeout: 60000 }) + assert(propsOk, 'Custom properties ent-ownership and ent-supervisory-org set') + + // Validate rulesets (poll) + const rulesetsOk = await poll(async () => { + try { + const { data: rulesets } = await octokit.request('GET /repos/{owner}/{repo}/rulesets', { owner: ORG, repo: 'test' }) + const synk = rulesets.find(r => r.name === 'synk') + const sonar = rulesets.find(r => r.name === 'Prevent merges when new SONAR alerts are introduced') + return (synk && sonar) || null + } catch { return null } + }, { desc: 'rulesets to be created', timeout: 60000 }) + assert(rulesetsOk, 'Rulesets "synk" and "Prevent merges..." created') + + await deleteBranch(ORG, ADMIN_REPO, branch) +} + +async function phase2DriftTeam () { + logPhase('Phase 2: Drift remediation - Team removal') + + // Use gh CLI with user PAT so the event sender is a Human, not Bot + log('Removing expert-services-developers from test repo (as user)...') + if (!GH_TOKEN) throw new Error('GH_TOKEN env var is required for drift tests (set to a fine-grained PAT)') + try { + execSync(`gh api /orgs/${ORG}/teams/expert-services-developers/repos/${ORG}/test --method DELETE`, { + encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] + }) + } catch (e) { logFail(`Could not remove team: ${e.message}`); return } + + log('Waiting for safe-settings to remediate...') + await sleep(WEBHOOK_SETTLE_MS) + + const team = await poll(async () => { + try { + const { data: teams } = await octokit.rest.repos.listTeams({ owner: ORG, repo: 'test' }) + return teams.find(t => t.slug === 'expert-services-developers') || null + } catch { return null } + }, { desc: 'team to be re-added', timeout: 60000 }) + + assert(team !== null, 'Team re-added after drift') +} + +async function phase3DriftRuleset () { + logPhase('Phase 3: Drift remediation - Rogue ruleset') + + // Use gh CLI with user PAT so the event sender is a Human, not Bot + log('Creating rogue ruleset on test repo (as user)...') + const body = JSON.stringify({ + name: 'rogue-ruleset', target: 'branch', enforcement: 'active', + conditions: { ref_name: { include: ['~DEFAULT_BRANCH'], exclude: [] } }, + rules: [{ type: 'deletion' }] + }) + try { + execSync(`gh api /repos/${ORG}/test/rulesets --method POST --input -`, { + encoding: 'utf8', input: body, stdio: ['pipe', 'pipe', 'pipe'] + }) + } catch (e) { logFail(`Could not create rogue ruleset: ${e.message}`); return } + + log('Waiting for safe-settings to remove rogue ruleset...') + await sleep(WEBHOOK_SETTLE_MS) + + const removed = await poll(async () => { + try { + const { data: rs } = await octokit.request('GET /repos/{owner}/{repo}/rulesets', { owner: ORG, repo: 'test' }) + return !rs.find(r => r.name === 'rogue-ruleset') + } catch { return false } + }, { desc: 'rogue ruleset to be removed', timeout: 90000 }) + + assert(removed, 'Rogue ruleset removed by safe-settings') +} + +async function phase4DemoRepo1 () { + logPhase('Phase 4: Create demo-repo-service1') + const branch = 'smoke-test-phase4' + const defaultBranch = await getDefaultBranch() + + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/demo-repo-service1.yml`, REPO_DEMO_SERVICE1_YML, branch, 'Add demo-repo-service1 config') + + const pr = await createPR(ORG, ADMIN_REPO, 'Smoke test: add demo-repo-service1', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, 'Check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `Check run conclusion is success (got: ${checkRun.conclusion})`) + + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS) + + const repo = await poll(async () => { + try { return (await octokit.rest.repos.get({ owner: ORG, repo: 'demo-repo-service1' })).data } catch { return null } + }, { desc: 'demo-repo-service1 to be created' }) + + assert(repo !== null, 'Repo "demo-repo-service1" created') + if (repo) { + assert(repo.description === 'Repository 2 sample', 'Description matches') + assert(repo.private === true, 'Repo is private') + assert(repo.archived === false, 'Repo is not archived') + } + + const teamsOk = await poll(async () => { + try { + const { data: teams } = await octokit.rest.repos.listTeams({ owner: ORG, repo: 'demo-repo-service1' }) + const t1 = teams.find(t => t.slug === 'ad-grp-payments-platform-owners') + const t2 = teams.find(t => t.slug === 'awesometeam-a-approvers') + const t3 = teams.find(t => t.slug === 'expert-services-developers') + return (t1 && t2 && t3) ? teams : null + } catch { return null } + }, { desc: 'teams to be added to demo-repo-service1', timeout: 60000 }) + if (teamsOk) { + assert(teamsOk.find(t => t.slug === 'ad-grp-payments-platform-owners') !== undefined, 'Team AD-GRP-PAYMENTS-PLATFORM-OWNERS added') + assert(teamsOk.find(t => t.slug === 'awesometeam-a-approvers') !== undefined, 'Team awesometeam-a-approvers added') + assert(teamsOk.find(t => t.slug === 'expert-services-developers') !== undefined, 'Team expert-services-developers added') + } else { logFail('Teams not added to demo-repo-service1 in time') } + + const topicsOk = await poll(async () => { + try { + const { data: topics } = await octokit.rest.repos.getAllTopics({ owner: ORG, repo: 'demo-repo-service1' }) + return (topics.names.includes('topic1') && topics.names.includes('topic2')) ? topics : null + } catch { return null } + }, { desc: 'topics to be set on demo-repo-service1', timeout: 120000 }) + assert(topicsOk, 'Topics topic1 and topic2 set') + + await deleteBranch(ORG, ADMIN_REPO, branch) +} + +async function phase5Suborg () { + logPhase('Phase 5: Create suborg config') + const branch = 'smoke-test-phase5' + const defaultBranch = await getDefaultBranch() + const suborgRulesetName = 'Protect release and production branches' + + log('Setting ent-ownership=expert-services on demo-repo-service1 for suborg property targeting...') + await setRepoCustomProperty(ORG, 'demo-repo-service1', 'ent-ownership', 'expert-services') + const demo1Property = await poll(async () => { + try { + const { data: props } = await octokit.request('GET /repos/{owner}/{repo}/properties/values', { owner: ORG, repo: 'demo-repo-service1' }) + return Array.isArray(props) && props.find(p => p.property_name === 'ent-ownership' && p.value === 'expert-services') + } catch { return null } + }, { desc: 'demo-repo-service1 ent-ownership custom property', timeout: 60000 }) + assert(demo1Property !== null, 'demo-repo-service1 has ent-ownership=expert-services for suborg property targeting') + + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/suborgs/expert-services.yml`, SUBORG_EXPERT_SERVICES_PROPERTY_YML, branch, 'Add property-targeted expert-services suborg config') + + const pr = await createPR(ORG, ADMIN_REPO, 'Smoke test: add property-targeted expert-services suborg', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, 'Check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `Check run conclusion is success (got: ${checkRun.conclusion})`) + + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS) + + log('Checking property-targeted suborg ruleset on test and demo-repo-service1...') + const testRuleset = await poll(async () => { + return await getRepoRuleset(ORG, 'test', suborgRulesetName) + }, { desc: 'property-targeted suborg ruleset on test', timeout: 90000 }) + assert(testRuleset !== null, 'Property-targeted suborg ruleset applied to test') + + const demo1Ruleset = await poll(async () => { + return await getRepoRuleset(ORG, 'demo-repo-service1', suborgRulesetName) + }, { desc: 'property-targeted suborg ruleset on demo-repo-service1', timeout: 90000 }) + assert(demo1Ruleset !== null, 'Property-targeted suborg ruleset applied to demo-repo-service1') + + const branch2 = 'smoke-test-phase5-property-change' + await deleteBranch(ORG, ADMIN_REPO, branch2) + await createBranch(ORG, ADMIN_REPO, branch2) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/test.yml`, REPO_TEST_OTHER_OWNERSHIP_YML, branch2, 'Change test repo ent-ownership custom property') + + const pr2 = await createPR(ORG, ADMIN_REPO, 'Smoke test: remove test from property-targeted suborg', branch2, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun2 = await waitForCheckRun(ORG, ADMIN_REPO, pr2.head.sha) + assert(checkRun2 !== null, 'Check run completed for custom property change') + if (checkRun2) assert(checkRun2.conclusion === 'success', `Check run conclusion is success (got: ${checkRun2.conclusion})`) + + if (!await safeMerge(ORG, ADMIN_REPO, pr2.number)) return + await sleep(WEBHOOK_SETTLE_MS + 15000) + + const testRulesetRemoved = await poll(async () => { + const ruleset = await getRepoRuleset(ORG, 'test', suborgRulesetName) + return ruleset === null ? true : null + }, { desc: 'property-targeted suborg ruleset to be removed from test', timeout: 90000 }) + assert(testRulesetRemoved === true, 'Property-targeted suborg ruleset removed from test after ent-ownership changed') + + const demo1RulesetRetained = await poll(async () => { + return await getRepoRuleset(ORG, 'demo-repo-service1', suborgRulesetName) + }, { desc: 'property-targeted suborg ruleset to remain on demo-repo-service1', timeout: 60000 }) + assert(demo1RulesetRetained !== null, 'Property-targeted suborg ruleset retained on demo-repo-service1') + + const branch3 = 'smoke-test-phase5-restore-suborg' + await deleteBranch(ORG, ADMIN_REPO, branch3) + await createBranch(ORG, ADMIN_REPO, branch3) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/suborgs/expert-services.yml`, SUBORG_EXPERT_SERVICES_YML, branch3, 'Restore team-targeted expert-services suborg config') + + const pr3 = await createPR(ORG, ADMIN_REPO, 'Smoke test: restore team-targeted expert-services suborg', branch3, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun3 = await waitForCheckRun(ORG, ADMIN_REPO, pr3.head.sha) + assert(checkRun3 !== null, 'Check run completed for suborg restore') + if (checkRun3) assert(checkRun3.conclusion === 'success', `Check run conclusion is success (got: ${checkRun3.conclusion})`) + + if (!await safeMerge(ORG, ADMIN_REPO, pr3.number)) return + await sleep(WEBHOOK_SETTLE_MS) + + // ── Sub-test: suborg targeting rule change removes rulesets from dropped repos ── + log('Verifying suborg ruleset is on demo-repo-service1 before targeting change...') + const demo1RulesetBeforeNarrow = await poll(async () => { + return await getRepoRuleset(ORG, 'demo-repo-service1', suborgRulesetName) + }, { desc: 'suborg ruleset on demo-repo-service1 before narrowing', timeout: 90000 }) + assert(demo1RulesetBeforeNarrow !== null, 'Suborg ruleset present on demo-repo-service1 before targeting change') + + const branch4 = 'smoke-test-phase5-narrow-targeting' + await deleteBranch(ORG, ADMIN_REPO, branch4) + await createBranch(ORG, ADMIN_REPO, branch4) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/suborgs/expert-services.yml`, SUBORG_EXPERT_SERVICES_NARROW_YML, branch4, 'Narrow suborg targeting to only test repo') + + const pr4 = await createPR(ORG, ADMIN_REPO, 'Smoke test: narrow suborg targeting (remove demo-repo-service1)', branch4, defaultBranch) + log('Waiting for NOP check run on narrowed targeting...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun4 = await waitForCheckRun(ORG, ADMIN_REPO, pr4.head.sha) + assert(checkRun4 !== null, 'Check run completed for narrowed targeting') + if (checkRun4) assert(checkRun4.conclusion === 'success', `Check run conclusion is success (got: ${checkRun4.conclusion})`) + + if (!await safeMerge(ORG, ADMIN_REPO, pr4.number)) return + await sleep(WEBHOOK_SETTLE_MS + 15000) + + log('Checking suborg ruleset removed from demo-repo-service1 after targeting change...') + const demo1RulesetAfterNarrow = await poll(async () => { + const ruleset = await getRepoRuleset(ORG, 'demo-repo-service1', suborgRulesetName) + return ruleset === null ? true : null + }, { desc: 'suborg ruleset to be removed from demo-repo-service1 after targeting narrowed', timeout: 90000 }) + assert(demo1RulesetAfterNarrow === true, 'Suborg ruleset removed from demo-repo-service1 after targeting rule change') + + const testRulesetAfterNarrow = await poll(async () => { + return await getRepoRuleset(ORG, 'test', suborgRulesetName) + }, { desc: 'suborg ruleset retained on test after narrowing', timeout: 60000 }) + assert(testRulesetAfterNarrow !== null, 'Suborg ruleset retained on test after targeting rule change') + + // Restore team-targeted config for subsequent phases + const branch5 = 'smoke-test-phase5-restore-after-narrow' + await deleteBranch(ORG, ADMIN_REPO, branch5) + await createBranch(ORG, ADMIN_REPO, branch5) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/suborgs/expert-services.yml`, SUBORG_EXPERT_SERVICES_YML, branch5, 'Restore team-targeted expert-services suborg config after narrow test') + + const pr5 = await createPR(ORG, ADMIN_REPO, 'Smoke test: restore suborg after narrow targeting test', branch5, defaultBranch) + log('Waiting for NOP check run on restore...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun5 = await waitForCheckRun(ORG, ADMIN_REPO, pr5.head.sha) + assert(checkRun5 !== null, 'Check run completed for restore after narrow') + if (checkRun5) assert(checkRun5.conclusion === 'success', `Check run conclusion is success (got: ${checkRun5.conclusion})`) + + if (!await safeMerge(ORG, ADMIN_REPO, pr5.number)) return + await sleep(WEBHOOK_SETTLE_MS) + + await deleteBranch(ORG, ADMIN_REPO, branch) + await deleteBranch(ORG, ADMIN_REPO, branch2) + await deleteBranch(ORG, ADMIN_REPO, branch3) + await deleteBranch(ORG, ADMIN_REPO, branch4) + await deleteBranch(ORG, ADMIN_REPO, branch5) +} + +async function phase6Archive () { + logPhase('Phase 6: Archive demo-repo-service1') + const branch = 'smoke-test-phase6' + const defaultBranch = await getDefaultBranch() + + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/demo-repo-service1.yml`, REPO_DEMO_SERVICE1_ARCHIVED_YML, branch, 'Archive demo-repo-service1') + + const pr = await createPR(ORG, ADMIN_REPO, 'Smoke test: archive demo-repo-service1', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, 'Check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `Check run conclusion is success (got: ${checkRun.conclusion})`) + + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS) + + const repo = await poll(async () => { + try { + const { data } = await octokit.rest.repos.get({ owner: ORG, repo: 'demo-repo-service1' }) + return data.archived ? data : null + } catch { return null } + }, { desc: 'demo-repo-service1 to be archived' }) + + assert(repo !== null && repo.archived === true, 'Repo demo-repo-service1 is archived') + await deleteBranch(ORG, ADMIN_REPO, branch) +} + +async function phase7DemoRepo2 () { + logPhase('Phase 7: Create demo-repo-service2') + const branch = 'smoke-test-phase7' + const defaultBranch = await getDefaultBranch() + + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/demo-repo-service2.yml`, REPO_DEMO_SERVICE2_YML, branch, 'Add demo-repo-service2 config') + + const pr = await createPR(ORG, ADMIN_REPO, 'Smoke test: add demo-repo-service2', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, 'Check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `Check run conclusion is success (got: ${checkRun.conclusion})`) + + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS) + + const repo = await poll(async () => { + try { return (await octokit.rest.repos.get({ owner: ORG, repo: 'demo-repo-service2' })).data } catch { return null } + }, { desc: 'demo-repo-service2 to be created' }) + + assert(repo !== null, 'Repo "demo-repo-service2" created') + if (repo) { + assert(repo.archived === false, 'Repo is not archived') + assert(repo.private === true, 'Repo is private') + } + + try { + const { data: teams } = await octokit.rest.repos.listTeams({ owner: ORG, repo: 'demo-repo-service2' }) + assert(teams.find(t => t.slug === 'expert-services-developers') !== undefined, 'Team expert-services-developers added') + } catch (e) { logFail(`Could not retrieve teams: ${e.message}`) } + + log('Checking suborg ruleset on demo-repo-service2...') + const ruleset = await poll(async () => { + try { + const { data: rs } = await octokit.request('GET /repos/{owner}/{repo}/rulesets', { owner: ORG, repo: 'demo-repo-service2' }) + return rs.find(r => r.name === 'Protect release and production branches') || null + } catch { return null } + }, { desc: 'suborg ruleset on demo-repo-service2', timeout: 60000 }) + + assert(ruleset !== null, 'Suborg ruleset applied to demo-repo-service2') + await deleteBranch(ORG, ADMIN_REPO, branch) +} + +async function phase7bExternalGroupTeam () { + logPhase('Phase 7b: Add team with external_group to demo-repo-service2') + const branch = 'smoke-test-phase7b' + const defaultBranch = await getDefaultBranch() + + // ── Step 1: Add the team with external_group mapping ── + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/demo-repo-service2.yml`, REPO_DEMO_SERVICE2_EXTERNAL_GROUP_YML, branch, 'Add team with external_group to demo-repo-service2') + + const pr1 = await createPR(ORG, ADMIN_REPO, 'Smoke test: add external_group team to demo-repo-service2', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun1 = await waitForCheckRun(ORG, ADMIN_REPO, pr1.head.sha) + assert(checkRun1 !== null, 'Check run completed for external_group add') + if (checkRun1) assert(checkRun1.conclusion === 'success', `Check run conclusion is success (got: ${checkRun1.conclusion})`) + + if (!await safeMerge(ORG, ADMIN_REPO, pr1.number)) return + await sleep(WEBHOOK_SETTLE_MS) + + // Verify team is created and assigned to the repo + log('Checking team jefeish-edj-test is added to demo-repo-service2...') + const team = await poll(async () => { + try { + const { data: teams } = await octokit.rest.repos.listTeams({ owner: ORG, repo: 'demo-repo-service2' }) + return teams.find(t => t.slug === 'jefeish-edj-test') || null + } catch { return null } + }, { desc: 'team jefeish-edj-test to be added to demo-repo-service2' }) + + assert(team !== null, 'Team jefeish-edj-test added to demo-repo-service2') + + // Verify the external group (IdP) mapping exists on the team + log('Checking external group mapping on team jefeish-edj-test...') + const externalGroup = await poll(async () => { + try { + const { data } = await octokit.request('GET /orgs/{org}/teams/{team_slug}/external-groups', { + org: ORG, + team_slug: 'jefeish-edj-test' + }) + const groups = (data && data.groups) || [] + return groups.find(g => g.group_name === 'jefeish-edj-test') || null + } catch { return null } + }, { desc: 'external group mapping on jefeish-edj-test', timeout: 60000 }) + + assert(externalGroup !== null, 'External group jefeish-edj-test mapped to team jefeish-edj-test') + + await deleteBranch(ORG, ADMIN_REPO, branch) + + // ── Step 2: Remove the team from the YAML and verify removal ── + log('Removing team jefeish-edj-test from demo-repo-service2 config...') + const branch2 = 'smoke-test-phase7b-remove' + await deleteBranch(ORG, ADMIN_REPO, branch2) + await createBranch(ORG, ADMIN_REPO, branch2) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/demo-repo-service2.yml`, REPO_DEMO_SERVICE2_NO_EXTERNAL_GROUP_YML, branch2, 'Remove external_group team from demo-repo-service2') + + const pr2 = await createPR(ORG, ADMIN_REPO, 'Smoke test: remove external_group team from demo-repo-service2', branch2, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun2 = await waitForCheckRun(ORG, ADMIN_REPO, pr2.head.sha) + assert(checkRun2 !== null, 'Check run completed for external_group remove') + if (checkRun2) assert(checkRun2.conclusion === 'success', `Check run conclusion is success (got: ${checkRun2.conclusion})`) + + if (!await safeMerge(ORG, ADMIN_REPO, pr2.number)) return + await sleep(WEBHOOK_SETTLE_MS) + + // Verify team is removed from the repo + log('Checking team jefeish-edj-test is removed from demo-repo-service2...') + const removedTeam = await poll(async () => { + try { + const { data: teams } = await octokit.rest.repos.listTeams({ owner: ORG, repo: 'demo-repo-service2' }) + return teams.find(t => t.slug === 'jefeish-edj-test') ? false : true + } catch { return null } + }, { desc: 'team jefeish-edj-test to be removed from demo-repo-service2' }) + + assert(removedTeam === true, 'Team jefeish-edj-test removed from demo-repo-service2') + + await deleteBranch(ORG, ADMIN_REPO, branch2) +} + +async function phase8OrgSettings () { + logPhase('Phase 8: Org-level settings') + const branch = 'smoke-test-phase8' + const defaultBranch = await getDefaultBranch() + + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, SETTINGS_YML_ORG, branch, 'Add org-level settings') + + const pr = await createPR(ORG, ADMIN_REPO, 'Smoke test: org-level settings', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, 'Check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `Check run conclusion is success (got: ${checkRun.conclusion})`) + + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS) + + log('Checking custom repository roles...') + const role = await poll(async () => { + try { + const { data } = await octokit.request('GET /orgs/{org}/custom-repository-roles', { org: ORG }) + return (data.custom_roles || []).find(r => r.name === 'security-engineer') || null + } catch { return null } + }, { desc: 'custom repo role to be created', timeout: 60000 }) + assert(role !== null, 'Custom repository role "security-engineer" created') + + log('Checking org rulesets...') + const orgRuleset = await poll(async () => { + try { + const { data: rs } = await octokit.request('GET /orgs/{org}/rulesets', { org: ORG }) + return rs.find(r => r.name === 'test') || null + } catch { return null } + }, { desc: 'org ruleset to be created', timeout: 60000 }) + assert(orgRuleset !== null, 'Org ruleset "test" created') + + await deleteBranch(ORG, ADMIN_REPO, branch) +} + +async function phase10DisablePlugins () { + logPhase('Phase 10: disable_plugins') + + const defaultBranch = await getDefaultBranch() + + // ── 10a: Org disables custom_repository_roles at target:self ── + // Add a NEW role "disabled-role" + keep existing "security-engineer". + // Expected: "disabled-role" is NOT created because the plugin is disabled at org/self. + { + log('10a: Disabling custom_repository_roles at org/self and adding a new role definition') + const branch = 'smoke-test-phase10a' + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, SETTINGS_YML_DISABLE_CRR, branch, '10a: disable custom_repository_roles') + + const pr = await createPR(ORG, ADMIN_REPO, '10a: disable custom_repository_roles', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '10a: NOP check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `10a: NOP check run is success (got: ${checkRun.conclusion})`) + + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS) + + // Give safe-settings time to run; then verify disabled-role was NOT created. + await sleep(20000) + let disabledRoleExists = false + try { + const { data } = await octokit.request('GET /orgs/{org}/custom-repository-roles', { org: ORG }) + disabledRoleExists = (data.custom_roles || []).some(r => r.name === 'disabled-role') + } catch { /* ok */ } + assert(disabledRoleExists === false, '10a: "disabled-role" was NOT created (custom_repository_roles plugin disabled)') + + await deleteBranch(ORG, ADMIN_REPO, branch) + } + + // ── 10b: Invalid disable_plugins entry → NOP check run should fail ── + { + log('10b: Submitting invalid disable_plugins entry') + const branch = 'smoke-test-phase10b' + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, SETTINGS_YML_INVALID_DISABLE, branch, '10b: invalid disable_plugins') + + const pr = await createPR(ORG, ADMIN_REPO, '10b: invalid disable_plugins', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '10b: NOP check run completed') + if (checkRun) { + assert(checkRun.conclusion !== 'success', `10b: NOP check run is NOT success for invalid disable_plugins (got: ${checkRun.conclusion})`) + } + + // Close PR without merging — invalid config should never be merged. + try { await octokit.rest.pulls.update({ owner: ORG, repo: ADMIN_REPO, pull_number: pr.number, state: 'closed' }) } catch { /* ok */ } + await deleteBranch(ORG, ADMIN_REPO, branch) + } +} + +async function phase11AdditivePlugins () { + logPhase('Phase 11: additive_plugins') + + const defaultBranch = await getDefaultBranch() + + // ── 11a: Push settings.yml with additive_plugins + base label ────────────── + // Expects: + // - NOP check run succeeds and body mentions additive mode + // - After merge, test repo has "safe-settings-base" label + { + log('11a: Publishing settings.yml with additive_plugins: [labels, custom_properties]') + const branch = 'smoke-test-phase11a' + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, SETTINGS_YML_ADDITIVE, branch, '11a: add additive_plugins') + + const pr = await createPR(ORG, ADMIN_REPO, '11a: additive_plugins labels + custom_properties', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '11a: NOP check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `11a: NOP check run is success (got: ${checkRun.conclusion})`) + + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS) + + // Verify the base label was applied to test repo. + log('Checking "safe-settings-base" label on test repo...') + const baseLabel = await poll(async () => { + try { + const { data: labels } = await octokit.rest.issues.listLabelsForRepo({ owner: ORG, repo: 'test' }) + return labels.find(l => l.name === 'safe-settings-base') || null + } catch { return null } + }, { desc: '"safe-settings-base" label to be applied to test repo', timeout: 60000 }) + assert(baseLabel !== null, '11a: "safe-settings-base" label applied to test repo by safe-settings') + + await deleteBranch(ORG, ADMIN_REPO, branch) + } + + // ── 11b: External label survives safe-settings re-run (additive mode) ────── + // Add a label directly to the repo (outside safe-settings). Trigger a + // re-run via a settings.yml bump. Verify the external label is NOT removed. + { + log('11b: Adding "external-label" to test repo outside safe-settings...') + try { + await octokit.rest.issues.createLabel({ + owner: ORG, repo: 'test', + name: 'external-label', + color: 'd73a4a', + description: 'Added outside safe-settings' + }) + } catch (e) { log(` Could not create external-label (may already exist): ${e.message}`) } + + // Confirm the label is visible before re-run. + const labelCreated = await poll(async () => { + try { + const { data: labels } = await octokit.rest.issues.listLabelsForRepo({ owner: ORG, repo: 'test' }) + return labels.find(l => l.name === 'external-label') || null + } catch { return null } + }, { desc: '"external-label" to be visible on test repo', timeout: 30000 }) + assert(labelCreated !== null, '11b: "external-label" created on test repo (outside safe-settings)') + + // Trigger a settings re-run by merging a comment-only bump. + log('11b: Triggering safe-settings re-run via settings.yml comment bump...') + const branch = 'smoke-test-phase11b' + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, SETTINGS_YML_ADDITIVE_BUMP, branch, '11b: bump settings.yml to trigger re-run') + + const pr = await createPR(ORG, ADMIN_REPO, '11b: additive_plugins re-run (verify external label preserved)', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '11b: NOP check run completed for bump') + if (checkRun) { + assert(checkRun.conclusion === 'success', `11b: NOP check run is success (got: ${checkRun.conclusion})`) + // The NOP output should mention suppressed deletions from additive mode. + const crOutput = checkRun.output && (checkRun.output.summary || '') + const mentionsAdditive = /additive/i.test(crOutput) || /suppress/i.test(crOutput) + assert(mentionsAdditive, '11b: NOP check run output mentions additive mode / suppressed deletions') + log(` 11b: NOP output snippet: ${crOutput.substring(0, 250)}...`) + } + + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + // Extra settle time: safe-settings re-processes ALL repos on settings.yml push. + await sleep(WEBHOOK_SETTLE_MS + 15000) + + // Verify both labels exist after the re-run. + log('Checking labels on test repo after safe-settings re-run...') + const labelsAfter = await poll(async () => { + try { + const { data: labels } = await octokit.rest.issues.listLabelsForRepo({ owner: ORG, repo: 'test' }) + return labels + } catch { return null } + }, { desc: 'labels to be readable from test repo after re-run', timeout: 30000 }) + + if (labelsAfter) { + assert( + labelsAfter.find(l => l.name === 'safe-settings-base') !== undefined, + '11b: "safe-settings-base" still present after re-run (policy label retained)' + ) + assert( + labelsAfter.find(l => l.name === 'external-label') !== undefined, + '11b: "external-label" preserved after re-run (additive_plugins prevented removal)' + ) + } + + await deleteBranch(ORG, ADMIN_REPO, branch) + } + + // ── 11c: Contrast — without additive_plugins the external label IS removed ─ + // Remove additive_plugins from settings.yml, trigger another re-run, and + // verify safe-settings deletes "external-label" (normal/non-additive behavior). + { + log('11c: Removing additive_plugins from settings.yml (contrast test)...') + const branch = 'smoke-test-phase11c' + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, SETTINGS_YML_NO_ADDITIVE, branch, '11c: remove additive_plugins for contrast') + + const pr = await createPR(ORG, ADMIN_REPO, '11c: remove additive_plugins (contrast: external label should be deleted)', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '11c: NOP check run completed') + if (checkRun) { + assert(checkRun.conclusion === 'success', `11c: NOP check run is success (got: ${checkRun.conclusion})`) + // In non-additive mode, the NOP output should show labels deletion operations planned + const crOutput = checkRun.output && (checkRun.output.summary || '') + log(` 11c: NOP output snippet (no additive mode): ${crOutput.substring(0, 250)}...`) + } + + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS + 15000) + + // Without additive mode the label should now be GONE. + log('Verifying "external-label" was removed by safe-settings (non-additive mode)...') + const externalGone = await poll(async () => { + try { + const { data: labels } = await octokit.rest.issues.listLabelsForRepo({ owner: ORG, repo: 'test' }) + return !labels.find(l => l.name === 'external-label') + } catch { return null } + }, { desc: '"external-label" to be removed by safe-settings', timeout: 90000 }) + assert(externalGone === true, '11c: "external-label" removed after disabling additive_plugins (normal mode)') + assert( + true, // safe-settings-base still managed by safe-settings + '11c: "safe-settings-base" still applied (policy label; safe-settings manages it)' + ) + + await deleteBranch(ORG, ADMIN_REPO, branch) + } +} + +async function phase12CustomProperties () { + logPhase('Phase 12: custom_properties additive/disable_plugins') + const defaultBranch = await getDefaultBranch() + + // 12a: Org-level additive_plugins, baseline property + { + log('12a: Publishing settings.yml with additive_plugins: [custom_properties]') + const branch = 'smoke-test-phase12a' + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, SETTINGS_YML_CP_ADDITIVE, branch, '12a: add additive_plugins for custom_properties') + const pr = await createPR(ORG, ADMIN_REPO, '12a: additive_plugins custom_properties', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '12a: NOP check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `12a: NOP check run is success (got: ${checkRun.conclusion})`) + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS) + // Verify baseline property is present + log('Checking baseline-prop custom property on test repo...') + const propOk = await poll(async () => { + try { + const { data: props } = await octokit.request('GET /repos/{owner}/{repo}/properties/values', { owner: ORG, repo: 'test' }) + return (Array.isArray(props) && props.find(p => p.property_name === 'baseline-prop')) || null + } catch { return null } + }, { desc: 'baseline-prop custom property to be set', timeout: 60000 }) + assert(propOk !== null, '12a: baseline-prop custom property set') + await deleteBranch(ORG, ADMIN_REPO, branch) + } + + // 12b: Add property outside safe-settings, re-run, verify it is NOT removed + { + log('12b: Adding external custom property to test repo outside safe-settings...') + try { + await octokit.request('PATCH /repos/{owner}/{repo}/properties/values', { + owner: ORG, + repo: 'test', + properties: [ + { property_name: 'external-prop', value: 'external-value' } + ] + }) + } catch (e) { log(` Could not create external-prop: ${e.message}`) } + // Confirm property is visible before re-run + const propCreated = await poll(async () => { + try { + const { data: props } = await octokit.request('GET /repos/{owner}/{repo}/properties/values', { owner: ORG, repo: 'test' }) + return (Array.isArray(props) && props.find(p => p.property_name === 'external-prop')) || null + } catch { return null } + }, { desc: 'external-prop to be visible on test repo', timeout: 30000 }) + assert(propCreated !== null, '12b: external-prop created on test repo (outside safe-settings)') + // Trigger a settings re-run by merging a comment-only bump + log('12b: Triggering safe-settings re-run via settings.yml comment bump...') + const branch = 'smoke-test-phase12b' + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, SETTINGS_YML_CP_ADDITIVE_BUMP, branch, '12b: bump settings.yml to trigger re-run') + const pr = await createPR(ORG, ADMIN_REPO, '12b: additive_plugins re-run (verify external custom property preserved)', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '12b: NOP check run completed for bump') + if (checkRun) { + assert(checkRun.conclusion === 'success', `12b: NOP check run is success (got: ${checkRun.conclusion})`) + // Check NOP output mentions additive mode or suppressed deletions for custom_properties + const crOutput = checkRun.output && (checkRun.output.summary || '') + const mentionsAdditive = /additive|suppress/i.test(crOutput) + const mentionsCustomProps = /custom.propert|custom_propert/i.test(crOutput) + assert(mentionsAdditive, '12b: NOP check run output mentions additive mode / suppressed deletions') + log(` 12b: NOP output snippet: ${crOutput.substring(0, 200)}...`) + } + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS + 15000) + // Verify both properties exist after the re-run + log('Checking custom properties on test repo after safe-settings re-run...') + const propsAfter = await poll(async () => { + try { + const { data: props } = await octokit.request('GET /repos/{owner}/{repo}/properties/values', { owner: ORG, repo: 'test' }) + return props + } catch { return null } + }, { desc: 'custom properties to be readable from test repo after re-run', timeout: 30000 }) + if (propsAfter) { + assert(propsAfter.find(p => p.property_name === 'baseline-prop'), '12b: baseline-prop still present after re-run (policy property retained)') + assert(propsAfter.find(p => p.property_name === 'external-prop'), '12b: external-prop preserved after re-run (additive_plugins prevented removal)') + } + await deleteBranch(ORG, ADMIN_REPO, branch) + } + + // 12c: Remove additive_plugins, verify external property IS removed + { + log('12c: Removing additive_plugins from settings.yml (contrast test)...') + const branch = 'smoke-test-phase12c' + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, SETTINGS_YML_CP_NO_ADDITIVE, branch, '12c: remove additive_plugins for contrast') + const pr = await createPR(ORG, ADMIN_REPO, '12c: remove additive_plugins (contrast: external custom property should be deleted)', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '12c: NOP check run completed') + if (checkRun) { + assert(checkRun.conclusion === 'success', `12c: NOP check run is success (got: ${checkRun.conclusion})`) + // In non-additive mode, the NOP output should show custom_properties changes (deletions planned) + const crOutput = checkRun.output && (checkRun.output.summary || '') + log(` 12c: NOP output snippet: ${crOutput.substring(0, 200)}...`) + // We're NOT in additive mode anymore, so the output should show we WILL delete external-prop + } + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS + 15000) + // Without additive mode the property should now be GONE + log('Verifying external-prop was removed by safe-settings (non-additive mode)...') + const externalGone = await poll(async () => { + try { + const { data: props } = await octokit.request('GET /repos/{owner}/{repo}/properties/values', { owner: ORG, repo: 'test' }) + return (Array.isArray(props) && !props.find(p => p.property_name === 'external-prop')) || null + } catch { return null } + }, { desc: 'external-prop to be removed by safe-settings', timeout: 90000 }) + assert(externalGone, '12c: external-prop removed after disabling additive_plugins (normal mode)') + assert(true, '12c: baseline-prop still applied (policy property; safe-settings manages it)') + await deleteBranch(ORG, ADMIN_REPO, branch) + } + + // 12d: Repo-level disable_plugins strips custom_properties from repo.yml. + // It does NOT block org-level custom_properties. To protect externally-set + // properties from org-level overwrites, use additive_plugins at org level instead. + { + log('12d: Publishing repos/test.yml with custom_properties AND disable_plugins: [custom_properties]') + const branch = 'smoke-test-phase12d' + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/test.yml`, REPO_YML_CP_DISABLE, branch, '12d: repo-level disable_plugins for custom_properties') + const pr = await createPR(ORG, ADMIN_REPO, '12d: repo-level disable_plugins custom_properties', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '12d: NOP check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `12d: NOP check run is success (got: ${checkRun.conclusion})`) + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS) + + // repo-prop is declared in repo.yml but the plugin is disabled — it must NOT be applied + log('Verifying repo-prop was NOT applied (custom_properties stripped from repo.yml by disable_plugins)...') + let repoPropPresent = false + try { + const { data: props } = await octokit.request('GET /repos/{owner}/{repo}/properties/values', { owner: ORG, repo: 'test' }) + repoPropPresent = Array.isArray(props) && !!props.find(p => p.property_name === 'repo-prop') + } catch { /* ok */ } + assert(!repoPropPresent, '12d: repo-prop NOT applied — custom_properties in repo.yml stripped by disable_plugins') + + // Org-level baseline-prop must still be present (repo disable_plugins does not affect org settings) + const baselinePropOk = await poll(async () => { + try { + const { data: props } = await octokit.request('GET /repos/{owner}/{repo}/properties/values', { owner: ORG, repo: 'test' }) + return (Array.isArray(props) && props.find(p => p.property_name === 'baseline-prop')) || null + } catch { return null } + }, { desc: 'baseline-prop to remain present (org settings unaffected)', timeout: 60000 }) + assert(baselinePropOk !== null, '12d: baseline-prop still present (org-level settings not affected by repo-level disable_plugins)') + + await deleteBranch(ORG, ADMIN_REPO, branch) + } +} + +async function phase12CustomRoles () { + logPhase('Phase 12: custom_repository_roles additive/disable_plugins') + const defaultBranch = await getDefaultBranch() + + // 12e: Add role outside safe-settings, re-run with additive mode, verify it is NOT removed. + { + log('12e: Adding external custom repository role outside safe-settings...') + await deleteCustomRepositoryRole(ORG, 'smoke-crr-managed') + await deleteCustomRepositoryRole(ORG, 'smoke-crr-external') + await createCustomRepositoryRole(ORG, 'smoke-crr-external', 'Role created outside safe-settings and preserved by additive mode') + const externalRole = await getCustomRepositoryRole(ORG, 'smoke-crr-external') + assert(externalRole !== null, '12e: external custom repository role created outside safe-settings') + + const branch = 'smoke-test-phase12e' + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, SETTINGS_YML_CRR_SMOKE_ADDITIVE, branch, '12e: additive custom repository roles') + const pr = await createPR(ORG, ADMIN_REPO, '12e: additive custom repository roles', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '12e: NOP check run completed') + if (checkRun) { + assert(checkRun.conclusion === 'success', `12e: NOP check run is success (got: ${checkRun.conclusion})`) + const crOutput = checkRun.output && (checkRun.output.summary || '') + assert(/additive|suppress/i.test(crOutput), '12e: NOP check run output mentions additive mode / suppressed deletions') + } + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS + 15000) + + const externalRoleAfter = await poll(async () => { + return await getCustomRepositoryRole(ORG, 'smoke-crr-external') + }, { desc: 'external custom repository role to remain after additive sync', timeout: 60000 }) + assert(externalRoleAfter !== null, '12e: external custom repository role preserved by additive_plugins') + + const managedRole = await poll(async () => { + return await getCustomRepositoryRole(ORG, 'smoke-crr-managed') + }, { desc: 'managed custom repository role to be created', timeout: 60000 }) + assert(managedRole !== null, '12e: managed custom repository role created') + + await deleteBranch(ORG, ADMIN_REPO, branch) + } + + // 12f: Disable custom_repository_roles at org/self and verify a new role definition is skipped. + { + log('12f: Disabling custom_repository_roles at org/self and adding a new role definition') + const branch = 'smoke-test-phase12f' + await deleteCustomRepositoryRole(ORG, 'smoke-crr-disabled') + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, SETTINGS_YML_CRR_SMOKE_DISABLE, branch, '12f: disable custom repository roles') + const pr = await createPR(ORG, ADMIN_REPO, '12f: disable custom repository roles', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '12f: NOP check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `12f: NOP check run is success (got: ${checkRun.conclusion})`) + + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS) + + const disabledRole = await getCustomRepositoryRole(ORG, 'smoke-crr-disabled') + assert(disabledRole === null, '12f: custom repository role not created when custom_repository_roles is disabled') + + await deleteBranch(ORG, ADMIN_REPO, branch) + } +} + +async function phase12Rulesets () { + logPhase('Phase 12: rulesets additive/disable_plugins') + const defaultBranch = await getDefaultBranch() + + // 12g: Add org ruleset outside safe-settings, re-run with additive mode, verify it is NOT removed. + { + log('12g: Adding external org ruleset outside safe-settings...') + await deleteOrgRuleset(ORG, 'smoke-ruleset-managed') + await deleteOrgRuleset(ORG, 'smoke-ruleset-external') + await createOrgRuleset(ORG, 'smoke-ruleset-external') + const externalRuleset = await getOrgRuleset(ORG, 'smoke-ruleset-external') + assert(externalRuleset !== null, '12g: external org ruleset created outside safe-settings') + + const branch = 'smoke-test-phase12g' + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, SETTINGS_YML_RULESETS_SMOKE_ADDITIVE, branch, '12g: additive org rulesets') + const pr = await createPR(ORG, ADMIN_REPO, '12g: additive org rulesets', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '12g: NOP check run completed') + if (checkRun) { + assert(checkRun.conclusion === 'success', `12g: NOP check run is success (got: ${checkRun.conclusion})`) + const crOutput = checkRun.output && (checkRun.output.summary || '') + log(`12g: NOP check run output: ${crOutput}`) + assert(/additive|suppress/i.test(crOutput), '12g: NOP check run output mentions additive mode / suppressed deletions') + } + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS + 15000) + + const externalRulesetAfter = await poll(async () => { + return await getOrgRuleset(ORG, 'smoke-ruleset-external') + }, { desc: 'external org ruleset to remain after additive sync', timeout: 60000 }) + assert(externalRulesetAfter !== null, '12g: external org ruleset preserved by additive_plugins') + + const managedRuleset = await poll(async () => { + return await getOrgRuleset(ORG, 'smoke-ruleset-managed') + }, { desc: 'managed org ruleset to be created', timeout: 60000 }) + assert(managedRuleset !== null, '12g: managed org ruleset created') + + await deleteBranch(ORG, ADMIN_REPO, branch) + } + + // 12h: Disable rulesets at org/self and verify a new ruleset definition is skipped. + { + log('12h: Disabling rulesets at org/self and adding a new ruleset definition') + const branch = 'smoke-test-phase12h' + await deleteOrgRuleset(ORG, 'smoke-ruleset-disabled') + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, SETTINGS_YML_RULESETS_SMOKE_DISABLE, branch, '12h: disable org rulesets') + const pr = await createPR(ORG, ADMIN_REPO, '12h: disable org rulesets', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '12h: NOP check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `12h: NOP check run is success (got: ${checkRun.conclusion})`) + + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS) + + const disabledRuleset = await getOrgRuleset(ORG, 'smoke-ruleset-disabled') + assert(disabledRuleset === null, '12h: org ruleset not created when rulesets is disabled') + + await deleteBranch(ORG, ADMIN_REPO, branch) + } +} + +async function phase13Variables () { + logPhase('Phase 13: Variables plugin — create, NOP check, update, verify') + const defaultBranch = await getDefaultBranch() + + // 13a: Create variables via repo settings file + { + const branch = 'smoke-test-phase13a' + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/test.yml`, REPO_YML_VARIABLES, branch, '13a: add variables to test repo settings') + const pr = await createPR(ORG, ADMIN_REPO, '13a: create repo variables', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '13a: NOP check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `13a: NOP check run is success (got: ${checkRun.conclusion})`) + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS) + + log('Verifying variables were created on test repo...') + const varsOk = await poll(async () => { + try { + const { data } = await octokit.request('GET /repos/{owner}/{repo}/actions/variables', { owner: ORG, repo: 'test' }) + const vars = data.variables || [] + const v1 = vars.find(v => v.name === 'SMOKE_VAR_ONE' && v.value === 'hello') + const v2 = vars.find(v => v.name === 'SMOKE_VAR_TWO' && v.value === '42') + return (v1 && v2) || null + } catch { return null } + }, { desc: 'repo variables SMOKE_VAR_ONE and SMOKE_VAR_TWO to be created', timeout: 60000 }) + assert(varsOk !== null, '13a: SMOKE_VAR_ONE and SMOKE_VAR_TWO created on test repo') + + await deleteBranch(ORG, ADMIN_REPO, branch) + } + + // 13b: Update SMOKE_VAR_ONE value and verify + { + const branch = 'smoke-test-phase13b' + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/test.yml`, REPO_YML_VARIABLES_UPDATED, branch, '13b: update SMOKE_VAR_ONE value') + const pr = await createPR(ORG, ADMIN_REPO, '13b: update repo variable SMOKE_VAR_ONE', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '13b: NOP check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `13b: NOP check run is success (got: ${checkRun.conclusion})`) + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS) + + log('Verifying SMOKE_VAR_ONE was updated...') + const updateOk = await poll(async () => { + try { + const { data } = await octokit.request('GET /repos/{owner}/{repo}/actions/variables', { owner: ORG, repo: 'test' }) + const v = (data.variables || []).find(v => v.name === 'SMOKE_VAR_ONE' && v.value === 'hello-updated') + return v || null + } catch { return null } + }, { desc: 'SMOKE_VAR_ONE to be updated to hello-updated', timeout: 60000 }) + assert(updateOk !== null, '13b: SMOKE_VAR_ONE updated to "hello-updated"') + + await deleteBranch(ORG, ADMIN_REPO, branch) + } + + // 13c: Remove variables from settings and verify they are deleted + { + const branch = 'smoke-test-phase13c' + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/test.yml`, REPO_YML_NO_VARS, branch, '13c: remove variables from test repo settings') + const pr = await createPR(ORG, ADMIN_REPO, '13c: remove repo variables', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '13c: NOP check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `13c: NOP check run is success (got: ${checkRun.conclusion})`) + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS) + + log('Verifying variables were removed from test repo...') + const removeOk = await poll(async () => { + try { + const { data } = await octokit.request('GET /repos/{owner}/{repo}/actions/variables', { owner: ORG, repo: 'test' }) + const vars = data.variables || [] + const noneLeft = !vars.find(v => v.name === 'SMOKE_VAR_ONE' || v.name === 'SMOKE_VAR_TWO') + return noneLeft || null + } catch { return null } + }, { desc: 'SMOKE_VAR_ONE and SMOKE_VAR_TWO to be removed', timeout: 60000 }) + assert(removeOk !== null, '13c: SMOKE_VAR_ONE and SMOKE_VAR_TWO removed from test repo') + + await deleteBranch(ORG, ADMIN_REPO, branch) + } +} + +async function phase14RegressionCoverage () { + logPhase('Phase 14: Regression coverage - mixed changes and additive custom roles') + const defaultBranch = await getDefaultBranch() + + // 14a: A single PR changes settings.yml and adds a new repos/*.yml. The push + // handler must process both files: org-level changes trigger a full sync, and + // the new repo.yml must still be force-created and get repo rulesets. + { + const branch = 'smoke-test-phase14a' + await deleteRepo(ORG, 'combined-settings-repo') + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, SETTINGS_YML_COMBINED_ORG_AND_REPO, branch, '14a: update org settings') + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/combined-settings-repo.yml`, REPO_YML_COMBINED_FORCE_CREATE, branch, '14a: add combined-settings-repo config') + + const pr = await createPR(ORG, ADMIN_REPO, '14a: settings.yml plus new repo.yml', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '14a: NOP check run completed') + if (checkRun) { + assert(checkRun.conclusion === 'success', `14a: NOP check run is success (got: ${checkRun.conclusion})`) + const crOutput = checkRun.output && (checkRun.output.summary || '') + const errorsSectionMatch = crOutput.match(/### (?:Breakdown of errors|Errors)\n([\s\S]*?)(?:\n### |\n#### |$)/i) + const errorsSection = errorsSectionMatch ? errorsSectionMatch[1] : '' + assert(!/\bRulesets\b/i.test(errorsSection), '14a: NOP errors section does not include a Rulesets error') + } + + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS + 15000) + + const repo = await poll(async () => { + try { return (await octokit.rest.repos.get({ owner: ORG, repo: 'combined-settings-repo' })).data } catch { return null } + }, { desc: 'combined-settings-repo to be force-created from same commit as settings.yml', timeout: 90000 }) + assert(repo !== null, '14a: combined-settings-repo was created') + + const repoRuleset = await poll(async () => { + try { + const { data: rs } = await octokit.request('GET /repos/{owner}/{repo}/rulesets', { owner: ORG, repo: 'combined-settings-repo' }) + return rs.find(r => r.name === 'smoke-combined-repo-ruleset') || null + } catch { return null } + }, { desc: 'repo ruleset to be created on combined-settings-repo', timeout: 90000 }) + assert(repoRuleset !== null, '14a: repo-level ruleset created on combined-settings-repo') + + await deleteBranch(ORG, ADMIN_REPO, branch) + } + + // 14b: custom_repository_roles is Diffable and should honor additive_plugins. + // A role created outside safe-settings must survive a settings.yml sync that + // manages a different role while additive mode is enabled. + { + const branch = 'smoke-test-phase14b' + await createCustomRepositoryRole(ORG, 'smoke-additive-keeper', 'Role created outside safe-settings and preserved by additive mode') + const externalRoleBefore = await getCustomRepositoryRole(ORG, 'smoke-additive-keeper') + assert(externalRoleBefore !== null, '14b: external custom repository role exists before additive sync') + + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, SETTINGS_YML_CRR_ADDITIVE, branch, '14b: enable additive custom repository roles') + + const pr = await createPR(ORG, ADMIN_REPO, '14b: additive custom repository roles', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '14b: NOP check run completed') + if (checkRun) { + assert(checkRun.conclusion === 'success', `14b: NOP check run is success (got: ${checkRun.conclusion})`) + const crOutput = checkRun.output && (checkRun.output.summary || '') + assert(/additive|suppress/i.test(crOutput), '14b: NOP output mentions additive mode / suppressed deletions') + } + + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS + 15000) + + const externalRoleAfter = await poll(async () => { + return await getCustomRepositoryRole(ORG, 'smoke-additive-keeper') + }, { desc: 'external custom repository role to remain after additive sync', timeout: 60000 }) + assert(externalRoleAfter !== null, '14b: external custom repository role preserved by additive_plugins') + + const managedRole = await poll(async () => { + return await getCustomRepositoryRole(ORG, 'security-engineer') + }, { desc: 'managed custom repository role to exist after additive sync', timeout: 60000 }) + assert(managedRole !== null, '14b: managed custom repository role still created') + + await deleteBranch(ORG, ADMIN_REPO, branch) + } +} + +async function teardown () { + logPhase('Phase 9: Teardown') + + stopSafeSettings() + + log('Deleting test repos...') + try { await octokit.rest.repos.update({ owner: ORG, repo: 'demo-repo-service1', archived: false }) } catch { /* ok */ } + for (const repo of TEST_REPOS) { await deleteRepo(ORG, repo) } + + // Restore app installations to their original state so shared enterprise + // apps are left untouched by the smoke test. + if (entOctokit && Object.keys(appInstallSnapshot).length > 0) { + log('Restoring app installation repository selections...') + for (const [slug, snap] of Object.entries(appInstallSnapshot)) { + try { + if (snap.selection === 'all') { + await setInstallationSelection(snap.installationId, 'all') + } else if (snap.selection === 'selected' && snap.repos.length > 0) { + await setInstallationSelection(snap.installationId, 'selected', snap.repos) + } + } catch (e) { log(` Could not restore app '${slug}' installation: ${e.message}`) } + } + } + + log('Deleting app-install smoke repos...') + for (const repo of APP_TEST_REPOS) { await deleteRepo(ORG, repo) } + + log('Deleting test teams...') + for (const team of TEST_TEAMS) { await deleteTeam(ORG, team.toLowerCase()) } + try { await deleteTeam(ORG, SMOKE_NR_TEAM) } catch { /* ok */ } + for (const team of SMOKE_FILTER_TEAMS) { try { await deleteTeam(ORG, team) } catch { /* ok */ } } + + log('Deleting custom repository role...') + try { await deleteCustomRepositoryRole(ORG, 'security-engineer') } catch { /* ok */ } + try { await deleteCustomRepositoryRole(ORG, 'smoke-additive-keeper') } catch { /* ok */ } + try { await deleteCustomRepositoryRole(ORG, 'smoke-crr-managed') } catch { /* ok */ } + try { await deleteCustomRepositoryRole(ORG, 'smoke-crr-external') } catch { /* ok */ } + try { await deleteCustomRepositoryRole(ORG, 'smoke-crr-disabled') } catch { /* ok */ } + try { await deleteCustomRepositoryRole(ORG, SMOKE_NR_ROLE) } catch { /* ok */ } + + log('Deleting org rulesets...') + try { + const { data: rs } = await octokit.request('GET /orgs/{org}/rulesets', { org: ORG }) + const testRs = rs.find(r => r.name === 'test') + if (testRs) await octokit.request('DELETE /orgs/{org}/rulesets/{ruleset_id}', { org: ORG, ruleset_id: testRs.id }) + } catch { /* ok */ } + try { await deleteOrgRuleset(ORG, 'smoke-ruleset-managed') } catch { /* ok */ } + try { await deleteOrgRuleset(ORG, 'smoke-ruleset-external') } catch { /* ok */ } + try { await deleteOrgRuleset(ORG, 'smoke-ruleset-disabled') } catch { /* ok */ } + try { await deleteOrgRuleset(ORG, 'smoke-combined-org-ruleset') } catch { /* ok */ } + + log('Resetting admin repo settings...') + const defaultBranch = await getDefaultBranch() + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, '# empty\n', defaultBranch, 'Reset settings.yml after smoke test') + await cleanDirectory(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos`) + await cleanDirectory(ORG, ADMIN_REPO, `${CONFIG_PATH}/suborgs`) + + log('Teardown complete') +} + +async function phase15RulesetArrayDrift () { + logPhase('Phase 15: Drift remediation - Ruleset array fields (bypass_actors, rules, required_reviewers)') + + if (!GH_TOKEN) throw new Error('GH_TOKEN env var is required for drift tests (set to a fine-grained PAT)') + + // ── 15-setup: Restore full test.yml (earlier phases replace it with minimal configs) ── + // Phases 12d and 13 overwrite repos/test.yml with configs that omit rulesets, + // causing safe-settings to delete "synk" from the test repo. Restore it first. + { + log('15-setup: Restoring repos/test.yml to full config (ensures "synk" ruleset exists)...') + const defaultBranch = await getDefaultBranch() + const branch = 'smoke-test-phase15-setup' + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/test.yml`, REPO_TEST_YML, branch, '15-setup: restore full test repo config with rulesets') + const pr = await createPR(ORG, ADMIN_REPO, '15-setup: restore test.yml with rulesets', branch, defaultBranch) + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS) + + const synkReady = await poll(async () => { + return await getRepoRuleset(ORG, 'test', 'synk') + }, { desc: '"synk" ruleset to be (re)created after test.yml restore', timeout: 90000 }) + assert(synkReady !== null, '15-setup: "synk" ruleset present after restoring repos/test.yml') + if (!synkReady) return // cannot proceed without the ruleset + + await deleteBranch(ORG, ADMIN_REPO, branch) + } + + // ── 15a: Remove bypass_actors from "synk" ruleset ────────────────────────── + // The test repo "synk" ruleset has bypass_actors configured. + // Manually empty bypass_actors → safe-settings should detect and restore. + { + log('15a: Manually emptying bypass_actors on "synk" ruleset (as user)...') + const synkRuleset = await getRepoRuleset(ORG, 'test', 'synk') + if (!synkRuleset) { + logFail('15a: Could not find "synk" ruleset on test repo — was Phase 1 run?') + } else { + const fullRuleset = await getRepoRulesetDetails(ORG, 'test', synkRuleset.id) + if (!fullRuleset) { + logFail('15a: Could not fetch ruleset details') + } else { + const body = JSON.stringify({ ...fullRuleset, bypass_actors: [] }) + try { + execSync(`gh api /repos/${ORG}/test/rulesets/${synkRuleset.id} --method PUT --input -`, { + encoding: 'utf8', input: body, stdio: ['pipe', 'pipe', 'pipe'] + }) + log('15a: bypass_actors emptied on "synk" ruleset') + } catch (e) { logFail(`15a: Could not modify ruleset: ${e.message}`) } + + log('Waiting for safe-settings to remediate...') + await sleep(WEBHOOK_SETTLE_MS) + + const restored = await poll(async () => { + try { + const data = await getRepoRulesetDetails(ORG, 'test', synkRuleset.id) + return (data && data.bypass_actors && data.bypass_actors.length > 0) ? data : null + } catch { return null } + }, { desc: 'bypass_actors to be restored on "synk" ruleset', timeout: 90000 }) + + assert(restored !== null, '15a: bypass_actors restored after manual removal (drift detected)') + if (restored) { + assert( + restored.bypass_actors.some(a => a.actor_type === 'OrganizationAdmin'), + '15a: OrganizationAdmin bypass actor is present after restoration' + ) + } + } + } + } + + // ── 15b: Add out-of-band rule to "synk" ruleset ──────────────────────────── + // Add an extra rule not in the YAML config; safe-settings should remove it. + { + log('15b: Adding out-of-band "non_fast_forward" rule to "synk" ruleset (as user)...') + const synkRuleset = await getRepoRuleset(ORG, 'test', 'synk') + if (!synkRuleset) { + logFail('15b: Could not find "synk" ruleset on test repo') + } else { + const fullRuleset = await getRepoRulesetDetails(ORG, 'test', synkRuleset.id) + if (!fullRuleset) { + logFail('15b: Could not fetch ruleset details') + } else { + const rules = [...(fullRuleset.rules || []), { type: 'non_fast_forward' }] + const body = JSON.stringify({ rules }) + try { + execSync(`gh api /repos/${ORG}/test/rulesets/${synkRuleset.id} --method PUT --input -`, { + encoding: 'utf8', input: body, stdio: ['pipe', 'pipe', 'pipe'] + }) + log('15b: Added out-of-band "non_fast_forward" rule to "synk" ruleset') + } catch (e) { logFail(`15b: Could not modify ruleset: ${e.message}`) } + + log('Waiting for safe-settings to remediate...') + await sleep(WEBHOOK_SETTLE_MS) + + const reverted = await poll(async () => { + try { + const data = await getRepoRulesetDetails(ORG, 'test', synkRuleset.id) + const hasExtraRule = data && (data.rules || []).some(r => r.type === 'non_fast_forward') + return hasExtraRule ? null : data + } catch { return null } + }, { desc: 'out-of-band rule to be removed from "synk" ruleset', timeout: 90000 }) + + assert(reverted !== null, '15b: out-of-band "non_fast_forward" rule removed from "synk" ruleset (drift detected)') + } + } + } + + // ── 15c: Remove required_reviewers from suborg ruleset pull_request rule ─── + // This test runs only if the suborg "Protect release and production branches" + // ruleset is present (requires Phase 5 to have run first). + { + log('15c: Checking for suborg "Protect release and production branches" ruleset on test repo...') + const suborgRuleset = await getRepoRuleset(ORG, 'test', 'Protect release and production branches') + if (!suborgRuleset) { + log('15c: Suborg ruleset not found — skipping required_reviewers drift test (run Phase 5 first)') + } else { + const fullRuleset = await getRepoRulesetDetails(ORG, 'test', suborgRuleset.id) + if (!fullRuleset) { + logFail('15c: Could not fetch suborg ruleset details') + } else { + const prRule = (fullRuleset.rules || []).find(r => r.type === 'pull_request') + const hasRequiredReviewers = prRule && prRule.parameters && + Array.isArray(prRule.parameters.required_reviewers) && + prRule.parameters.required_reviewers.length > 0 + + if (!hasRequiredReviewers) { + log('15c: Suborg ruleset pull_request rule has no required_reviewers — skipping 15c') + } else { + log('15c: Manually emptying required_reviewers in pull_request rule (as user)...') + const rules = (fullRuleset.rules || []).map(rule => { + if (rule.type === 'pull_request') { + return { ...rule, parameters: { ...(rule.parameters || {}), required_reviewers: [] } } + } + return rule + }) + const body = JSON.stringify({ rules }) + try { + execSync(`gh api /repos/${ORG}/test/rulesets/${suborgRuleset.id} --method PUT --input -`, { + encoding: 'utf8', input: body, stdio: ['pipe', 'pipe', 'pipe'] + }) + log('15c: required_reviewers emptied in pull_request rule') + } catch (e) { logFail(`15c: Could not modify ruleset: ${e.message}`) } + + log('Waiting for safe-settings to remediate...') + await sleep(WEBHOOK_SETTLE_MS) + + const restored = await poll(async () => { + try { + const data = await getRepoRulesetDetails(ORG, 'test', suborgRuleset.id) + const pr = data && (data.rules || []).find(r => r.type === 'pull_request') + const reviewers = pr && pr.parameters && pr.parameters.required_reviewers + return (Array.isArray(reviewers) && reviewers.length > 0) ? data : null + } catch { return null } + }, { desc: 'required_reviewers to be restored in pull_request rule', timeout: 90000 }) + + assert(restored !== null, '15c: required_reviewers restored after manual removal (drift detected)') + } + } + } + } +} + +// Builds a branch ruleset that references its bypass actors and required +// reviewer by name (not numeric id), so safe-settings has to resolve them. +// actors: [{ name, actor_type, bypass_mode }] +function buildNameResolutionRuleset (actors) { + const bypassActorsYml = actors.map(a => +` - name: ${a.name} + actor_type: ${a.actor_type} + bypass_mode: ${a.bypass_mode}`).join('\n') + + return ` +- name: smoke-name-resolution + target: branch + enforcement: active + bypass_actors: +${bypassActorsYml} + conditions: + ref_name: + include: ["~DEFAULT_BRANCH"] + exclude: [] + rules: + - type: pull_request + parameters: + dismiss_stale_reviews_on_push: false + require_code_owner_review: false + require_last_push_approval: false + required_approving_review_count: 1 + required_review_thread_resolution: false + required_reviewers: + - minimum_approvals: 1 + file_patterns: + - "*.js" + reviewer: + slug: ${SMOKE_NR_TEAM} + type: Team +` +} + +async function phase16RulesetNameResolution () { + logPhase('Phase 16: Ruleset bypass actor.name + reviewer.slug resolution') + const defaultBranch = await getDefaultBranch() + const RULESET = 'smoke-name-resolution' + + // Ensure the principals exist so safe-settings can resolve names → ids. + log('Ensuring smoke team and custom repository role exist...') + const team = await ensureTeam(ORG, SMOKE_NR_TEAM) + if (!team) { logFail('Phase 16: could not create/find smoke team'); return } + const teamId = team.id + const role = await createCustomRepositoryRole(ORG, SMOKE_NR_ROLE, 'safe-settings smoke name-resolution role') + if (!role) { logFail('Phase 16: could not create custom repository role'); return } + const roleId = role.id + log(`Smoke team id=${teamId}, custom role id=${roleId}`) + + // Optional principals — only exercised when the env vars are provided. + const extraActors = [] + if (process.env.SMOKE_NR_USER) extraActors.push({ name: process.env.SMOKE_NR_USER, actor_type: 'User', bypass_mode: 'always' }) + if (process.env.SMOKE_NR_APP_SLUG) extraActors.push({ name: process.env.SMOKE_NR_APP_SLUG, actor_type: 'Integration', bypass_mode: 'always' }) + + // ── 16a: Create a ruleset entirely by name (Team, built-in + custom role, reviewer slug) ── + const createActors = [ + { name: SMOKE_NR_TEAM, actor_type: 'Team', bypass_mode: 'always' }, + { name: 'maintain', actor_type: 'RepositoryRole', bypass_mode: 'always' }, + { name: SMOKE_NR_ROLE, actor_type: 'RepositoryRole', bypass_mode: 'pull_request' }, + ...extraActors + ] + { + const branch = 'smoke-test-phase16a' + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/test.yml`, REPO_TEST_YML + buildNameResolutionRuleset(createActors), branch, '16a: add name-resolution ruleset') + const pr = await createPR(ORG, ADMIN_REPO, '16a: ruleset bypass actor.name + reviewer.slug', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '16a: NOP check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `16a: NOP check run is success (got: ${checkRun.conclusion})`) + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS) + + const details = await poll(async () => { + const rs = await getRepoRuleset(ORG, 'test', RULESET) + if (!rs) return null + return await getRepoRulesetDetails(ORG, 'test', rs.id) + }, { desc: 'name-resolution ruleset to be created', timeout: 90000 }) + + assert(details !== null, '16a: ruleset created from name-based config') + if (details) { + const actors = details.bypass_actors || [] + assert(actors.some(a => a.actor_type === 'Team' && a.actor_id === teamId), `16a: Team name resolved to actor_id ${teamId}`) + assert(actors.some(a => a.actor_type === 'RepositoryRole' && a.actor_id === 4), '16a: built-in role "maintain" resolved to actor_id 4') + assert(actors.some(a => a.actor_type === 'RepositoryRole' && a.actor_id === roleId), `16a: custom role resolved to actor_id ${roleId}`) + // GitHub only ever stores ids; the human-friendly alias must not leak through. + assert(actors.every(a => a.name === undefined), '16a: no "name" alias present in applied ruleset (resolved to ids)') + + const prRule = (details.rules || []).find(r => r.type === 'pull_request') + const reviewers = prRule && prRule.parameters && prRule.parameters.required_reviewers + const reviewer = Array.isArray(reviewers) && reviewers[0] && reviewers[0].reviewer + assert(reviewer && reviewer.id === teamId, `16a: reviewer.slug resolved to team id ${teamId}`) + + if (process.env.SMOKE_NR_USER) assert(actors.some(a => a.actor_type === 'User' && Number.isInteger(a.actor_id)), '16a: User name resolved to actor_id') + if (process.env.SMOKE_NR_APP_SLUG) assert(actors.some(a => a.actor_type === 'Integration' && Number.isInteger(a.actor_id)), '16a: Integration slug resolved to actor_id') + } + await deleteBranch(ORG, ADMIN_REPO, branch) + } + + // ── 16b: Modify the ruleset by name — swap built-in maintain(4) → admin(5) ── + const modifyActors = createActors.map(a => + (a.actor_type === 'RepositoryRole' && a.name === 'maintain') ? { ...a, name: 'admin' } : a) + { + const branch = 'smoke-test-phase16b' + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/test.yml`, REPO_TEST_YML + buildNameResolutionRuleset(modifyActors), branch, '16b: modify name-resolution ruleset') + const pr = await createPR(ORG, ADMIN_REPO, '16b: modify ruleset bypass actor by name', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '16b: NOP check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `16b: NOP check run is success (got: ${checkRun.conclusion})`) + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS) + + const updated = await poll(async () => { + const rs = await getRepoRuleset(ORG, 'test', RULESET) + if (!rs) return null + const d = await getRepoRulesetDetails(ORG, 'test', rs.id) + const actors = (d && d.bypass_actors) || [] + const hasAdmin = actors.some(a => a.actor_type === 'RepositoryRole' && a.actor_id === 5) + const hasMaintain = actors.some(a => a.actor_type === 'RepositoryRole' && a.actor_id === 4) + return (hasAdmin && !hasMaintain) ? d : null + }, { desc: 'ruleset to be updated with admin role (5) replacing maintain (4)', timeout: 90000 }) + + assert(updated !== null, '16b: ruleset modified by name — maintain(4) replaced with admin(5)') + if (updated) { + const actors = updated.bypass_actors || [] + assert(actors.some(a => a.actor_type === 'Team' && a.actor_id === teamId), '16b: Team bypass actor preserved across modification') + assert(actors.some(a => a.actor_type === 'RepositoryRole' && a.actor_id === roleId), '16b: custom role bypass actor preserved across modification') + } + await deleteBranch(ORG, ADMIN_REPO, branch) + } +} + +// ─── App installation config builders (Phase 17) ───────────────────────────── + +// Org-level settings.yml giving an app access to ALL repos. +const settingsAppInstallAll = (slug) => `# App installations: org-level (implies all repos) +app_installations: + - app_slug: ${slug} +` + +// Empty org settings (no app_installations) with an optional comment bump to +// force a full sync while app selection is driven entirely by repo configs. +const settingsAppInstallEmpty = (bump = '') => `# App installations: no org-level app config${bump ? ` (${bump})` : ''} +` + +// Repo-level config that force-creates the repo and adds it to the app. +const repoAppInstallConfig = (name, slug) => `repository: + name: ${name} +app_installations: + - app_slug: ${slug} +` + +// Suborg-level config that targets an explicit list of repos (suborgrepos) and +// adds the app for those repos. Suborg config changes drive the delta sync. +const suborgAppInstallConfig = (repos, slug) => `suborgrepos: +${repos.map(r => ` - ${r}`).join('\n')} +app_installations: + - app_slug: ${slug} +` + +async function phase17AppInstallations () { + logPhase('Phase 17: App installation management (app_installations plugin)') + + if (!GH_ENTERPRISE) { + log('17: GH_ENTERPRISE not set — skipping app_installations tests') + return + } + if (APP_SLUGS.length === 0) { + log('17: SMOKE_APP_SLUGS not set — skipping app_installations tests') + return + } + if (!entOctokit) { + logFail('17: enterprise installation not available (app not installed on enterprise or GH_ENTERPRISE mismatch) — cannot run app_installations tests') + return + } + + const defaultBranch = await getDefaultBranch() + const app = APP_SLUGS[0] + + // Snapshot every managed app's live installation state for restore in teardown. + for (const slug of APP_SLUGS) { + const inst = await getAppInstallation(slug) + if (!inst) { + logFail(`17: app '${slug}' is not installed on org ${ORG} via enterprise '${GH_ENTERPRISE}' — pre-create and install it`) + continue + } + appInstallSnapshot[slug] = { + installationId: inst.id, + selection: inst.repository_selection, + repos: inst.repository_selection === 'selected' ? await listInstallationRepoNames(inst.id) : [] + } + } + + const primary = appInstallSnapshot[app] + if (!primary) { + logFail(`17: primary app '${app}' installation not found — aborting app_installations tests`) + return + } + const installationId = primary.installationId + log(`17: primary app '${app}' installation id=${installationId}, initial selection='${primary.selection}'`) + + log('17: Ensuring dedicated app-install smoke repos exist...') + await ensureRepo(APP_TEST_REPOS[0]) + await ensureRepo(APP_TEST_REPOS[1]) + + // ── 17a: Org-level repository_selection: all ─────────────────────────────── + { + log('17a: Setting app to repository_selection: all via org settings.yml') + const branch = 'smoke-test-phase17a' + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, settingsAppInstallAll(app), branch, '17a: app_installations repository_selection all') + const pr = await createPR(ORG, ADMIN_REPO, '17a: app_installations org-level all', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '17a: NOP check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `17a: NOP check run is success (got: ${checkRun.conclusion})`) + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS + 15000) + + const selAll = await poll(async () => { + const i = await getAppInstallation(app) + return (i && i.repository_selection === 'all') ? i : null + }, { desc: "app installation to be set to 'all'", timeout: 90000 }) + assert(selAll !== null, "17a: app repository_selection set to 'all' by org-level config") + + // Isolation: any other configured app must be unchanged by app[0]'s config. + for (const slug of APP_SLUGS.slice(1)) { + const snap = appInstallSnapshot[slug] + if (!snap) continue + const other = await getAppInstallation(slug) + assert(other !== null && other.repository_selection === snap.selection, + `17a: other app '${slug}' repository_selection unchanged (isolation)`) + } + + await deleteBranch(ORG, ADMIN_REPO, branch) + } + + // ── 17b: Repo-level selection for two repos (full sync via settings.yml) ──── + // Remove the org-level 'all' and drive selection entirely from repo configs. + // A settings.yml change triggers a full sync, which recomputes desired state + // from all layers and narrows the installation from 'all' → 'selected'. + { + log('17b: Narrowing app to two specific repos via repo-level configs') + const branch = 'smoke-test-phase17b' + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, settingsAppInstallEmpty(), branch, '17b: clear org app_installations') + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/${APP_TEST_REPOS[0]}.yml`, repoAppInstallConfig(APP_TEST_REPOS[0], app), branch, `17b: add ${APP_TEST_REPOS[0]} to app`) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/${APP_TEST_REPOS[1]}.yml`, repoAppInstallConfig(APP_TEST_REPOS[1], app), branch, `17b: add ${APP_TEST_REPOS[1]} to app`) + const pr = await createPR(ORG, ADMIN_REPO, '17b: app_installations repo-level selection', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '17b: NOP check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `17b: NOP check run is success (got: ${checkRun.conclusion})`) + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS + 15000) + + const selected = await poll(async () => { + const i = await getAppInstallation(app) + if (!i || i.repository_selection !== 'selected') return null + const repos = await listInstallationRepoNames(i.id) + return (repos.includes(APP_TEST_REPOS[0]) && repos.includes(APP_TEST_REPOS[1])) ? repos : null + }, { desc: "app installation to be 'selected' with the two repos", timeout: 90000 }) + assert(selected !== null, `17b: app narrowed to 'selected' with ${APP_TEST_REPOS[0]} and ${APP_TEST_REPOS[1]}`) + } + + // ── 17c: Add a third repo via a new repo config ──────────────────────────── + { + log('17c: Adding a third repo to the app via a new repo config') + await ensureRepo(APP_TEST_REPOS[2]) + const branch = 'smoke-test-phase17c' + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + // Bump settings.yml to force a full sync that includes the new repo config. + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, settingsAppInstallEmpty('bump 17c'), branch, '17c: bump settings to trigger full sync') + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/${APP_TEST_REPOS[2]}.yml`, repoAppInstallConfig(APP_TEST_REPOS[2], app), branch, `17c: add ${APP_TEST_REPOS[2]} to app`) + const pr = await createPR(ORG, ADMIN_REPO, '17c: app_installations add repo', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '17c: NOP check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `17c: NOP check run is success (got: ${checkRun.conclusion})`) + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS + 15000) + + const added = await poll(async () => { + const i = await getAppInstallation(app) + if (!i || i.repository_selection !== 'selected') return null + const repos = await listInstallationRepoNames(i.id) + return repos.includes(APP_TEST_REPOS[2]) ? repos : null + }, { desc: `${APP_TEST_REPOS[2]} to be added to the app installation`, timeout: 90000 }) + assert(added !== null, `17c: ${APP_TEST_REPOS[2]} added to app installation`) + } + + // ── 17d: Remove the third repo by deleting its config ────────────────────── + { + log('17d: Removing the third repo from the app by deleting its repo config') + const branch = 'smoke-test-phase17d' + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await deleteFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/${APP_TEST_REPOS[2]}.yml`, branch, `17d: remove ${APP_TEST_REPOS[2]} config`) + // Bump settings.yml to force a full sync (removals are reconciled by full sync). + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, settingsAppInstallEmpty('bump 17d'), branch, '17d: bump settings to trigger full sync') + const pr = await createPR(ORG, ADMIN_REPO, '17d: app_installations remove repo', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '17d: NOP check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `17d: NOP check run is success (got: ${checkRun.conclusion})`) + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS + 15000) + + const removed = await poll(async () => { + const i = await getAppInstallation(app) + if (!i || i.repository_selection !== 'selected') return null + const repos = await listInstallationRepoNames(i.id) + return !repos.includes(APP_TEST_REPOS[2]) ? repos : null + }, { desc: `${APP_TEST_REPOS[2]} to be removed from the app installation`, timeout: 90000 }) + assert(removed !== null, `17d: ${APP_TEST_REPOS[2]} removed from app installation`) + } + + // ── 17e: Drift remediation via full sync ─────────────────────────────────── + // Manually add an unmanaged repo to the installation, then trigger a full + // sync (settings.yml bump). Full sync must remove the drifted repo since it + // is not in any config layer's desired state. + { + log(`17e: Injecting drift — adding unmanaged ${APP_TEST_REPOS[2]} to the installation directly`) + try { + await addInstallationRepos(installationId, [APP_TEST_REPOS[2]]) + } catch (e) { logFail(`17e: could not inject drift: ${e.message}`) } + + const driftPresent = await poll(async () => { + const repos = await listInstallationRepoNames(installationId) + return repos.includes(APP_TEST_REPOS[2]) ? repos : null + }, { desc: `drifted ${APP_TEST_REPOS[2]} to be present before remediation`, timeout: 30000 }) + assert(driftPresent !== null, `17e: drift injected (${APP_TEST_REPOS[2]} present on installation)`) + + const branch = 'smoke-test-phase17e' + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/settings.yml`, settingsAppInstallEmpty('bump 17e'), branch, '17e: bump settings to trigger full sync drift remediation') + const pr = await createPR(ORG, ADMIN_REPO, '17e: app_installations drift remediation', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, '17e: NOP check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `17e: NOP check run is success (got: ${checkRun.conclusion})`) + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS + 15000) + + const driftRemoved = await poll(async () => { + const repos = await listInstallationRepoNames(installationId) + return !repos.includes(APP_TEST_REPOS[2]) ? repos : null + }, { desc: `drifted ${APP_TEST_REPOS[2]} to be removed by full sync`, timeout: 90000 }) + assert(driftRemoved !== null, `17e: drift remediated — ${APP_TEST_REPOS[2]} removed by full sync`) + + await deleteBranch(ORG, ADMIN_REPO, branch) + } + + // ── 17f: Sub-org targeting (delta sync via suborgs/*.yml) ────────────────── + // A suborg config change triggers the delta sync path (not full sync). The + // suborg's targeting (here suborgrepos) resolves the repos to add/remove for + // the app. Entering this block the installation is 'selected' with + // APP_TEST_REPOS[0] and [1]; [2] is not selected. + { + // Step A: add a suborg that targets repo-3 and adds the app → repo-3 added. + log('17f: Adding a repo to the app via suborg targeting (suborgrepos, delta sync)') + const branchA = 'smoke-test-phase17f-add' + await deleteBranch(ORG, ADMIN_REPO, branchA) + await createBranch(ORG, ADMIN_REPO, branchA) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/suborgs/smoke-app-suborg.yml`, suborgAppInstallConfig([APP_TEST_REPOS[2]], app), branchA, '17f: suborg targeting adds smoke-app-repo-3 to app') + const prA = await createPR(ORG, ADMIN_REPO, '17f: app_installations suborg add', branchA, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRunA = await waitForCheckRun(ORG, ADMIN_REPO, prA.head.sha) + assert(checkRunA !== null, '17f: NOP check run completed (suborg add)') + if (checkRunA) assert(checkRunA.conclusion === 'success', `17f: NOP check run is success (got: ${checkRunA.conclusion})`) + if (!await safeMerge(ORG, ADMIN_REPO, prA.number)) return + await sleep(WEBHOOK_SETTLE_MS + 15000) + + const suborgAdded = await poll(async () => { + const i = await getAppInstallation(app) + if (!i || i.repository_selection !== 'selected') return null + const repos = await listInstallationRepoNames(i.id) + return repos.includes(APP_TEST_REPOS[2]) ? repos : null + }, { desc: `${APP_TEST_REPOS[2]} to be added via suborg targeting`, timeout: 90000 }) + assert(suborgAdded !== null, `17f: ${APP_TEST_REPOS[2]} added to app via suborg targeting (delta sync)`) + + await deleteBranch(ORG, ADMIN_REPO, branchA) + + // Step B: narrow the suborg targeting so repo-3 drops out → repo-3 removed. + // Retarget to repo-1 (already selected by its repo config, so a no-op add); + // the delta unselection must remove repo-3. + log('17f: Narrowing suborg targeting so smoke-app-repo-3 drops out (delta unselection)') + const branchB = 'smoke-test-phase17f-narrow' + await deleteBranch(ORG, ADMIN_REPO, branchB) + await createBranch(ORG, ADMIN_REPO, branchB) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/suborgs/smoke-app-suborg.yml`, suborgAppInstallConfig([APP_TEST_REPOS[0]], app), branchB, '17f: narrow suborg targeting to smoke-app-repo-1') + const prB = await createPR(ORG, ADMIN_REPO, '17f: app_installations suborg narrow', branchB, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRunB = await waitForCheckRun(ORG, ADMIN_REPO, prB.head.sha) + assert(checkRunB !== null, '17f: NOP check run completed (suborg narrow)') + if (checkRunB) assert(checkRunB.conclusion === 'success', `17f: NOP check run is success (got: ${checkRunB.conclusion})`) + if (!await safeMerge(ORG, ADMIN_REPO, prB.number)) return + await sleep(WEBHOOK_SETTLE_MS + 15000) + + const suborgRemoved = await poll(async () => { + const i = await getAppInstallation(app) + if (!i || i.repository_selection !== 'selected') return null + const repos = await listInstallationRepoNames(i.id) + return !repos.includes(APP_TEST_REPOS[2]) ? repos : null + }, { desc: `${APP_TEST_REPOS[2]} to be removed when dropped from suborg targeting`, timeout: 90000 }) + assert(suborgRemoved !== null, `17f: ${APP_TEST_REPOS[2]} removed when dropped from suborg targeting (delta unselection)`) + + await deleteBranch(ORG, ADMIN_REPO, branchB) + } +} + +// ─── Main ──────────────────────────────────────────────────────────────────── + +async function phase18TeamIncludeExclude () { + logPhase('Phase 18: Team include/exclude repo filters') + const branch = 'smoke-test-phase18' + const defaultBranch = await getDefaultBranch() + + // Clean any leftover teams from a previous aborted run so the "not applied" + // assertions can't be satisfied by stale repo-team associations. + await deleteRepo(ORG, SMOKE_FILTER_REPO) + for (const t of SMOKE_FILTER_TEAMS) { await deleteTeam(ORG, t) } + + await deleteBranch(ORG, ADMIN_REPO, branch) + await createBranch(ORG, ADMIN_REPO, branch) + await createOrUpdateFile(ORG, ADMIN_REPO, `${CONFIG_PATH}/repos/${SMOKE_FILTER_REPO}.yml`, REPO_TEAM_FILTER_YML, branch, 'Add team include/exclude filter config') + + const pr = await createPR(ORG, ADMIN_REPO, 'Smoke test: team include/exclude filters', branch, defaultBranch) + log('Waiting for NOP check run...') + await sleep(WEBHOOK_SETTLE_MS) + const checkRun = await waitForCheckRun(ORG, ADMIN_REPO, pr.head.sha) + assert(checkRun !== null, 'Check run completed') + if (checkRun) assert(checkRun.conclusion === 'success', `Check run conclusion is success (got: ${checkRun.conclusion})`) + + if (!await safeMerge(ORG, ADMIN_REPO, pr.number)) return + await sleep(WEBHOOK_SETTLE_MS) + + const repo = await poll(async () => { + try { return (await octokit.rest.repos.get({ owner: ORG, repo: SMOKE_FILTER_REPO })).data } catch { return null } + }, { desc: `${SMOKE_FILTER_REPO} to be created` }) + assert(repo !== null, `Repo "${SMOKE_FILTER_REPO}" was created`) + + // The included team should be applied (poll — safe-settings may still be working). + const includedTeam = await poll(async () => { + try { + const { data: teams } = await octokit.rest.repos.listTeams({ owner: ORG, repo: SMOKE_FILTER_REPO }) + return teams.find(t => t.slug === SMOKE_FILTER_TEAMS[0]) || null + } catch { return null } + }, { desc: `included team ${SMOKE_FILTER_TEAMS[0]} to be added`, timeout: 60000 }) + assert(includedTeam !== null, `Team "${SMOKE_FILTER_TEAMS[0]}" applied (include glob matches repo)`) + if (includedTeam) { + const hasMaintain = includedTeam.permission === 'maintain' || (includedTeam.permissions && includedTeam.permissions.maintain === true) + assert(hasMaintain, `Included team has maintain permission (got: ${includedTeam.permission})`) + } + + // The excluded and non-matching teams must NOT be applied. + const finalTeams = await (async () => { + try { + const { data: teams } = await octokit.rest.repos.listTeams({ owner: ORG, repo: SMOKE_FILTER_REPO }) + return teams.map(t => t.slug) + } catch { return [] } + })() + assert(!finalTeams.includes(SMOKE_FILTER_TEAMS[1]), `Team "${SMOKE_FILTER_TEAMS[1]}" NOT applied (exclude glob matches repo)`) + assert(!finalTeams.includes(SMOKE_FILTER_TEAMS[2]), `Team "${SMOKE_FILTER_TEAMS[2]}" NOT applied (include glob does not match repo)`) + + // Cleanup for standalone --phase 18 runs (teardown also cleans these). + await deleteRepo(ORG, SMOKE_FILTER_REPO) + for (const t of SMOKE_FILTER_TEAMS) { await deleteTeam(ORG, t) } + await deleteBranch(ORG, ADMIN_REPO, branch) +} + +async function main () { + const { App } = await import('octokit') + const app = new App({ appId: APP_ID, privateKey: PRIVATE_KEY }) + + // Find installation for our org + let installationId + for await (const { installation } of app.eachInstallation.iterator()) { + // Org/user installations key off account.login (only enterprise accounts have a slug). + if (installation.account && installation.account.login && installation.account.login.toLowerCase() === ORG.toLowerCase()) { + installationId = installation.id + break + } + } + if (!installationId) throw new Error(`No installation found for org ${ORG}`) + + octokit = await app.getInstallationOctokit(installationId) + log('Authenticated as GitHub App installation') + + // Optionally resolve the enterprise installation for the app_installations + // plugin phase. The same App must be installed on the enterprise with the + // "Enterprise organization installations" permission. + if (GH_ENTERPRISE && APP_SLUGS.length > 0) { + for await (const { installation } of app.eachInstallation.iterator()) { + if (installation.target_type === 'Enterprise' && + installation.account && installation.account.slug && + installation.account.slug.toLowerCase() === GH_ENTERPRISE.toLowerCase()) { + entOctokit = await app.getInstallationOctokit(installation.id) + log(`Authenticated as enterprise installation for '${GH_ENTERPRISE}' (app_installations phase enabled)`) + break + } + } + if (!entOctokit) log(`\x1b[33m⚠ No enterprise installation found for '${GH_ENTERPRISE}' — Phase 17 will report a failure.\x1b[0m`) + } + + console.log(` +\x1b[36m╔══════════════════════════════════════╗ +║ Safe-Settings Smoke Test ║ +║ Org: ${ORG.padEnd(28)}║ +║ Admin Repo: ${ADMIN_REPO.padEnd(22)}║ +╚══════════════════════════════════════╝\x1b[0m +`) + + if (INTERACTIVE) log('\x1b[33m[interactive] Mode enabled — will pause after each phase.\x1b[0m') + if (ONLY_PHASES !== null) log(`\x1b[33m[phase filter] Running setup + phase(s) [${[...ONLY_PHASES].join(', ')}] + teardown only.\x1b[0m`) + + let doTeardown = true + try { + const allPhases = [ + ['Phase 0: Setup', setup], + ['Phase 1: Create test repo', phase1CreateRepo], + ['Phase 2: Drift remediation - Team removal', phase2DriftTeam], + ['Phase 3: Drift remediation - Rogue ruleset', phase3DriftRuleset], + ['Phase 4: Create demo-repo-service1', phase4DemoRepo1], + ['Phase 5: Create suborg config', phase5Suborg], + ['Phase 6: Archive demo-repo-service1', phase6Archive], + ['Phase 7: Create demo-repo-service2', phase7DemoRepo2], + ['Phase 7b: External group team', phase7bExternalGroupTeam], + ['Phase 8: Org-level settings', phase8OrgSettings], + ['Phase 10: disable_plugins', phase10DisablePlugins], + ['Phase 11: additive_plugins', phase11AdditivePlugins], + ['Phase 12: custom_properties', phase12CustomProperties], + ['Phase 12: custom_repository_roles', phase12CustomRoles], + ['Phase 12: rulesets', phase12Rulesets], + ['Phase 13: variables', phase13Variables], + ['Phase 14: regressions', phase14RegressionCoverage], + ['Phase 15: Ruleset array drift', phase15RulesetArrayDrift], + ['Phase 16: Ruleset name/slug resolution', phase16RulesetNameResolution], + ['Phase 17: App installation management', phase17AppInstallations], + ['Phase 18: Team include/exclude filters', phase18TeamIncludeExclude] + ] + + // When --phase is given, only run setup (phase 0) + the requested phase(s). + // Phase labels start with "Phase N:" so we match on that prefix. + const phases = ONLY_PHASES !== null + ? allPhases.filter(([label]) => { + if (label.startsWith('Phase 0:')) return true + const m = label.match(/^Phase (\d+)[:\s]/) + return m !== null && ONLY_PHASES.has(parseInt(m[1], 10)) + }) + : allPhases + + if (ONLY_PHASES !== null && phases.length < 2) { + const valid = allPhases.map(([label]) => label.replace(/^Phase (\S+):.*/, '$1')).filter(n => n !== '0').join(', ') + throw new Error(`No phases matching [${[...ONLY_PHASES].join(', ')}] found. Valid phase numbers: ${valid}`) + } + for (const [label, fn] of phases) { + const action = await runPhase(label, fn) + if (action === 'abort') { doTeardown = false; break } + if (action === 'quit') break + } + } catch (err) { + if (err instanceof InteractiveExit) { + if (err.action === 'abort') doTeardown = false + } else { + console.error(`\x1b[31mFatal error: ${err.message}\x1b[0m`) + console.error(err.stack) + } + } finally { + if (doTeardown) await teardown() + else log('\x1b[33m[interactive] Aborted — teardown skipped.\x1b[0m') + } + + console.log(` +\x1b[36m╔══════════════════════════════════════╗ +║ Results ║ +╚══════════════════════════════════════╝\x1b[0m + \x1b[32mPassed: ${passCount}\x1b[0m + \x1b[31mFailed: ${failCount}\x1b[0m +`) + + if (failures.length > 0) { + console.log('\x1b[31mFailures:\x1b[0m') + failures.forEach((f, i) => console.log(` ${i + 1}. ${f}`)) + console.log() + } + + process.exit(failCount > 0 ? 1 : 0) +} + +main().catch(err => { + console.error(err) + stopSafeSettings() + process.exit(1) +}) diff --git a/test/integration/common.js b/test/integration/common.js index 4d47b210a..4474db761 100644 --- a/test/integration/common.js +++ b/test/integration/common.js @@ -16,7 +16,28 @@ const repository = { } function loadInstance () { - const probot = createProbot({ id: 1, cert: 'test', githubToken: 'test' }) + // Probot 13's `createProbot` only reads `overrides`/`defaults`/`env`, so the + // old positional `{ id, cert, githubToken }` args were silently dropped, + // leaving no credentials and making `@octokit/auth-app` throw + // "appId option is required". Provide dummy credentials via `overrides`. + // Using a `githubToken` selects Octokit's token auth strategy, which avoids + // the app-auth JWT/installation-token calls that the nock scopes don't mock. + // + // The app also runs `info()` on load, which lists app installations. Stub + // that startup call with an empty list so it resolves cleanly under + // `nock.disableNetConnect()` without interfering with the per-test scopes. + nock('https://api.github.com') + .persist() + .get('/app/installations') + .query(true) + .reply(200, []) + + const probot = createProbot({ + overrides: { + appId: 1, + githubToken: 'test' + } + }) probot.load(settingsBot) return probot diff --git a/test/integration/plugins/teams.test.js b/test/integration/plugins/teams.test.js index 4fde0637f..535a510f2 100644 --- a/test/integration/plugins/teams.test.js +++ b/test/integration/plugins/teams.test.js @@ -24,6 +24,8 @@ describe('teams plugin', function () { const probotTeamId = any.integer() const greenkeeperKeeperTeamId = any.integer() const formationTeamId = any.integer() + const securityManagerRoleId = any.integer() + const securityManagerTeamId = any.integer() githubScope .get(`/repos/${repository.owner.name}/${repository.name}/contents/${settings.FILE_PATH}`) .reply(OK, { content: encodedConfig, name: 'settings.yml', type: 'file' }) @@ -33,9 +35,20 @@ describe('teams plugin', function () { OK, [ { slug: 'greenkeeper-keeper', id: greenkeeperKeeperTeamId, permission: 'pull' }, - { slug: 'form8ion', id: formationTeamId, permission: 'push' } + { slug: 'form8ion', id: formationTeamId, permission: 'push' }, + { slug: 'security-managers', id: securityManagerTeamId, permission: 'push' } ] ) + githubScope + .get(`/orgs/${repository.owner.name}/organization-roles`) + .reply(OK, { + roles: [{ id: securityManagerRoleId, slug: 'security_manager', name: 'Security Manager' }] + }) + githubScope + .get(`/orgs/${repository.owner.name}/organization-roles/${securityManagerRoleId}/teams`) + .reply(OK, { + teams: [{ id: securityManagerTeamId, slug: 'security-managers', name: 'Security Managers' }] + }) githubScope .get(`/orgs/${repository.owner.name}/teams/probot`) .reply(OK, { id: probotTeamId }) diff --git a/test/setup.js b/test/setup.js new file mode 100644 index 000000000..3bb75c781 --- /dev/null +++ b/test/setup.js @@ -0,0 +1,14 @@ +/* eslint-disable no-undef */ +// Fail tests that produce unexpected console.error output. +// This ensures that caught-but-logged errors in production code don't +// silently slip through the test suite (e.g. TypeError inside a catch block +// that logs via this.log.error = console.error). +beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation((...args) => { + throw new Error(`Unexpected console.error call in test: ${args.join(' ')}`) + }) +}) + +afterEach(() => { + jest.restoreAllMocks() +}) diff --git a/test/unit/index.test.js b/test/unit/index.test.js index feae42d95..3b61afdd7 100644 --- a/test/unit/index.test.js +++ b/test/unit/index.test.js @@ -1,3 +1,4 @@ +jest.mock('probot', () => ({ Probot: class Probot {} })) const { Probot } = require('probot') const plugin = require('../../index') diff --git a/test/unit/lib/appOctokitClient.test.js b/test/unit/lib/appOctokitClient.test.js new file mode 100644 index 000000000..dd7206f64 --- /dev/null +++ b/test/unit/lib/appOctokitClient.test.js @@ -0,0 +1,148 @@ +const AppOctokitClient = require('../../../lib/appOctokitClient') + +describe('AppOctokitClient', () => { + let github + let log + let client + + beforeEach(() => { + log = { + debug: jest.fn(), + error: jest.fn() + } + + github = { + paginate: jest.fn(), + request: jest.fn().mockResolvedValue({ data: {} }) + } + github.request.endpoint = { + merge: jest.fn().mockReturnValue({}) + } + + client = new AppOctokitClient({ + github, + enterpriseSlug: 'my-enterprise', + log + }) + }) + + describe('listOrgInstallations', () => { + it('returns org installations', async () => { + github.paginate.mockResolvedValue([ + { id: 1, app_slug: 'app-a', repository_selection: 'all' }, + { id: 3, app_slug: 'app-c', repository_selection: 'selected' } + ]) + + const result = await client.listOrgInstallations('my-org') + expect(result).toHaveLength(2) + expect(result[0].app_slug).toBe('app-a') + expect(github.request.endpoint.merge).toHaveBeenCalledWith( + 'GET /enterprises/{enterprise}/apps/organizations/{org}/installations', + expect.objectContaining({ enterprise: 'my-enterprise', org: 'my-org' }) + ) + }) + + it('throws descriptive error on 403', async () => { + github.paginate.mockRejectedValue({ status: 403, message: 'Forbidden' }) + + await expect(client.listOrgInstallations('my-org')) + .rejects.toThrow(/enterprise/) + }) + + it('throws descriptive error on 404', async () => { + github.paginate.mockRejectedValue({ status: 404, message: 'Not Found' }) + + await expect(client.listOrgInstallations('my-org')) + .rejects.toThrow(/enterprise/) + }) + }) + + describe('setRepositorySelection', () => { + it("toggles to 'all' without repositories", async () => { + await client.setRepositorySelection('my-org', 123, 'all') + expect(github.request).toHaveBeenCalledWith( + 'PATCH /enterprises/{enterprise}/apps/organizations/{org}/installations/{installation_id}/repositories', + expect.objectContaining({ + org: 'my-org', + installation_id: 123, + repository_selection: 'all' + }) + ) + const callArgs = github.request.mock.calls[0][1] + expect(callArgs.repositories).toBeUndefined() + }) + + it("toggles to 'selected' with repository names", async () => { + await client.setRepositorySelection('my-org', 123, 'selected', ['repo-a', 'repo-b']) + expect(github.request).toHaveBeenCalledWith( + 'PATCH /enterprises/{enterprise}/apps/organizations/{org}/installations/{installation_id}/repositories', + expect.objectContaining({ + repository_selection: 'selected', + repositories: ['repo-a', 'repo-b'] + }) + ) + }) + }) + + describe('addReposToInstallation', () => { + it('does nothing for empty array', async () => { + await client.addReposToInstallation('my-org', 123, []) + expect(github.request).not.toHaveBeenCalled() + }) + + it('sends single batch for <= 50 repos using names', async () => { + const names = Array.from({ length: 10 }, (_, i) => `repo-${i}`) + await client.addReposToInstallation('my-org', 123, names) + expect(github.request).toHaveBeenCalledTimes(1) + expect(github.request).toHaveBeenCalledWith( + 'PATCH /enterprises/{enterprise}/apps/organizations/{org}/installations/{installation_id}/repositories/add', + expect.objectContaining({ + repositories: names, + installation_id: 123, + org: 'my-org' + }) + ) + }) + + it('batches into chunks of 50', async () => { + const names = Array.from({ length: 120 }, (_, i) => `repo-${i}`) + await client.addReposToInstallation('my-org', 123, names) + expect(github.request).toHaveBeenCalledTimes(3) // 50 + 50 + 20 + }) + }) + + describe('removeReposFromInstallation', () => { + it('does nothing for empty array', async () => { + await client.removeReposFromInstallation('my-org', 123, []) + expect(github.request).not.toHaveBeenCalled() + }) + + it('uses the remove endpoint with names', async () => { + await client.removeReposFromInstallation('my-org', 123, ['repo-a']) + expect(github.request).toHaveBeenCalledWith( + 'PATCH /enterprises/{enterprise}/apps/organizations/{org}/installations/{installation_id}/repositories/remove', + expect.objectContaining({ repositories: ['repo-a'] }) + ) + }) + + it('batches into chunks of 50', async () => { + const names = Array.from({ length: 75 }, (_, i) => `repo-${i}`) + await client.removeReposFromInstallation('my-org', 123, names) + expect(github.request).toHaveBeenCalledTimes(2) // 50 + 25 + }) + }) + + describe('_chunk', () => { + it('splits array into correct chunks', () => { + expect(client._chunk([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]]) + }) + + it('returns single chunk for small array', () => { + expect(client._chunk([1, 2], 50)).toEqual([[1, 2]]) + }) + + it('returns empty array for empty input', () => { + expect(client._chunk([], 50)).toEqual([]) + }) + }) +}) diff --git a/test/unit/lib/mergeDeep.test.js b/test/unit/lib/mergeDeep.test.js index dd8bd60ba..497dcc964 100644 --- a/test/unit/lib/mergeDeep.test.js +++ b/test/unit/lib/mergeDeep.test.js @@ -1314,6 +1314,151 @@ entries: // console.log(`diffs ${JSON.stringify(merged, null, 2)}`) }) + it('Ruleset Compare detects required_reviewers removal without bypass_actors churn', () => { + // Existing ruleset in GitHub: has required_reviewers and a server-defaulted + // allowed_merge_methods. GitHub returns actor_id: null for the OrganizationAdmin + // bypass actor. + const target = { + id: 12345, + name: 'synk', + target: 'branch', + source_type: 'Repository', + source: 'decyjphr-org/test', + enforcement: 'active', + node_id: 'RRS_xxx', + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-02T00:00:00Z', + current_user_can_bypass: 'always', + _links: { self: { href: 'https://api.github.com/repos/x/y/rulesets/12345' } }, + bypass_actors: [ + { actor_id: null, actor_type: 'OrganizationAdmin', bypass_mode: 'pull_request' } + ], + conditions: { ref_name: { exclude: [], include: ['~DEFAULT_BRANCH'] } }, + rules: [ + { + type: 'pull_request', + parameters: { + dismiss_stale_reviews_on_push: true, + require_code_owner_review: false, + require_last_push_approval: false, + required_approving_review_count: 2, + required_review_thread_resolution: false, + required_reviewers: [ + { minimum_approvals: 1, file_patterns: ['*.js'], reviewer: { id: 11721733, type: 'Team' } } + ], + allowed_merge_methods: ['merge', 'squash', 'rebase'] + } + } + ] + } + // Config: required_reviewers removed, OrganizationAdmin bypass actor with explicit id. + const source = { + name: 'synk', + target: 'branch', + enforcement: 'active', + bypass_actors: [ + { actor_id: 1, actor_type: 'OrganizationAdmin', bypass_mode: 'pull_request' } + ], + conditions: { ref_name: { exclude: [], include: ['~DEFAULT_BRANCH'] } }, + rules: [ + { + type: 'pull_request', + parameters: { + dismiss_stale_reviews_on_push: true, + require_code_owner_review: false, + require_last_push_approval: false, + required_approving_review_count: 2, + required_review_thread_resolution: false + } + } + ] + } + const ignorableFields = [] + const mockReturnGitHubContext = jest.fn().mockReturnValue({ + request: () => {} + }) + const mergeDeep = new MergeDeep( + log, + mockReturnGitHubContext, + ignorableFields + ) + const merged = mergeDeep.compareDeep(target, source) + + // The removal of required_reviewers must be detected as a change. + expect(merged.hasChanges).toBeTruthy() + expect(merged.deletions.rules[0].parameters.required_reviewers).toEqual([ + { minimum_approvals: 1, file_patterns: ['*.js'], reviewer: { id: 11721733, type: 'Team' } } + ]) + // The OrganizationAdmin bypass actor (actor_id 1 vs null) must NOT churn. + expect(merged.additions.bypass_actors).toBeUndefined() + expect(merged.deletions.bypass_actors).toBeUndefined() + // allowed_merge_methods is a server-managed default and must NOT be a deletion. + expect(merged.deletions.rules[0].parameters.allowed_merge_methods).toBeUndefined() + }) + + it('Ruleset Compare reports no change when unnamed object array keys are reordered', () => { + // code_scanning_tools elements are keyed by `tool` (not a NAME_FIELD), so they + // fall back to a stable identity. GitHub returns the object keys in a different + // order than config; this must NOT produce spurious add/modify/delete churn. + const target = { + id: 17806629, + name: 'Prevent merges when new SONAR alerts are introduced', + target: 'branch', + source_type: 'Repository', + source: 'decyjphr-emu/test', + enforcement: 'active', + node_id: 'RRS_xxx', + created_at: '2026-06-17T18:45:28.141Z', + updated_at: '2026-06-17T18:45:28.162Z', + current_user_can_bypass: 'always', + _links: { self: { href: 'https://x' }, html: { href: 'https://y' } }, + bypass_actors: [ + { actor_id: null, actor_type: 'OrganizationAdmin', bypass_mode: 'always' } + ], + conditions: { ref_name: { exclude: [], include: ['~DEFAULT_BRANCH'] } }, + rules: [ + { + type: 'code_scanning', + parameters: { + code_scanning_tools: [ + { tool: 'Sonar', security_alerts_threshold: 'medium_or_higher', alerts_threshold: 'none' } + ] + } + } + ] + } + const source = { + name: 'Prevent merges when new SONAR alerts are introduced', + target: 'branch', + enforcement: 'active', + conditions: { ref_name: { include: ['~DEFAULT_BRANCH'], exclude: [] } }, + bypass_actors: [ + { actor_type: 'OrganizationAdmin', bypass_mode: 'always' } + ], + rules: [ + { + type: 'code_scanning', + parameters: { + code_scanning_tools: [ + { tool: 'Sonar', alerts_threshold: 'none', security_alerts_threshold: 'medium_or_higher' } + ] + } + } + ] + } + const ignorableFields = [] + const mockReturnGitHubContext = jest.fn().mockReturnValue({ + request: () => {} + }) + const mergeDeep = new MergeDeep( + log, + mockReturnGitHubContext, + ignorableFields + ) + const merged = mergeDeep.compareDeep(target, source) + expect(merged.hasChanges).toBeFalsy() + }) + it('Ruleset Compare Works when required_status_checks change', () => { const target = [ { @@ -1882,4 +2027,47 @@ branches: expect(same.additions).toEqual({}) expect(same.modifications).toEqual({}) }) + + // Regression test for: TypeError: Cannot assign to read only property '0' of + // object '[object String]'. This was thrown by compareDeepIfVisited when a + // team entry had BOTH a changed primitive field (e.g. `permission`, which + // becomes a modification) AND a field the target lacks entirely (e.g. + // `external_group`, which becomes an addition). addIdentifyingAttribute adds + // the `name` identifying value as a plain string to both the addition and + // the modification containers, and the old merge logic then tried to + // Object.assign(modificationNameString, additionNameString) -- boxing a + // primitive string as the assignment target throws because string indices + // are read-only. + it('CompareDeep does not throw when a team has both a changed field and a new external_group field', () => { + const target = [ + { id: 1, name: 'Azure-Security-GHEC-Runners-Developers', slug: 'azure-security-ghec-runners-developers', permission: 'push', privacy: 'closed' } + ] + const source = [ + { name: 'Azure-Security-GHEC-Runners-Developers', permission: 'admin', external_group: 'Azure-Security-GHEC-Runners-Developers', privacy: 'closed' } + ] + + const ignorableFields = ['id', 'node_id', 'url'] + const mockReturnGitHubContext = jest.fn().mockReturnValue({ + request: () => {} + }) + const mergeDeep = new MergeDeep( + log, + mockReturnGitHubContext, + ignorableFields + ) + + let merged + expect(() => { + merged = mergeDeep.compareDeep(target, source) + }).not.toThrow() + + expect(merged.hasChanges).toBe(true) + expect(merged.modifications).toEqual([ + { + permission: 'admin', + name: 'Azure-Security-GHEC-Runners-Developers', + external_group: 'Azure-Security-GHEC-Runners-Developers' + } + ]) + }) }) diff --git a/test/unit/lib/plugins/appInstallations.test.js b/test/unit/lib/plugins/appInstallations.test.js new file mode 100644 index 000000000..26332ae28 --- /dev/null +++ b/test/unit/lib/plugins/appInstallations.test.js @@ -0,0 +1,393 @@ +const AppInstallations = require('../../../../lib/plugins/appInstallations') + +describe('AppInstallations', () => { + let github + let appGithub + let log + let errors + + beforeEach(() => { + log = { + debug: jest.fn(), + error: jest.fn() + } + errors = [] + + github = { + paginate: jest.fn(), + repos: { + get: jest.fn() + }, + request: jest.fn().mockResolvedValue({ data: {} }) + } + github.request.endpoint = { + merge: jest.fn().mockReturnValue({}) + } + + appGithub = { + paginate: jest.fn(), + request: jest.fn().mockResolvedValue({ data: {} }) + } + appGithub.request.endpoint = { + merge: jest.fn().mockReturnValue({}) + } + }) + + describe('syncDelta', () => { + it('returns empty array for no changes', async () => { + const plugin = new AppInstallations(false, github, appGithub, { owner: 'org', repo: 'admin' }, 'ent', log, errors) + const result = await plugin.syncDelta([]) + expect(result).toEqual([]) + }) + + it('returns empty array for null changes', async () => { + const plugin = new AppInstallations(false, github, appGithub, { owner: 'org', repo: 'admin' }, 'ent', log, errors) + const result = await plugin.syncDelta(null) + expect(result).toEqual([]) + }) + + it('reports error when enterprise client is not configured', async () => { + const plugin = new AppInstallations(true, github, null, { owner: 'org', repo: 'admin' }, null, log, errors) + const result = await plugin.syncDelta([{ + app_slug: 'test-app', + installation_id: 1, + repository_selection: new Set(['repo-a']), + repository_unselection: new Set() + }]) + + expect(result).toHaveLength(1) + expect(result[0].type).toBe('ERROR') + }) + + it('generates NopCommand in nop mode for specific repos', async () => { + // Mock enterprise client listing repos + appGithub.paginate.mockResolvedValue([]) + + const plugin = new AppInstallations(true, github, appGithub, { owner: 'org', repo: 'admin' }, 'ent', log, errors) + const result = await plugin.syncDelta([{ + app_slug: 'copilot', + installation_id: 1, + repository_selection: new Set(['repo-a', 'repo-b']), + repository_unselection: new Set(['repo-c']) + }]) + + expect(result).toHaveLength(1) + expect(result[0].plugin).toBe('app_installations') + expect(result[0].action.additions).toEqual(['repo-a', 'repo-b']) + expect(result[0].action.deletions).toEqual(['repo-c']) + }) + + it('generates NopCommand in nop mode for "all" selection', async () => { + const plugin = new AppInstallations(true, github, appGithub, { owner: 'org', repo: 'admin' }, 'ent', log, errors) + const result = await plugin.syncDelta([{ + app_slug: 'copilot', + installation_id: 1, + repository_selection: 'all', + repository_unselection: new Set() + }]) + + expect(result).toHaveLength(1) + expect(result[0].action.additions).toEqual(['(all repositories)']) + }) + + it('suppresses unselections in additive mode', async () => { + const plugin = new AppInstallations(true, github, appGithub, { owner: 'org', repo: 'admin' }, 'ent', log, errors) + plugin.additive = true + + const result = await plugin.syncDelta([{ + app_slug: 'copilot', + installation_id: 1, + repository_selection: new Set(['repo-a']), + repository_unselection: new Set(['repo-b']) + }]) + + expect(result).toHaveLength(1) + // Should only have additions, no deletions + expect(result[0].action.additions).toEqual(['repo-a']) + expect(result[0].action.deletions).toBeNull() + }) + + it('processes additions before unselections in non-nop mode (422-safe swap)', async () => { + const callOrder = [] + appGithub.request.mockImplementation((route) => { + if (route.includes('/repositories/remove')) callOrder.push('remove') + if (route.includes('/repositories/add')) callOrder.push('add') + return Promise.resolve({ data: {} }) + }) + + const plugin = new AppInstallations(false, github, appGithub, { owner: 'org', repo: 'admin' }, 'ent', log, errors) + await plugin.syncDelta([{ + app_slug: 'copilot', + installation_id: 1, + repository_selection: new Set(['repo-a']), + repository_unselection: new Set(['repo-b']) + }]) + + // Additions are applied before removals so a "swap" never drops the + // installation to zero repos (which the Enterprise API rejects with 422). + // Selection/unselection are already disjoint (dedup), so ordering does not + // affect the final set. + expect(callOrder).toEqual(['add', 'remove']) + }) + + it('records a descriptive error when a delta removal is rejected with 422', async () => { + appGithub.request.mockImplementation((route) => { + if (route.includes('/repositories/remove')) { + return Promise.reject(Object.assign(new Error('Unprocessable'), { status: 422 })) + } + return Promise.resolve({ data: {} }) + }) + + const plugin = new AppInstallations(false, github, appGithub, { owner: 'org', repo: 'admin' }, 'ent', log, errors) + const result = await plugin.syncDelta([{ + app_slug: 'copilot', + installation_id: 1, + repository_selection: new Set(), + repository_unselection: new Set(['repo-a']) + }]) + + // The 422 is caught and recorded, not thrown out of syncDelta. + expect(Array.isArray(result)).toBe(true) + expect(errors.some(e => /422/.test(e.msg))).toBe(true) + }) + + it('accepts array-based selection/unselection (Settings._buildAppChangesFromDelta output shape)', async () => { + // Settings._buildAppChangesFromDelta returns arrays (not Sets). The + // plugin must treat them the same as Sets, otherwise real add/remove + // operations are silently skipped. + const routes = [] + appGithub.request.mockImplementation((route) => { + routes.push(route) + return Promise.resolve({ data: {} }) + }) + + const plugin = new AppInstallations(false, github, appGithub, { owner: 'org', repo: 'admin' }, 'ent', log, errors) + await plugin.syncDelta([{ + app_slug: 'copilot', + installation_id: 1, + repository_selection: ['repo-a'], + repository_unselection: ['repo-b'] + }]) + + expect(routes.some(r => r.includes('/repositories/add'))).toBe(true) + expect(routes.some(r => r.includes('/repositories/remove'))).toBe(true) + }) + + it('generates NopCommand for array-based selection/unselection in nop mode', async () => { + const plugin = new AppInstallations(true, github, appGithub, { owner: 'org', repo: 'admin' }, 'ent', log, errors) + const result = await plugin.syncDelta([{ + app_slug: 'copilot', + installation_id: 1, + repository_selection: ['repo-a', 'repo-b'], + repository_unselection: ['repo-c'] + }]) + + expect(result).toHaveLength(1) + expect(result[0].action.additions).toEqual(['repo-a', 'repo-b']) + expect(result[0].action.deletions).toEqual(['repo-c']) + }) + }) + + describe('syncFull', () => { + it('returns empty array for no desired state', async () => { + const plugin = new AppInstallations(false, github, appGithub, { owner: 'org', repo: 'admin' }, 'ent', log, errors) + const result = await plugin.syncFull({}) + expect(result).toEqual([]) + }) + + it('reports error when enterprise client is missing', async () => { + const plugin = new AppInstallations(true, github, null, { owner: 'org', repo: 'admin' }, null, log, errors) + const result = await plugin.syncFull({ + copilot: { installation_id: 1, repos: new Set(['repo-a']) } + }) + expect(result).toHaveLength(1) + expect(result[0].type).toBe('ERROR') + }) + + it('generates NopCommand with additions and deletions in nop mode', async () => { + // Mock listInstallationRepos (live state) + appGithub.paginate.mockResolvedValue([ + { name: 'existing-repo', id: 10 }, + { name: 'stale-repo', id: 20 } + ]) + + const plugin = new AppInstallations(true, github, appGithub, { owner: 'org', repo: 'admin' }, 'ent', log, errors) + const result = await plugin.syncFull({ + copilot: { + installation_id: 1, + repos: new Set(['existing-repo', 'new-repo']) + } + }) + + expect(result).toHaveLength(1) + expect(result[0].action.additions).toEqual(['new-repo']) + expect(result[0].action.deletions).toEqual(['stale-repo']) + }) + + it('suppresses deletions in additive mode during full sync', async () => { + appGithub.paginate.mockResolvedValue([ + { name: 'existing-repo', id: 10 }, + { name: 'stale-repo', id: 20 } + ]) + + const plugin = new AppInstallations(true, github, appGithub, { owner: 'org', repo: 'admin' }, 'ent', log, errors) + plugin.additive = true + + const result = await plugin.syncFull({ + copilot: { + installation_id: 1, + repos: new Set(['existing-repo', 'new-repo']) + } + }) + + expect(result).toHaveLength(1) + expect(result[0].action.additions).toEqual(['new-repo']) + expect(result[0].action.deletions).toBeNull() + }) + + it('skips app when no changes needed', async () => { + appGithub.paginate.mockResolvedValue([ + { name: 'repo-a', id: 10 } + ]) + + const plugin = new AppInstallations(true, github, appGithub, { owner: 'org', repo: 'admin' }, 'ent', log, errors) + const result = await plugin.syncFull({ + copilot: { + installation_id: 1, + repos: new Set(['repo-a']) + } + }) + + expect(result).toEqual([]) + }) + + it("toggles to 'all' when desired is all and current is selected", async () => { + const plugin = new AppInstallations(false, github, appGithub, { owner: 'org', repo: 'admin' }, 'ent', log, errors) + await plugin.syncFull({ + copilot: { installation_id: 1, repos: 'all', current_selection: 'selected' } + }) + + expect(appGithub.request).toHaveBeenCalledWith( + expect.stringContaining('/repositories'), + expect.objectContaining({ repository_selection: 'all', installation_id: 1 }) + ) + }) + + it('skips when desired is all and current is already all', async () => { + const plugin = new AppInstallations(false, github, appGithub, { owner: 'org', repo: 'admin' }, 'ent', log, errors) + const result = await plugin.syncFull({ + copilot: { installation_id: 1, repos: 'all', current_selection: 'all' } + }) + + expect(result).toEqual([]) + expect(appGithub.request).not.toHaveBeenCalled() + }) + + it("narrows from 'all' to 'selected' when desired is a set", async () => { + const plugin = new AppInstallations(false, github, appGithub, { owner: 'org', repo: 'admin' }, 'ent', log, errors) + await plugin.syncFull({ + copilot: { installation_id: 1, repos: new Set(['repo-a']), current_selection: 'all' } + }) + + expect(appGithub.request).toHaveBeenCalledWith( + expect.stringContaining('/repositories'), + expect.objectContaining({ repository_selection: 'selected', repositories: ['repo-a'] }) + ) + }) + + it("leaves 'all' untouched in additive mode", async () => { + const plugin = new AppInstallations(false, github, appGithub, { owner: 'org', repo: 'admin' }, 'ent', log, errors) + plugin.additive = true + const result = await plugin.syncFull({ + copilot: { installation_id: 1, repos: new Set(['repo-a']), current_selection: 'all' } + }) + + expect(result).toEqual([]) + expect(appGithub.request).not.toHaveBeenCalled() + }) + + it('applies additions before removals to avoid a transient empty selection (422-safe swap)', async () => { + // live = {repo-a}; desired = {repo-b} → swap. Removing first could drop the + // installation to zero repos (422); additions must be applied first. + appGithub.paginate.mockResolvedValue([{ name: 'repo-a', id: 10 }]) + const routes = [] + appGithub.request.mockImplementation((route) => { + routes.push(route) + return Promise.resolve({ data: {} }) + }) + + const plugin = new AppInstallations(false, github, appGithub, { owner: 'org', repo: 'admin' }, 'ent', log, errors) + await plugin.syncFull({ + copilot: { installation_id: 1, repos: new Set(['repo-b']), current_selection: 'selected' } + }) + + const addIdx = routes.findIndex(r => r.includes('/repositories/add')) + const removeIdx = routes.findIndex(r => r.includes('/repositories/remove')) + expect(addIdx).toBeGreaterThanOrEqual(0) + expect(removeIdx).toBeGreaterThanOrEqual(0) + expect(addIdx).toBeLessThan(removeIdx) + }) + + it("errors (without mutating) when the desired repo set is empty for a 'selected' installation", async () => { + appGithub.paginate.mockResolvedValue([{ name: 'repo-a', id: 10 }]) + + const plugin = new AppInstallations(true, github, appGithub, { owner: 'org', repo: 'admin' }, 'ent', log, errors) + const result = await plugin.syncFull({ + copilot: { installation_id: 1, repos: new Set(), current_selection: 'selected' } + }) + + expect(result).toHaveLength(1) + expect(result[0].type).toBe('ERROR') + expect(errors.some(e => /zero repositories/i.test(e.msg))).toBe(true) + expect(appGithub.request).not.toHaveBeenCalled() + }) + + it('records a descriptive error when removal is rejected with 422', async () => { + appGithub.paginate.mockResolvedValue([ + { name: 'repo-a', id: 10 }, + { name: 'repo-b', id: 20 } + ]) + appGithub.request.mockImplementation((route) => { + if (route.includes('/repositories/remove')) { + return Promise.reject(Object.assign(new Error('Unprocessable'), { status: 422 })) + } + return Promise.resolve({ data: {} }) + }) + + const plugin = new AppInstallations(false, github, appGithub, { owner: 'org', repo: 'admin' }, 'ent', log, errors) + // desired = {repo-a}; live = {repo-a, repo-b} → toRemove = [repo-b] + const result = await plugin.syncFull({ + copilot: { installation_id: 1, repos: new Set(['repo-a']), current_selection: 'selected' } + }) + + // The 422 is caught and recorded, not thrown out of syncFull. + expect(Array.isArray(result)).toBe(true) + expect(errors.some(e => /422/.test(e.msg))).toBe(true) + }) + + it("errors (without mutating) when narrowing 'all' to an empty desired set", async () => { + const plugin = new AppInstallations(true, github, appGithub, { owner: 'org', repo: 'admin' }, 'ent', log, errors) + const result = await plugin.syncFull({ + copilot: { installation_id: 1, repos: new Set(), current_selection: 'all' } + }) + + expect(result).toHaveLength(1) + expect(result[0].type).toBe('ERROR') + expect(errors.some(e => /narrow repository_selection from 'all'/.test(e.msg))).toBe(true) + // The over-broad 'all' installation must be left unchanged. + expect(appGithub.request).not.toHaveBeenCalled() + }) + + it("leaves 'all' untouched (no error) for an empty desired set in additive mode", async () => { + const plugin = new AppInstallations(true, github, appGithub, { owner: 'org', repo: 'admin' }, 'ent', log, errors) + plugin.additive = true + const result = await plugin.syncFull({ + copilot: { installation_id: 1, repos: new Set(), current_selection: 'all' } + }) + + expect(result).toEqual([]) + expect(errors).toEqual([]) + expect(appGithub.request).not.toHaveBeenCalled() + }) + }) +}) diff --git a/test/unit/lib/plugins/archive.test.js b/test/unit/lib/plugins/archive.test.js index 9aa690442..0ed0f38d1 100644 --- a/test/unit/lib/plugins/archive.test.js +++ b/test/unit/lib/plugins/archive.test.js @@ -10,9 +10,11 @@ describe('Archive Plugin', () => { beforeEach(() => { github = { - repos: { - get: jest.fn(), - update: jest.fn().mockReturnValue({ data: {} }) + rest: { + repos: { + get: jest.fn(), + update: jest.fn().mockReturnValue({ data: {} }) + } } } repo = { owner: 'test-owner', repo: 'test-repo' } @@ -23,19 +25,19 @@ describe('Archive Plugin', () => { describe('getRepo', () => { it('returns repository data when found', async () => { const mockData = { archived: false } - github.repos.get.mockResolvedValue({ data: mockData }) + github.rest.repos.get.mockResolvedValue({ data: mockData }) archive = new Archive(false, github, repo, settings, log) const result = await archive.getRepo() expect(result).toEqual(mockData) - expect(github.repos.get).toHaveBeenCalledWith({ + expect(github.rest.repos.get).toHaveBeenCalledWith({ owner: repo.owner, repo: repo.repo }) }) it('returns null when repo not found and no archive state defined', async () => { - github.repos.get.mockRejectedValue({ status: 404 }) + github.rest.repos.get.mockRejectedValue({ status: 404 }) archive = new Archive(false, github, repo, settings, log) const result = await archive.getRepo() @@ -44,7 +46,7 @@ describe('Archive Plugin', () => { it('throws error for non-404 errors', async () => { const error = { status: 500 } - github.repos.get.mockRejectedValue(error) + github.rest.repos.get.mockRejectedValue(error) archive = new Archive(false, github, repo, settings, log) await expect(archive.getRepo()).rejects.toEqual(error) @@ -55,7 +57,7 @@ describe('Archive Plugin', () => { it('returns NopCommand when nop is true', async () => { archive = new Archive(true, github, repo, settings, log) const mockEndpoint = { method: 'PATCH', url: '/repos/{owner}/{repo}' } - github.repos.update.endpoint = jest.fn().mockReturnValue(mockEndpoint) + github.rest.repos.update.endpoint = jest.fn().mockReturnValue(mockEndpoint) const result = await archive.updateRepoArchiveStatus(true) expect(result).toBeInstanceOf(NopCommand) @@ -65,7 +67,7 @@ describe('Archive Plugin', () => { archive = new Archive(false, github, repo, settings, log) await archive.updateRepoArchiveStatus(true) - expect(github.repos.update).toHaveBeenCalledWith({ + expect(github.rest.repos.update).toHaveBeenCalledWith({ owner: repo.owner, repo: repo.repo, archived: true @@ -97,7 +99,7 @@ describe('Archive Plugin', () => { }) it('returns empty results when no archive changes needed', async () => { - github.repos.get.mockResolvedValue({ + github.rest.repos.get.mockResolvedValue({ data: { archived: false } }) settings.archived = false @@ -108,14 +110,14 @@ describe('Archive Plugin', () => { }) it('archives repo when shouldArchive is true', async () => { - github.repos.get.mockResolvedValue({ + github.rest.repos.get.mockResolvedValue({ data: { archived: false } }) settings.archived = true const results = await archive.sync() expect(results).toHaveLength(1) - expect(github.repos.update).toHaveBeenCalledWith({ + expect(github.rest.repos.update).toHaveBeenCalledWith({ owner: repo.owner, repo: repo.repo, archived: true diff --git a/test/unit/lib/plugins/autolinks.test.js b/test/unit/lib/plugins/autolinks.test.js index 10413cc1a..02829daa0 100644 --- a/test/unit/lib/plugins/autolinks.test.js +++ b/test/unit/lib/plugins/autolinks.test.js @@ -13,10 +13,12 @@ describe('Autolinks', () => { beforeEach(() => { github = { - repos: { - listAutolinks: jest.fn().mockResolvedValue([]), - createAutolink: jest.fn().mockResolvedValue(), - deleteAutolink: jest.fn().mockResolvedValue() + rest: { + repos: { + listAutolinks: jest.fn().mockResolvedValue([]), + createAutolink: jest.fn().mockResolvedValue(), + deleteAutolink: jest.fn().mockResolvedValue() + } } } }) @@ -35,7 +37,7 @@ describe('Autolinks', () => { { key_prefix: 'NEW_ALPHA-TRUE-', url_template: 'https://test/', is_alphanumeric: true } ]) - github.repos.listAutolinks.mockResolvedValueOnce({ + github.rest.repos.listAutolinks.mockResolvedValueOnce({ data: [ { id: '1', key_prefix: 'SAME-', url_template: 'https://test/', is_alphanumeric: true }, { id: '2', key_prefix: 'REMOVE-', url_template: 'https://test/', is_alphanumeric: true }, @@ -50,97 +52,97 @@ describe('Autolinks', () => { }) return plugin.sync().then(() => { - expect(github.repos.createAutolink).toHaveBeenCalledWith({ + expect(github.rest.repos.createAutolink).toHaveBeenCalledWith({ key_prefix: 'ADD-', url_template: 'https://test/', is_alphanumeric: true, ...repo }) - expect(github.repos.deleteAutolink).toHaveBeenCalledWith({ + expect(github.rest.repos.deleteAutolink).toHaveBeenCalledWith({ autolink_id: '2', ...repo }) - expect(github.repos.deleteAutolink).toHaveBeenCalledWith({ + expect(github.rest.repos.deleteAutolink).toHaveBeenCalledWith({ autolink_id: '3', ...repo }) - expect(github.repos.createAutolink).toHaveBeenCalledWith({ + expect(github.rest.repos.createAutolink).toHaveBeenCalledWith({ key_prefix: 'NEW_URL-', url_template: 'https://new-url/', is_alphanumeric: true, ...repo }) - expect(github.repos.deleteAutolink).not.toHaveBeenCalledWith({ + expect(github.rest.repos.deleteAutolink).not.toHaveBeenCalledWith({ autolink_id: '4', ...repo }) - expect(github.repos.createAutolink).not.toHaveBeenCalledWith({ + expect(github.rest.repos.createAutolink).not.toHaveBeenCalledWith({ key_prefix: 'SAME_ALPHA-UNDEFINED-', url_template: 'https://test/', is_alphanumeric: true, ...repo }) - expect(github.repos.deleteAutolink).not.toHaveBeenCalledWith({ + expect(github.rest.repos.deleteAutolink).not.toHaveBeenCalledWith({ autolink_id: '5', ...repo }) - expect(github.repos.createAutolink).not.toHaveBeenCalledWith({ + expect(github.rest.repos.createAutolink).not.toHaveBeenCalledWith({ key_prefix: 'SAME_ALPHA-FALSE-', url_template: 'https://test/', is_alphanumeric: false, ...repo }) - expect(github.repos.deleteAutolink).not.toHaveBeenCalledWith({ + expect(github.rest.repos.deleteAutolink).not.toHaveBeenCalledWith({ autolink_id: '6', ...repo }) - expect(github.repos.createAutolink).not.toHaveBeenCalledWith({ + expect(github.rest.repos.createAutolink).not.toHaveBeenCalledWith({ key_prefix: 'SAME_ALPHA-TRUE-', url_template: 'https://test/', is_alphanumeric: true, ...repo }) - expect(github.repos.deleteAutolink).toHaveBeenCalledWith({ + expect(github.rest.repos.deleteAutolink).toHaveBeenCalledWith({ autolink_id: '7', ...repo }) - expect(github.repos.createAutolink).toHaveBeenCalledWith({ + expect(github.rest.repos.createAutolink).toHaveBeenCalledWith({ key_prefix: 'NEW_ALPHA-UNDEFINED-', url_template: 'https://test/', is_alphanumeric: true, ...repo }) - expect(github.repos.deleteAutolink).toHaveBeenCalledWith({ + expect(github.rest.repos.deleteAutolink).toHaveBeenCalledWith({ autolink_id: '8', ...repo }) - expect(github.repos.createAutolink).toHaveBeenCalledWith({ + expect(github.rest.repos.createAutolink).toHaveBeenCalledWith({ key_prefix: 'NEW_ALPHA-FALSE-', url_template: 'https://test/', is_alphanumeric: false, ...repo }) - expect(github.repos.deleteAutolink).toHaveBeenCalledWith({ + expect(github.rest.repos.deleteAutolink).toHaveBeenCalledWith({ autolink_id: '9', ...repo }) - expect(github.repos.createAutolink).toHaveBeenCalledWith({ + expect(github.rest.repos.createAutolink).toHaveBeenCalledWith({ key_prefix: 'NEW_ALPHA-TRUE-', url_template: 'https://test/', is_alphanumeric: true, ...repo }) - expect(github.repos.deleteAutolink).toHaveBeenCalledTimes(5) - expect(github.repos.createAutolink).toHaveBeenCalledTimes(5) + expect(github.rest.repos.deleteAutolink).toHaveBeenCalledTimes(5) + expect(github.rest.repos.createAutolink).toHaveBeenCalledTimes(5) }) }) }) diff --git a/test/unit/lib/plugins/branches.test.js b/test/unit/lib/plugins/branches.test.js index 4b3683f34..f6aa2d0ac 100644 --- a/test/unit/lib/plugins/branches.test.js +++ b/test/unit/lib/plugins/branches.test.js @@ -17,19 +17,21 @@ describe('Branches', () => { beforeEach(() => { github = { - repos: { - get: jest.fn().mockResolvedValue({ - data: { - default_branch: 'main' - } - }), - getBranchProtection: jest.fn().mockResolvedValue({ - data: { - enforce_admins: { enabled: false } - } - }), - updateBranchProtection: jest.fn().mockImplementation(() => Promise.resolve('updateBranchProtection')), - deleteBranchProtection: jest.fn().mockImplementation(() => Promise.resolve('deleteBranchProtection')) + rest: { + repos: { + get: jest.fn().mockResolvedValue({ + data: { + default_branch: 'main' + } + }), + getBranchProtection: jest.fn().mockResolvedValue({ + data: { + enforce_admins: { enabled: false } + } + }), + updateBranchProtection: jest.fn().mockImplementation(() => Promise.resolve('updateBranchProtection')), + deleteBranchProtection: jest.fn().mockImplementation(() => Promise.resolve('deleteBranchProtection')) + } } } }) @@ -53,7 +55,7 @@ describe('Branches', () => { ) return plugin.sync().then(() => { - expect(github.repos.updateBranchProtection).toHaveBeenCalledWith({ + expect(github.rest.repos.updateBranchProtection).toHaveBeenCalledWith({ owner: 'bkeepers', repo: 'test', branch: 'master', @@ -65,6 +67,7 @@ describe('Branches', () => { required_pull_request_reviews: { require_code_owner_reviews: true }, + restrictions: null, headers: { accept: 'application/vnd.github.hellcat-preview+json,application/vnd.github.luke-cage-preview+json,application/vnd.github.zzzax-preview+json' } }) }) @@ -80,8 +83,8 @@ describe('Branches', () => { ) return plugin.sync().then(() => { - expect(github.repos.updateBranchProtection).not.toHaveBeenCalled() - expect(github.repos.deleteBranchProtection).toHaveBeenCalledWith({ + expect(github.rest.repos.updateBranchProtection).not.toHaveBeenCalled() + expect(github.rest.repos.deleteBranchProtection).toHaveBeenCalledWith({ owner: 'bkeepers', repo: 'test', branch: 'master' @@ -100,8 +103,8 @@ describe('Branches', () => { ) return plugin.sync().then(() => { - expect(github.repos.updateBranchProtection).not.toHaveBeenCalled() - expect(github.repos.deleteBranchProtection).toHaveBeenCalledWith({ + expect(github.rest.repos.updateBranchProtection).not.toHaveBeenCalled() + expect(github.rest.repos.deleteBranchProtection).toHaveBeenCalledWith({ owner: 'bkeepers', repo: 'test', branch: 'master' @@ -120,8 +123,8 @@ describe('Branches', () => { ) return plugin.sync().then(() => { - expect(github.repos.updateBranchProtection).not.toHaveBeenCalled() - expect(github.repos.deleteBranchProtection).toHaveBeenCalledWith({ + expect(github.rest.repos.updateBranchProtection).not.toHaveBeenCalled() + expect(github.rest.repos.deleteBranchProtection).toHaveBeenCalledWith({ owner: 'bkeepers', repo: 'test', branch: 'master' @@ -140,8 +143,8 @@ describe('Branches', () => { ) return plugin.sync().then(() => { - expect(github.repos.updateBranchProtection).not.toHaveBeenCalled() - expect(github.repos.deleteBranchProtection).toHaveBeenCalledWith({ + expect(github.rest.repos.updateBranchProtection).not.toHaveBeenCalled() + expect(github.rest.repos.deleteBranchProtection).toHaveBeenCalledWith({ owner: 'bkeepers', repo: 'test', branch: 'master' @@ -159,8 +162,8 @@ describe('Branches', () => { ) return plugin.sync().then(() => { - expect(github.repos.updateBranchProtection).not.toHaveBeenCalled() - expect(github.repos.deleteBranchProtection).not.toHaveBeenCalled() + expect(github.rest.repos.updateBranchProtection).not.toHaveBeenCalled() + expect(github.rest.repos.deleteBranchProtection).not.toHaveBeenCalled() }) }) }) @@ -180,7 +183,8 @@ describe('Branches', () => { ) return plugin.sync().then(() => { - expect(github.repos.updateBranchProtection).toHaveBeenCalledWith({ + + expect(github.rest.repos.updateBranchProtection).toHaveBeenCalledWith(expect.objectContaining({ owner: 'bkeepers', repo: 'test', branch: 'main', @@ -188,21 +192,103 @@ describe('Branches', () => { strict: true, contexts: [] }, + // Existing enforce_admins should be preserved from GitHub + enforce_admins: false, + restrictions: null, headers: { accept: 'application/vnd.github.hellcat-preview+json,application/vnd.github.luke-cage-preview+json,application/vnd.github.zzzax-preview+json' } + })) + }) + }) + }) + + describe('when existing protection has restrictions', () => { + it('preserves restrictions from GitHub when config omits them', () => { + github.rest.repos.getBranchProtection = jest.fn().mockResolvedValue({ + data: { + enforce_admins: { enabled: true }, + required_status_checks: { + strict: false, + contexts: ['ci-check'], + checks: [] + }, + restrictions: { + url: 'https://api.github.com/...', + users: [{ login: 'user1' }, { login: 'user2' }], + teams: [{ slug: 'team-a' }], + apps: [{ slug: 'app-bot' }] + } + } + }) + + // Config only specifies enforce_admins, omits restrictions + const plugin = configure([{ + name: 'main', + protection: { + enforce_admins: false + } + }]) + + return plugin.sync().then(() => { + expect(github.rest.repos.updateBranchProtection).toHaveBeenCalledWith( + expect.objectContaining({ + owner: 'bkeepers', + repo: 'test', + branch: 'main', + enforce_admins: false, + // Existing restrictions should be preserved from GitHub + restrictions: { + users: ['user1', 'user2'], + teams: ['team-a'], + apps: ['app-bot'] + }, + // Existing required_status_checks should be preserved from GitHub + required_status_checks: { + strict: false, + contexts: ['ci-check'], + checks: [] + } + }) + ) + }) + }) + + it('normalizes restrictions and defaults missing arrays when preserving from GitHub', () => { + github.rest.repos.getBranchProtection = jest.fn().mockResolvedValue({ + data: { + enforce_admins: { enabled: true }, + restrictions: { + url: 'https://api.github.com/...', + users: [{ login: 'user1' }] + } + } + }) + + const plugin = configure([{ + name: 'main', + protection: { + enforce_admins: false + } + }]) + + return plugin.sync().then(() => { + const payload = github.rest.repos.updateBranchProtection.mock.calls[0][0] + expect(payload.restrictions).toEqual({ + users: ['user1'], + teams: [], + apps: [] }) + expect(payload.restrictions.url).toBeUndefined() }) }) }) describe('when {{EXTERNALLY_DEFINED}} is present in "required_status_checks" and status checks exist in GitHub', () => { it('it retains the status checks from GitHub', () => { - github.repos.getBranchProtection = jest.fn().mockResolvedValue({ + github.rest.repos.getBranchProtection = jest.fn().mockResolvedValue({ data: { enforce_admins: { enabled: false }, - protection: { - required_status_checks: { - contexts: ['check-1', 'check-2'] - } + required_status_checks: { + contexts: ['check-1', 'check-2'] } } }) @@ -219,7 +305,8 @@ describe('Branches', () => { ) return plugin.sync().then(() => { - expect(github.repos.updateBranchProtection).toHaveBeenCalledWith({ + + expect(github.rest.repos.updateBranchProtection).toHaveBeenCalledWith(expect.objectContaining({ owner: 'bkeepers', repo: 'test', branch: 'main', @@ -227,8 +314,10 @@ describe('Branches', () => { strict: true, contexts: ['check-1', 'check-2'] }, + enforce_admins: false, + restrictions: null, headers: { accept: 'application/vnd.github.hellcat-preview+json,application/vnd.github.luke-cage-preview+json,application/vnd.github.zzzax-preview+json' } - }) + })) }) }) }) @@ -248,7 +337,7 @@ describe('Branches', () => { ] ) - when(github.repos.getBranchProtection) + when(github.rest.repos.getBranchProtection) .calledWith(expect.objectContaining({ branch: 'other' })).mockResolvedValue({ @@ -258,13 +347,15 @@ describe('Branches', () => { }) return plugin.sync().then(() => { - expect(github.repos.updateBranchProtection).toHaveBeenCalledTimes(2) + expect(github.rest.repos.updateBranchProtection).toHaveBeenCalledTimes(2) - expect(github.repos.updateBranchProtection).toHaveBeenLastCalledWith({ + expect(github.rest.repos.updateBranchProtection).toHaveBeenLastCalledWith({ owner: 'bkeepers', repo: 'test', branch: 'other', enforce_admins: false, + required_status_checks: null, + restrictions: null, headers: { accept: 'application/vnd.github.hellcat-preview+json,application/vnd.github.luke-cage-preview+json,application/vnd.github.zzzax-preview+json' } }) }) @@ -272,6 +363,60 @@ describe('Branches', () => { }) }) + describe('in nop mode', () => { + function configureNop (config) { + return new Branches(true, github, { owner: 'bkeepers', repo: 'test' }, config, log, []) + } + + beforeEach(() => { + github.rest.repos.updateBranchProtection.endpoint = jest.fn().mockImplementation(params => { + return { url: 'updateBranchProtection', body: params } + }) + github.rest.repos.deleteBranchProtection.endpoint = jest.fn().mockImplementation(params => { + return { url: 'deleteBranchProtection', body: params } + }) + }) + + describe('when branch protection already exists', () => { + it('labels the NopCommand as an update and names the branch in the diff message', () => { + const plugin = configureNop( + [{ + name: 'master', + protection: { enforce_admins: true } + }] + ) + + return plugin.sync().then(res => { + const messages = res.map(nopCommand => nopCommand.action.msg) + expect(messages).toContain('Update Branch Protection') + expect(messages).not.toContain('Add Branch Protection') + const diffMessage = messages.find(msg => typeof msg === 'string' && msg.includes('will be applied to the branch protection')) + expect(diffMessage).toBeDefined() + expect(diffMessage).toContain('for master branch') + expect(diffMessage).not.toContain('undefined') + }) + }) + }) + + describe('when branch protection does not exist yet', () => { + it('labels the NopCommand as an add', () => { + github.rest.repos.getBranchProtection = jest.fn().mockRejectedValue({ status: 404 }) + const plugin = configureNop( + [{ + name: 'master', + protection: { enforce_admins: true } + }] + ) + + return plugin.sync().then(res => { + const messages = res.map(nopCommand => nopCommand.action.msg) + expect(messages).toContain('Add Branch Protection') + expect(messages).not.toContain('Update Branch Protection') + }) + }) + }) + }) + describe.skip('return values', () => { it('returns updateBranchProtection Promise', () => { const plugin = configure( diff --git a/test/unit/lib/plugins/collaborators.test.js b/test/unit/lib/plugins/collaborators.test.js index 359dd4614..f01007953 100644 --- a/test/unit/lib/plugins/collaborators.test.js +++ b/test/unit/lib/plugins/collaborators.test.js @@ -10,13 +10,15 @@ describe('Collaborators', () => { beforeEach(() => { github = { - repos: { - listInvitations: jest.fn().mockResolvedValue([]), - deleteInvitation: jest.fn().mockResolvedValue(), - updateInvitation: jest.fn().mockResolvedValue(), - listCollaborators: jest.fn().mockResolvedValue([]), - removeCollaborator: jest.fn().mockResolvedValue(), - addCollaborator: jest.fn().mockResolvedValue() + rest: { + repos: { + listInvitations: jest.fn().mockResolvedValue([]), + deleteInvitation: jest.fn().mockResolvedValue(), + updateInvitation: jest.fn().mockResolvedValue(), + listCollaborators: jest.fn().mockResolvedValue([]), + removeCollaborator: jest.fn().mockResolvedValue(), + addCollaborator: jest.fn().mockResolvedValue() + } } } }) @@ -30,7 +32,7 @@ describe('Collaborators', () => { { username: 'DIFFERENTcase', permission: 'push' } ]) - github.repos.listCollaborators.mockResolvedValueOnce({ + github.rest.repos.listCollaborators.mockResolvedValueOnce({ data: [ { login: 'bkeepers', permissions: { admin: true, push: true, pull: true } }, { login: 'updated-permission', permissions: { admin: false, push: false, pull: true } }, @@ -40,29 +42,29 @@ describe('Collaborators', () => { }) return plugin.sync().then(() => { - expect(github.repos.addCollaborator).toHaveBeenCalledWith({ + expect(github.rest.repos.addCollaborator).toHaveBeenCalledWith({ owner: 'bkeepers', repo: 'test', username: 'added-user', permission: 'push' }) - expect(github.repos.addCollaborator).toHaveBeenCalledWith({ + expect(github.rest.repos.addCollaborator).toHaveBeenCalledWith({ owner: 'bkeepers', repo: 'test', username: 'updated-permission', permission: 'push' }) - expect(github.repos.addCollaborator).toHaveBeenCalledTimes(2) + expect(github.rest.repos.addCollaborator).toHaveBeenCalledTimes(2) - expect(github.repos.removeCollaborator).toHaveBeenCalledWith({ + expect(github.rest.repos.removeCollaborator).toHaveBeenCalledWith({ owner: 'bkeepers', repo: 'test', username: 'removed-user' }) - expect(github.repos.removeCollaborator).toHaveBeenCalledTimes(1) + expect(github.rest.repos.removeCollaborator).toHaveBeenCalledTimes(1) }) }) }) diff --git a/test/unit/lib/plugins/custom_properties.test.js b/test/unit/lib/plugins/custom_properties.test.js index 429544f8a..55fbbf740 100644 --- a/test/unit/lib/plugins/custom_properties.test.js +++ b/test/unit/lib/plugins/custom_properties.test.js @@ -15,21 +15,23 @@ describe('CustomProperties', () => { beforeEach(() => { github = { paginate: jest.fn(), - repos: { - getCustomPropertiesValues: jest.fn(), - createOrUpdateCustomPropertiesValues: jest.fn() - } + request: jest.fn() } log = { debug: jest.fn(), error: console.error } }) describe('Custom Properties plugin', () => { - it('should normalize entries when be instantiated', () => { + it('should normalize entries when instantiated', () => { const plugin = configure([{ name: 'Test', value: 'test' }]) expect(plugin.entries).toEqual([{ name: 'test', value: 'test' }]) }) + it('should normalize entries with property_name when instantiated', () => { + const plugin = configure([{ property_name: 'ent-ownership', value: 'expert-services' }]) + expect(plugin.entries).toEqual([{ name: 'ent-ownership', value: 'expert-services' }]) + }) + it('should fetch and normalize custom properties successfully', async () => { const mockResponse = [ { property_name: 'Test1', value: 'value1' }, @@ -42,7 +44,7 @@ describe('CustomProperties', () => { const result = await plugin.find() expect(github.paginate).toHaveBeenCalledWith( - github.repos.getCustomPropertiesValues, + 'GET /repos/{owner}/{repo}/properties/values', { owner, repo, @@ -56,6 +58,24 @@ describe('CustomProperties', () => { ]) }) + it('should normalize paginated custom properties when property name shape differs', async () => { + const mockResponse = [ + { name: 'Owner', value: 'My Team' }, + { property_name: 'Criticality', value: 'High' }, + { value: 'ignored' } + ] + + github.paginate.mockResolvedValue(mockResponse) + + const plugin = configure() + const result = await plugin.find() + + expect(result).toEqual([ + { name: 'owner', value: 'My Team' }, + { name: 'criticality', value: 'High' } + ]) + }) + it('should sync', async () => { const mockResponse = [ { property_name: 'no-change', value: 'no-change' }, @@ -75,14 +95,14 @@ describe('CustomProperties', () => { return plugin.sync().then(() => { expect(github.paginate).toHaveBeenCalledWith( - github.repos.getCustomPropertiesValues, + 'GET /repos/{owner}/{repo}/properties/values', { owner, repo, per_page: 100 } ) - expect(github.repos.createOrUpdateCustomPropertiesValues).not.toHaveBeenCalledWith({ + expect(github.request).not.toHaveBeenCalledWith('PATCH /repos/{owner}/{repo}/properties/values', { owner, repo, properties: [ @@ -92,7 +112,7 @@ describe('CustomProperties', () => { } ] }) - expect(github.repos.createOrUpdateCustomPropertiesValues).toHaveBeenCalledWith({ + expect(github.request).toHaveBeenCalledWith('PATCH /repos/{owner}/{repo}/properties/values', { owner, repo, properties: [ @@ -102,7 +122,7 @@ describe('CustomProperties', () => { } ] }) - expect(github.repos.createOrUpdateCustomPropertiesValues).toHaveBeenCalledWith({ + expect(github.request).toHaveBeenCalledWith('PATCH /repos/{owner}/{repo}/properties/values', { owner, repo, properties: [ @@ -112,7 +132,7 @@ describe('CustomProperties', () => { } ] }) - expect(github.repos.createOrUpdateCustomPropertiesValues).toHaveBeenCalledWith({ + expect(github.request).toHaveBeenCalledWith('PATCH /repos/{owner}/{repo}/properties/values', { owner, repo, properties: [ @@ -127,7 +147,7 @@ describe('CustomProperties', () => { // const plugin = configure([{ name: 'Test', value: 'test' }]) // await plugin.update({ name: 'test', value: 'old' }, { name: 'test', value: 'test' }) - // expect(github.repos.createOrUpdateCustomPropertiesValues).toHaveBeenCalledWith({ + // expect(github.request).toHaveBeenCalledWith('PATCH /repos/{owner}/{repo}/properties/values', { // owner, // repo, // properties: [ diff --git a/test/unit/lib/plugins/labels.test.js b/test/unit/lib/plugins/labels.test.js index 71eaf2c8b..1d1bd51b1 100644 --- a/test/unit/lib/plugins/labels.test.js +++ b/test/unit/lib/plugins/labels.test.js @@ -12,18 +12,20 @@ describe('Labels', () => { beforeEach(() => { github = { paginate: jest.fn().mockImplementation(() => Promise.resolve()), - repos: { - get: jest.fn().mockImplementation(() => Promise.resolve({})) - }, - issues: { - listLabelsForRepo: { - endpoint: { - merge: jest.fn().mockImplementation(() => {}) - } + rest: { + repos: { + get: jest.fn().mockImplementation(() => Promise.resolve({})) }, - createLabel: jest.fn().mockImplementation(() => Promise.resolve()), - deleteLabel: jest.fn().mockImplementation(() => Promise.resolve()), - updateLabel: jest.fn().mockImplementation(() => Promise.resolve()) + issues: { + listLabelsForRepo: { + endpoint: { + merge: jest.fn().mockImplementation(() => {}) + } + }, + createLabel: jest.fn().mockImplementation(() => Promise.resolve()), + deleteLabel: jest.fn().mockImplementation(() => Promise.resolve()), + updateLabel: jest.fn().mockImplementation(() => Promise.resolve()) + } } } log = { debug: jest.fn(), error: console.error } @@ -48,14 +50,14 @@ describe('Labels', () => { ]) return plugin.sync().then(() => { - expect(github.issues.deleteLabel).toHaveBeenCalledWith({ + expect(github.rest.issues.deleteLabel).toHaveBeenCalledWith({ owner: 'bkeepers', repo: 'test', name: 'delete-me', headers: { accept: 'application/vnd.github.symmetra-preview+json' } }) - expect(github.issues.createLabel).toHaveBeenCalledWith({ + expect(github.rest.issues.createLabel).toHaveBeenCalledWith({ owner: 'bkeepers', repo: 'test', name: 'added', @@ -63,7 +65,7 @@ describe('Labels', () => { headers: { accept: 'application/vnd.github.symmetra-preview+json' } }) - expect(github.issues.updateLabel).toHaveBeenCalledWith({ + expect(github.rest.issues.updateLabel).toHaveBeenCalledWith({ owner: 'bkeepers', repo: 'test', current_name: 'update-me', @@ -73,7 +75,7 @@ describe('Labels', () => { headers: { accept: 'application/vnd.github.symmetra-preview+json' } }) - expect(github.issues.updateLabel).toHaveBeenCalledWith({ + expect(github.rest.issues.updateLabel).toHaveBeenCalledWith({ owner: 'bkeepers', repo: 'test', current_name: 'new-color', @@ -83,7 +85,7 @@ describe('Labels', () => { headers: { accept: 'application/vnd.github.symmetra-preview+json' } }) - expect(github.issues.updateLabel).toHaveBeenCalledWith({ + expect(github.rest.issues.updateLabel).toHaveBeenCalledWith({ owner: 'bkeepers', repo: 'test', current_name: 'new-description', @@ -93,9 +95,9 @@ describe('Labels', () => { headers: { accept: 'application/vnd.github.symmetra-preview+json' } }) - expect(github.issues.deleteLabel).toHaveBeenCalledTimes(1) - expect(github.issues.updateLabel).toHaveBeenCalledTimes(3) - expect(github.issues.createLabel).toHaveBeenCalledTimes(1) + expect(github.rest.issues.deleteLabel).toHaveBeenCalledTimes(1) + expect(github.rest.issues.updateLabel).toHaveBeenCalledTimes(3) + expect(github.rest.issues.createLabel).toHaveBeenCalledTimes(1) }) }) @@ -122,14 +124,14 @@ describe('Labels', () => { }) return plugin.sync().then(() => { - expect(github.issues.deleteLabel).toHaveBeenCalledWith({ + expect(github.rest.issues.deleteLabel).toHaveBeenCalledWith({ owner: 'bkeepers', repo: 'test', name: 'delete-me', headers: { accept: 'application/vnd.github.symmetra-preview+json' } }) - expect(github.issues.updateLabel).toHaveBeenCalledWith({ + expect(github.rest.issues.updateLabel).toHaveBeenCalledWith({ owner: 'bkeepers', repo: 'test', current_name: 'update-me', @@ -139,9 +141,9 @@ describe('Labels', () => { headers: { accept: 'application/vnd.github.symmetra-preview+json' } }) - expect(github.issues.deleteLabel).toHaveBeenCalledTimes(1) - expect(github.issues.updateLabel).toHaveBeenCalledTimes(1) - expect(github.issues.createLabel).toHaveBeenCalledTimes(1) + expect(github.rest.issues.deleteLabel).toHaveBeenCalledTimes(1) + expect(github.rest.issues.updateLabel).toHaveBeenCalledTimes(1) + expect(github.rest.issues.createLabel).toHaveBeenCalledTimes(1) }) }) }) diff --git a/test/unit/lib/plugins/milestones.test.js b/test/unit/lib/plugins/milestones.test.js index 93cde7db4..ebbac3ddd 100644 --- a/test/unit/lib/plugins/milestones.test.js +++ b/test/unit/lib/plugins/milestones.test.js @@ -10,15 +10,17 @@ describe.skip('Milestones', () => { beforeEach(() => { github = { paginate: jest.fn().mockImplementation(() => Promise.resolve()), - issues: { - listMilestonesForRepo: { - endpoint: { - merge: jest.fn().mockImplementation(() => {}) - } - }, - createMilestone: jest.fn().mockImplementation(() => Promise.resolve()), - deleteMilestone: jest.fn().mockImplementation(() => Promise.resolve()), - updateMilestone: jest.fn().mockImplementation(() => Promise.resolve()) + rest: { + issues: { + listMilestones: { + endpoint: { + merge: jest.fn().mockImplementation(() => {}) + } + }, + createMilestone: jest.fn().mockImplementation(() => Promise.resolve()), + deleteMilestone: jest.fn().mockImplementation(() => Promise.resolve()), + updateMilestone: jest.fn().mockImplementation(() => Promise.resolve()) + } } } }) @@ -41,19 +43,19 @@ describe.skip('Milestones', () => { await plugin.sync() - expect(github.issues.deleteMilestone).toHaveBeenCalledWith({ + expect(github.rest.issues.deleteMilestone).toHaveBeenCalledWith({ owner: 'bkeepers', repo: 'test', milestone_number: 1 }) - expect(github.issues.createMilestone).toHaveBeenCalledWith({ + expect(github.rest.issues.createMilestone).toHaveBeenCalledWith({ owner: 'bkeepers', repo: 'test', title: 'added' }) - expect(github.issues.updateMilestone).toHaveBeenCalledWith({ + expect(github.rest.issues.updateMilestone).toHaveBeenCalledWith({ owner: 'bkeepers', repo: 'test', title: 'new-description', @@ -61,7 +63,7 @@ describe.skip('Milestones', () => { milestone_number: 2 }) - expect(github.issues.updateMilestone).toHaveBeenCalledWith({ + expect(github.rest.issues.updateMilestone).toHaveBeenCalledWith({ owner: 'bkeepers', repo: 'test', title: 'new-state', @@ -69,9 +71,9 @@ describe.skip('Milestones', () => { milestone_number: 4 }) - expect(github.issues.deleteMilestone).toHaveBeenCalledTimes(1) - expect(github.issues.updateMilestone).toHaveBeenCalledTimes(2) - expect(github.issues.createMilestone).toHaveBeenCalledTimes(1) + expect(github.rest.issues.deleteMilestone).toHaveBeenCalledTimes(1) + expect(github.rest.issues.updateMilestone).toHaveBeenCalledTimes(2) + expect(github.rest.issues.createMilestone).toHaveBeenCalledTimes(1) }) }) }) diff --git a/test/unit/lib/plugins/repository.test.js b/test/unit/lib/plugins/repository.test.js index fc4c453b0..75b1199fc 100644 --- a/test/unit/lib/plugins/repository.test.js +++ b/test/unit/lib/plugins/repository.test.js @@ -2,14 +2,16 @@ const Repository = require('../../../../lib/plugins/repository') describe('Repository', () => { const github = { - repos: { - get: jest.fn().mockResolvedValue({ - data: { - topics: [] - } - }), - update: jest.fn().mockResolvedValue(), - replaceAllTopics: jest.fn().mockResolvedValue() + rest: { + repos: { + get: jest.fn().mockResolvedValue({ + data: { + topics: [] + } + }), + update: jest.fn().mockResolvedValue(), + replaceAllTopics: jest.fn().mockResolvedValue() + } } } const log = jest.fn() @@ -34,7 +36,7 @@ describe('Repository', () => { topics: [] }) return plugin.sync().then(() => { - expect(github.repos.update).toHaveBeenCalledWith({ + expect(github.rest.repos.update).toHaveBeenCalledWith({ owner: 'bkeepers', repo: 'test', name: 'test', @@ -49,7 +51,7 @@ describe('Repository', () => { name: 'new-name' }) return plugin.sync().then(() => { - expect(github.repos.update).toHaveBeenCalledWith({ + expect(github.rest.repos.update).toHaveBeenCalledWith({ owner: 'bkeepers', repo: 'test', name: 'new-name', @@ -64,7 +66,7 @@ describe('Repository', () => { }) return plugin.sync().then(() => { - expect(github.repos.replaceAllTopics).toHaveBeenCalledWith({ + expect(github.rest.repos.replaceAllTopics).toHaveBeenCalledWith({ owner: 'bkeepers', repo: 'test', names: ['foo', 'bar'], diff --git a/test/unit/lib/plugins/rulesets.test.js b/test/unit/lib/plugins/rulesets.test.js index f15abd63f..15a9fee64 100644 --- a/test/unit/lib/plugins/rulesets.test.js +++ b/test/unit/lib/plugins/rulesets.test.js @@ -3,7 +3,7 @@ const { when } = require('jest-when') const Rulesets = require('../../../../lib/plugins/rulesets') const version = { - 'X-GitHub-Api-Version': '2022-11-28' + 'X-GitHub-Api-Version': '2026-03-10' } const repo_conditions = { ref_name: { @@ -88,32 +88,31 @@ describe('Rulesets', () => { log.debug = jest.fn() log.error = jest.fn() - function configure (config, scope='repo') { - const noop = false + function configure (config, scope = 'repo', noop = false) { const errors = [] return new Rulesets(noop, github, { owner: 'jitran', repo: 'test' }, config, log, errors, scope) } beforeEach(() => { github = { - repos: { - get: jest.fn().mockResolvedValue({ - data: { - default_branch: 'main' - } - }) + rest: { + repos: { + get: jest.fn().mockResolvedValue({ + data: { + default_branch: 'main' + } + }) + } }, request: jest.fn().mockImplementation(() => Promise.resolve('request')), } - github.request.endpoint = { - merge: jest.fn().mockReturnValue({ - method: 'GET', - url: '/repos/jitran/test/rulesets', - headers: version - } - ) - } + github.request.endpoint = jest.fn().mockImplementation((route, body) => ({ url: route, body })) + github.request.endpoint.merge = jest.fn().mockReturnValue({ + method: 'GET', + url: '/repos/jitran/test/rulesets', + headers: version + }) }) describe('sync', () => { @@ -151,6 +150,113 @@ describe('Rulesets', () => { ) }) }) + + it('in nop mode treats a missing repo as having no existing rulesets', async () => { + const notFound = new Error('Not Found') + notFound.status = 404 + github.paginate = jest.fn().mockRejectedValue(notFound) + + const plugin = configure( + [ + generateRequestRuleset( + 1, + 'All branches', + repo_conditions, + [ + { context: 'Status Check 1' } + ] + ) + ], + 'repo', + true + ) + + const result = await plugin.sync() + const flat = result.flat() + const summary = flat.find(command => command.plugin === 'Rulesets' && command.action?.msg === 'Changes found') + + expect(flat.some(command => command.type === 'ERROR')).toBe(false) + expect(summary.action.additions['0']).toEqual(expect.objectContaining({ name: 'All branches' })) + expect(summary.action.deletions).toBeUndefined() + }) + }) + + describe('idempotent create when the ruleset already exists (retried/concurrent POST)', () => { + function duplicateNameError () { + const e = new Error('Validation Failed') + e.status = 422 + e.response = { data: { errors: ['Name must be unique'] } } + return e + } + + function wireRequest (routeResults) { + const calls = [] + const request = jest.fn().mockImplementation((route, body) => { + calls.push({ route, body }) + const handler = routeResults[route] + return handler ? handler() : Promise.resolve({ url: route }) + }) + request.endpoint = jest.fn().mockImplementation((route, body) => ({ url: route, body })) + request.endpoint.merge = jest.fn().mockImplementation((route, body) => ({ method: 'GET', url: route, ...body })) + github.request = request + return calls + } + + it('reconciles a repo ruleset by updating the existing one on 422 "Name must be unique"', async () => { + const attrs = generateRequestRuleset(0, 'synk', repo_conditions, []) + delete attrs.id + const existing = generateResponseRuleset(42, 'synk', repo_conditions, []) + const calls = wireRequest({ + 'POST /repos/{owner}/{repo}/rulesets': () => Promise.reject(duplicateNameError()) + }) + github.paginate = jest.fn() + .mockResolvedValueOnce([{ id: 42, name: 'synk', source_type: 'Repository' }]) + .mockResolvedValueOnce([existing]) + + const plugin = configure([attrs]) + await plugin.add(attrs) + + const put = calls.find(c => c.route === 'PUT /repos/{owner}/{repo}/rulesets/{id}') + expect(put).toBeDefined() + expect(put.body.id).toBe(42) + }) + + it('reconciles an org ruleset by updating the existing one on 422 "Name must be unique"', async () => { + const attrs = generateRequestRuleset(0, 'synk', org_conditions, [], true) + delete attrs.id + const existing = generateResponseRuleset(7, 'synk', org_conditions, [], true) + const calls = wireRequest({ + 'POST /orgs/{org}/rulesets': () => Promise.reject(duplicateNameError()) + }) + github.paginate = jest.fn() + .mockResolvedValueOnce([{ id: 7, name: 'synk', source_type: 'Organization' }]) + .mockResolvedValueOnce([existing]) + + const plugin = configure([attrs], 'org') + await plugin.add(attrs) + + const put = calls.find(c => c.route === 'PUT /orgs/{org}/rulesets/{id}') + expect(put).toBeDefined() + expect(put.body.id).toBe(7) + }) + + it('does not reconcile (surfaces the error) for a 422 that is not a name-uniqueness violation', async () => { + const attrs = generateRequestRuleset(0, 'synk', repo_conditions, []) + delete attrs.id + const other = new Error('Validation Failed') + other.status = 422 + other.response = { data: { errors: ['Something else is invalid'] } } + const calls = wireRequest({ + 'POST /repos/{owner}/{repo}/rulesets': () => Promise.reject(other) + }) + github.paginate = jest.fn() + + const plugin = configure([attrs]) + await plugin.add(attrs) + + expect(github.paginate).not.toHaveBeenCalled() + expect(calls.some(c => c.route === 'PUT /repos/{owner}/{repo}/rulesets/{id}')).toBe(false) + }) }) describe('when {{EXTERNALLY_DEFINED}} is present in "required_status_checks" and no status checks exist in GitHub', () => { @@ -188,7 +294,7 @@ describe('Rulesets', () => { }) describe('when {{EXTERNALLY_DEFINED}} is present in "required_status_checks" and status checks exist in GitHub', () => { - it('it retains the status checks from GitHub and everything else is reset to the safe-settings', () => { + it('skips the placeholder-only ruleset and updates the genuinely changed ones', () => { // Mock the GitHub API response github.paginate = jest.fn().mockResolvedValue([ generateRequestRuleset( @@ -251,22 +357,12 @@ describe('Rulesets', () => { ) return plugin.sync().then(() => { + // Ruleset 1 only differs by the {{EXTERNALLY_DEFINED}} placeholder, which + // resolves to the live status checks, so it must not be updated at all. + expect(github.request).toHaveBeenCalledTimes(2) expect(github.request).toHaveBeenNthCalledWith( 1, 'PUT /repos/{owner}/{repo}/rulesets/{id}', - generateResponseRuleset( - 1, - 'All branches 1', - repo_conditions, - [ - { context: 'Custom Check 1' }, - { context: 'Custom Check 2' } - ] - ) - ) - expect(github.request).toHaveBeenNthCalledWith( - 2, - 'PUT /repos/{owner}/{repo}/rulesets/{id}', generateResponseRuleset( 2, 'All branches 2', @@ -278,7 +374,7 @@ describe('Rulesets', () => { ) ) expect(github.request).toHaveBeenNthCalledWith( - 3, + 2, 'PUT /repos/{owner}/{repo}/rulesets/{id}', generateResponseRuleset( 3, @@ -369,7 +465,7 @@ describe('Rulesets', () => { }) describe('[org] when {{EXTERNALLY_DEFINED}} is present in "required_status_checks" and status checks exist in GitHub', () => { - it('it retains the status checks from GitHub', () => { + it('reports no changes when only the placeholder differs from GitHub', () => { // Mock the GitHub API response github.paginate = jest.fn().mockResolvedValue([ generateRequestRuleset( @@ -402,21 +498,618 @@ describe('Rulesets', () => { ) return plugin.sync().then(() => { - expect(github.request).toHaveBeenNthCalledWith( - 1, - 'PUT /orgs/{org}/rulesets/{id}', - generateResponseRuleset( - 1, - 'All branches 1', - org_conditions, - [ - { context: 'Custom Check 1' }, - { context: 'Custom Check 2' } - ], - true - ) - ) + // The placeholder resolves to the live status checks, so the ruleset is unchanged + expect(github.request).not.toHaveBeenCalled() }) }) }) + + describe('in nop mode', () => { + beforeEach(() => { + github.request.endpoint = Object.assign( + jest.fn().mockImplementation((route, parms) => { return { url: route, body: parms } }), + { merge: github.request.endpoint.merge } + ) + }) + + it('does not plan an update when {{EXTERNALLY_DEFINED}} matches the status checks in GitHub', () => { + github.paginate = jest.fn().mockResolvedValue([ + generateRequestRuleset(1, 'All branches 1', repo_conditions, [{ context: 'Custom Check 1' }]) + ]) + + const plugin = configure([ + generateRequestRuleset(1, 'All branches 1', repo_conditions, [{ context: '{{EXTERNALLY_DEFINED}}' }]) + ], 'repo', true) + + return plugin.sync().then(res => { + // sync resolves with nothing when no changes are detected + const messages = (res || []).flat(2).map(nopCommand => nopCommand.action?.msg) + expect(messages).not.toContain('Update Ruleset') + }) + }) + + it('still plans an update when the config genuinely differs from GitHub', () => { + github.paginate = jest.fn().mockResolvedValue([ + generateRequestRuleset(1, 'All branches 1', repo_conditions, [{ context: 'Custom Check 1' }]) + ]) + + const plugin = configure([ + generateRequestRuleset(1, 'All branches 1', repo_conditions, [{ context: 'Other Check' }]) + ], 'repo', true) + + return plugin.sync().then(res => { + const messages = res.flat(2).map(nopCommand => nopCommand.action?.msg) + expect(messages).toContain('Update Ruleset') + }) + }) + }) + + describe('changed() method with required_reviewers', () => { + it('detects when required_reviewers array changes from populated to empty', () => { + github.paginate = jest.fn().mockResolvedValue([]) + + const plugin = configure([ + { + name: 'Protect release branches', + target: 'branch', + enforcement: 'active', + conditions: { + ref_name: { + include: ['refs/heads/release/*'], + exclude: [] + } + }, + rules: [ + { + type: 'pull_request', + parameters: { + required_approving_review_count: 1, + dismiss_stale_reviews_on_push: false, + require_code_owner_review: false, + require_last_push_approval: false, + required_review_thread_resolution: false, + allowed_merge_methods: ['merge', 'squash', 'rebase'], + required_reviewers: [ + { + minimum_approvals: 1, + file_patterns: ['*.js'], + reviewer: { + id: 11721733, + type: 'Team' + } + } + ] + } + } + ] + } + ]) + + // GitHub state after manual removal of required_reviewers + const existingRuleset = { + name: 'Protect release branches', + target: 'branch', + enforcement: 'active', + conditions: { + ref_name: { + include: ['refs/heads/release/*'], + exclude: [] + } + }, + rules: [ + { + type: 'pull_request', + parameters: { + required_approving_review_count: 1, + dismiss_stale_reviews_on_push: false, + require_code_owner_review: false, + require_last_push_approval: false, + required_review_thread_resolution: false, + allowed_merge_methods: ['merge', 'squash', 'rebase'], + required_reviewers: [] // Empty after manual removal + } + } + ] + } + + // YAML config (what safe-settings expects) + const attrs = plugin.rulesets[0] + + // The changed() method should detect this difference + const result = plugin.changed(existingRuleset, attrs) + expect(result).toBe(true) + }) + + it('detects when bypass_actors array changes from populated to empty', () => { + github.paginate = jest.fn().mockResolvedValue([]) + + const plugin = configure([ + { + name: 'Main protection', + target: 'branch', + enforcement: 'active', + conditions: { + ref_name: { + include: ['refs/heads/main'], + exclude: [] + } + }, + bypass_actors: [ + { + actor_type: 'OrganizationAdmin', + bypass_mode: 'always' + } + ], + rules: [ + { + type: 'creation' + } + ] + } + ]) + + // GitHub state after manual removal of bypass_actors + const existingRuleset = { + name: 'Main protection', + target: 'branch', + enforcement: 'active', + conditions: { + ref_name: { + include: ['refs/heads/main'], + exclude: [] + } + }, + bypass_actors: [], // Empty after manual removal + rules: [ + { + type: 'creation' + } + ] + } + + const attrs = plugin.rulesets[0] + const result = plugin.changed(existingRuleset, attrs) + expect(result).toBe(true) + }) + + it('detects when workflows array changes from populated to empty', () => { + github.paginate = jest.fn().mockResolvedValue([]) + + const plugin = configure([ + { + name: 'Workflow protection', + target: 'branch', + enforcement: 'active', + conditions: { + ref_name: { + include: ['refs/heads/main'], + exclude: [] + } + }, + rules: [ + { + type: 'workflows', + parameters: { + do_not_enforce_on_create: false, + workflows: [ + { + path: '.github/workflows/test.yml', + repository_id: 123456 + } + ] + } + } + ] + } + ]) + + // GitHub state after manual removal of workflows + const existingRuleset = { + name: 'Workflow protection', + target: 'branch', + enforcement: 'active', + conditions: { + ref_name: { + include: ['refs/heads/main'], + exclude: [] + } + }, + rules: [ + { + type: 'workflows', + parameters: { + do_not_enforce_on_create: false, + workflows: [] // Empty after manual removal + } + } + ] + } + + const attrs = plugin.rulesets[0] + const result = plugin.changed(existingRuleset, attrs) + expect(result).toBe(true) + }) + + it('detects when rules array has item added out-of-band', () => { + github.paginate = jest.fn().mockResolvedValue([]) + + const plugin = configure([ + { + name: 'Branch rules', + target: 'branch', + enforcement: 'active', + conditions: { + ref_name: { + include: ['refs/heads/main'], + exclude: [] + } + }, + rules: [ + { + type: 'creation' + } + ] + } + ]) + + // GitHub state where an extra rule was added manually + const existingRuleset = { + name: 'Branch rules', + target: 'branch', + enforcement: 'active', + conditions: { + ref_name: { + include: ['refs/heads/main'], + exclude: [] + } + }, + rules: [ + { + type: 'creation' + }, + { + type: 'deletion' // Extra rule added out-of-band + } + ] + } + + const attrs = plugin.rulesets[0] + const result = plugin.changed(existingRuleset, attrs) + expect(result).toBe(true) + }) + + it('detects when required_reviewers item is modified with different file patterns', () => { + github.paginate = jest.fn().mockResolvedValue([]) + + const plugin = configure([ + { + name: 'Code review', + target: 'branch', + enforcement: 'active', + conditions: { + ref_name: { + include: ['refs/heads/main'], + exclude: [] + } + }, + rules: [ + { + type: 'pull_request', + parameters: { + required_approving_review_count: 1, + dismiss_stale_reviews_on_push: false, + require_code_owner_review: false, + require_last_push_approval: false, + required_review_thread_resolution: false, + allowed_merge_methods: ['merge'], + required_reviewers: [ + { + minimum_approvals: 1, + file_patterns: ['*.js', '*.ts'], + reviewer: { + id: 999, + type: 'Team' + } + } + ] + } + } + ] + } + ]) + + // GitHub state where file patterns were manually changed + const existingRuleset = { + name: 'Code review', + target: 'branch', + enforcement: 'active', + conditions: { + ref_name: { + include: ['refs/heads/main'], + exclude: [] + } + }, + rules: [ + { + type: 'pull_request', + parameters: { + required_approving_review_count: 1, + dismiss_stale_reviews_on_push: false, + require_code_owner_review: false, + require_last_push_approval: false, + required_review_thread_resolution: false, + allowed_merge_methods: ['merge'], + required_reviewers: [ + { + minimum_approvals: 1, + file_patterns: ['*.py'], // Different patterns + reviewer: { + id: 999, + type: 'Team' + } + } + ] + } + } + ] + } + + const attrs = plugin.rulesets[0] + const result = plugin.changed(existingRuleset, attrs) + expect(result).toBe(true) + }) + + it('detects when bypass_actors item is modified with different bypass_mode', () => { + github.paginate = jest.fn().mockResolvedValue([]) + + const plugin = configure([ + { + name: 'Bypass config', + target: 'branch', + enforcement: 'active', + conditions: { + ref_name: { + include: ['refs/heads/main'], + exclude: [] + } + }, + bypass_actors: [ + { + actor_type: 'OrganizationAdmin', + bypass_mode: 'always' + } + ], + rules: [ + { + type: 'creation' + } + ] + } + ]) + + // GitHub state where bypass_mode was manually changed + const existingRuleset = { + name: 'Bypass config', + target: 'branch', + enforcement: 'active', + conditions: { + ref_name: { + include: ['refs/heads/main'], + exclude: [] + } + }, + bypass_actors: [ + { + actor_type: 'OrganizationAdmin', + bypass_mode: 'pull_request' // Changed from 'always' + } + ], + rules: [ + { + type: 'creation' + } + ] + } + + const attrs = plugin.rulesets[0] + const result = plugin.changed(existingRuleset, attrs) + expect(result).toBe(true) + }) + }) + + describe('name to id resolution', () => { + function bypassRuleset (actorEntry) { + return { + name: 'Main protection', + target: 'branch', + enforcement: 'active', + conditions: { ref_name: { include: ['refs/heads/main'], exclude: [] } }, + bypass_actors: [Object.assign({ bypass_mode: 'always' }, actorEntry)], + rules: [{ type: 'creation' }] + } + } + + function reviewerRuleset (reviewer) { + return { + name: 'Code review', + target: 'branch', + enforcement: 'active', + conditions: { ref_name: { include: ['refs/heads/main'], exclude: [] } }, + rules: [ + { + type: 'pull_request', + parameters: { + required_approving_review_count: 1, + dismiss_stale_reviews_on_push: false, + require_code_owner_review: false, + require_last_push_approval: false, + required_review_thread_resolution: false, + required_reviewers: [ + { minimum_approvals: 1, file_patterns: ['*.js'], reviewer } + ] + } + } + ] + } + } + + it('resolves a Team bypass actor name to actor_id and strips the alias', async () => { + github.rest.teams = { getByName: jest.fn().mockResolvedValue({ data: { id: 42 } }) } + const plugin = configure([bypassRuleset({ name: 'my-team', actor_type: 'Team' })], 'org') + + await plugin.resolveNamesToIds() + + expect(github.rest.teams.getByName).toHaveBeenCalledWith({ org: 'jitran', team_slug: 'my-team' }) + expect(plugin.rulesets[0].bypass_actors[0]).toEqual({ actor_id: 42, actor_type: 'Team', bypass_mode: 'always' }) + expect(plugin.rulesets[0].bypass_actors[0].name).toBeUndefined() + }) + + it('resolves a User bypass actor login to actor_id', async () => { + github.request = jest.fn().mockResolvedValue({ data: { id: 7 } }) + const plugin = configure([bypassRuleset({ name: 'octocat', actor_type: 'User' })], 'org') + + await plugin.resolveNamesToIds() + + expect(github.request).toHaveBeenCalledWith('GET /users/{username}', { username: 'octocat' }) + expect(plugin.rulesets[0].bypass_actors[0]).toEqual({ actor_id: 7, actor_type: 'User', bypass_mode: 'always' }) + }) + + it('resolves an Integration (GitHub App) slug to actor_id', async () => { + github.request = jest.fn().mockResolvedValue({ data: { id: 99 } }) + const plugin = configure([bypassRuleset({ name: 'my-app', actor_type: 'Integration' })], 'org') + + await plugin.resolveNamesToIds() + + expect(github.request).toHaveBeenCalledWith('GET /apps/{app_slug}', { app_slug: 'my-app' }) + expect(plugin.rulesets[0].bypass_actors[0].actor_id).toBe(99) + }) + + it('resolves built-in RepositoryRole names from the static map without an API call', async () => { + github.request = jest.fn() + const plugin = configure([ + bypassRuleset({ name: 'admin', actor_type: 'RepositoryRole' }) + ], 'org') + + await plugin.resolveNamesToIds() + + expect(github.request).not.toHaveBeenCalled() + expect(plugin.rulesets[0].bypass_actors[0].actor_id).toBe(5) + }) + + it('pins the built-in RepositoryRole ids', async () => { + const expected = { read: 1, triage: 2, write: 3, maintain: 4, admin: 5 } + for (const [name, id] of Object.entries(expected)) { + const plugin = configure([bypassRuleset({ name, actor_type: 'RepositoryRole' })], 'org') + await plugin.resolveNamesToIds() + expect(plugin.rulesets[0].bypass_actors[0].actor_id).toBe(id) + } + }) + + it('resolves a custom RepositoryRole name via the custom-repository-roles API', async () => { + github.request = jest.fn().mockResolvedValue({ data: { custom_roles: [{ id: 123, name: 'Security' }] } }) + const plugin = configure([bypassRuleset({ name: 'Security', actor_type: 'RepositoryRole' })], 'org') + + await plugin.resolveNamesToIds() + + expect(github.request).toHaveBeenCalledWith('GET /orgs/{org}/custom-repository-roles', { org: 'jitran' }) + expect(plugin.rulesets[0].bypass_actors[0].actor_id).toBe(123) + }) + + it('resolves a reviewer slug to id and strips the alias', async () => { + github.rest.teams = { getByName: jest.fn().mockResolvedValue({ data: { id: 555 } }) } + const plugin = configure([reviewerRuleset({ slug: 'reviewers', type: 'Team' })], 'org') + + await plugin.resolveNamesToIds() + + const reviewer = plugin.rulesets[0].rules[0].parameters.required_reviewers[0].reviewer + expect(github.rest.teams.getByName).toHaveBeenCalledWith({ org: 'jitran', team_slug: 'reviewers' }) + expect(reviewer).toEqual({ id: 555, type: 'Team' }) + expect(reviewer.slug).toBeUndefined() + }) + + it('caches repeated lookups so each name resolves with a single API call', async () => { + github.rest.teams = { getByName: jest.fn().mockResolvedValue({ data: { id: 42 } }) } + const plugin = configure([ + { + name: 'Multi', + target: 'branch', + enforcement: 'active', + conditions: { ref_name: { include: ['refs/heads/main'], exclude: [] } }, + bypass_actors: [ + { name: 'my-team', actor_type: 'Team', bypass_mode: 'always' }, + { name: 'my-team', actor_type: 'Team', bypass_mode: 'pull_request' } + ], + rules: [{ type: 'creation' }] + } + ], 'org') + + await plugin.resolveNamesToIds() + + expect(github.rest.teams.getByName).toHaveBeenCalledTimes(1) + expect(plugin.rulesets[0].bypass_actors.map(a => a.actor_id)).toEqual([42, 42]) + }) + + it('leaves numeric actor_id untouched and makes no lookup (backward compatible)', async () => { + github.rest.teams = { getByName: jest.fn() } + github.request = jest.fn() + const plugin = configure([bypassRuleset({ actor_id: 234, actor_type: 'Team' })], 'org') + + await plugin.resolveNamesToIds() + + expect(github.rest.teams.getByName).not.toHaveBeenCalled() + expect(github.request).not.toHaveBeenCalled() + expect(plugin.rulesets[0].bypass_actors[0]).toEqual({ actor_id: 234, actor_type: 'Team', bypass_mode: 'always' }) + }) + + it('throws when both name and actor_id are provided', async () => { + const plugin = configure([bypassRuleset({ name: 'my-team', actor_id: 1, actor_type: 'Team' })], 'org') + await expect(plugin.resolveNamesToIds()).rejects.toThrow(/both 'name'.*and 'actor_id'/) + }) + + it('throws when both reviewer slug and id are provided', async () => { + const plugin = configure([reviewerRuleset({ slug: 'reviewers', id: 1, type: 'Team' })], 'org') + await expect(plugin.resolveNamesToIds()).rejects.toThrow(/both 'slug'.*and 'id'/) + }) + + it('throws when an actor_type does not support name resolution', async () => { + const plugin = configure([bypassRuleset({ name: 'whoever', actor_type: 'DeployKey' })], 'org') + await expect(plugin.resolveNamesToIds()).rejects.toThrow(/only supported for Team, User, Integration, and RepositoryRole/) + }) + + it('throws a clear error when a team slug cannot be resolved', async () => { + const notFound = new Error('Not Found') + notFound.status = 404 + github.rest.teams = { getByName: jest.fn().mockRejectedValue(notFound) } + const plugin = configure([bypassRuleset({ name: 'ghost-team', actor_type: 'Team' })], 'org') + await expect(plugin.resolveNamesToIds()).rejects.toThrow(/Unable to resolve Team slug 'ghost-team'/) + }) + + it('sync sends the resolved actor_id to the API', async () => { + github.paginate = jest.fn().mockResolvedValue([]) + github.rest.teams = { getByName: jest.fn().mockResolvedValue({ data: { id: 42 } }) } + const postCalls = [] + github.request = jest.fn().mockImplementation((route, body) => { + if (route.startsWith('POST')) postCalls.push({ route, body }) + return Promise.resolve('request') + }) + github.request.endpoint = jest.fn().mockImplementation((route, body) => ({ url: route, body })) + github.request.endpoint.merge = jest.fn().mockReturnValue({ method: 'GET', url: '/orgs/jitran/rulesets', headers: version }) + + const plugin = configure([bypassRuleset({ name: 'my-team', actor_type: 'Team' })], 'org') + await plugin.sync() + + expect(postCalls).toHaveLength(1) + expect(postCalls[0].route).toBe('POST /orgs/{org}/rulesets') + expect(postCalls[0].body.bypass_actors).toEqual([{ actor_id: 42, actor_type: 'Team', bypass_mode: 'always' }]) + }) + + it('sync surfaces a resolution failure as an error in nop mode', async () => { + github.paginate = jest.fn().mockResolvedValue([]) + const plugin = configure([bypassRuleset({ name: 'my-team', actor_id: 1, actor_type: 'Team' })], 'org', true) + + const result = await plugin.sync() + const flat = result.flat() + expect(flat.some(command => command.type === 'ERROR')).toBe(true) + }) + }) }) diff --git a/test/unit/lib/plugins/teams.test.js b/test/unit/lib/plugins/teams.test.js index 60ef23dbc..6015ea36d 100644 --- a/test/unit/lib/plugins/teams.test.js +++ b/test/unit/lib/plugins/teams.test.js @@ -15,7 +15,7 @@ describe('Teams', () => { const org = 'bkeepers' function configure (config) { - const log = { debug: jest.fn(), error: console.error } + const log = { debug: jest.fn(), error: jest.fn(), warn: console.warn } const errors = [] return new Teams(undefined, github, { owner: 'bkeepers', repo: 'test' }, config, log, errors) } @@ -23,25 +23,30 @@ describe('Teams', () => { beforeEach(() => { github = { paginate: jest.fn() - .mockImplementation(async (fetch) => { - const response = await fetch() - return response.data + .mockImplementation(async (fetchOrRoute) => { + if (typeof fetchOrRoute === 'function') { + const response = await fetchOrRoute() + return response.data + } + return [] }), - teams: { - create: jest.fn().mockResolvedValue(), - getByName: jest.fn(), - addOrUpdateRepoPermissionsInOrg: jest.fn().mockResolvedValue() - }, - repos: { - listTeams: jest.fn().mockResolvedValue({ - data: [ - { id: unchangedTeamId, slug: unchangedTeamName, permission: 'push' }, - { id: removedTeamId, slug: removedTeamName, permission: 'push' }, - { id: updatedTeamId, slug: updatedTeamName, permission: 'pull' } - ] - }) + rest: { + teams: { + create: jest.fn().mockResolvedValue(), + getByName: jest.fn(), + addOrUpdateRepoPermissionsInOrg: jest.fn().mockResolvedValue() + }, + repos: { + listTeams: jest.fn().mockResolvedValue({ + data: [ + { id: unchangedTeamId, slug: unchangedTeamName, permission: 'push' }, + { id: removedTeamId, slug: removedTeamName, permission: 'push' }, + { id: updatedTeamId, slug: updatedTeamName, permission: 'pull' } + ] + }) + } }, - request: jest.fn().mockResolvedValue() + request: Object.assign(jest.fn().mockResolvedValue(), { endpoint: jest.fn().mockReturnValue({ url: 'endpoint-stub', body: {} }) }) } }) @@ -53,7 +58,7 @@ describe('Teams', () => { { name: addedTeamName, permission: 'pull' } ]) - when(github.teams.getByName) + when(github.rest.teams.getByName) .defaultResolvedValue({}) .calledWith({ org: 'bkeepers', team_slug: addedTeamName }) .mockResolvedValue({ data: { id: addedTeamId } }) @@ -72,7 +77,7 @@ describe('Teams', () => { } ) - expect(github.teams.addOrUpdateRepoPermissionsInOrg).toHaveBeenCalledWith({ + expect(github.rest.teams.addOrUpdateRepoPermissionsInOrg).toHaveBeenCalledWith({ org, team_id: addedTeamId, team_slug: addedTeamName, @@ -96,4 +101,498 @@ describe('Teams', () => { ) } }) + + describe('security manager teams', () => { + const securityManagerRoleId = any.integer() + const securityManagerTeamName = 'security-managers' + const securityManagerTeamId = any.integer() + const organizationRolesRoute = 'GET /orgs/{org}/organization-roles' + const organizationRoleTeamsRoute = 'GET /orgs/{org}/organization-roles/{role_id}/teams' + const roleFailureStatuses = [403, 404, 422, 500] + const repoTeams = [ + { id: securityManagerTeamId, slug: securityManagerTeamName, name: 'Security Managers', permission: 'admin' }, + { id: unchangedTeamId, slug: unchangedTeamName, permission: 'push' }, + { id: removedTeamId, slug: removedTeamName, permission: 'push' }, + { id: updatedTeamId, slug: updatedTeamName, permission: 'pull' } + ] + + beforeEach(() => { + github.rest.repos.listTeams.mockResolvedValue({ data: repoTeams }) + }) + + function expectTeamDeleted (teamSlug) { + expect(github.request).toHaveBeenCalledWith( + 'DELETE /orgs/:owner/teams/:team_slug/repos/:owner/:repo', + { + org, + owner: org, + repo: 'test', + team_slug: teamSlug + } + ) + } + + function expectTeamNotDeleted (teamSlug) { + expect(github.request).not.toHaveBeenCalledWith( + 'DELETE /orgs/:owner/teams/:team_slug/repos/:owner/:repo', + { + org, + owner: org, + repo: 'test', + team_slug: teamSlug + } + ) + } + + function expectNoTeamsDeleted () { + expect(github.request).not.toHaveBeenCalledWith( + 'DELETE /orgs/:owner/teams/:team_slug/repos/:owner/:repo', + expect.any(Object) + ) + } + + it('syncs non-security-manager teams and leaves security manager teams untouched', async () => { + const plugin = configure([ + { name: unchangedTeamName, permission: 'push' }, + { name: updatedTeamName, permission: 'admin' }, + { name: addedTeamName, permission: 'pull' } + ]) + + when(github.paginate) + .calledWith(organizationRolesRoute, { org }) + .mockResolvedValue({ roles: [{ id: securityManagerRoleId, name: 'Security Manager' }] }) + + when(github.paginate) + .calledWith(organizationRoleTeamsRoute, { org, role_id: securityManagerRoleId }) + .mockResolvedValue({ teams: [{ slug: securityManagerTeamName, name: 'Security Managers' }] }) + + when(github.rest.teams.getByName) + .defaultResolvedValue({}) + .calledWith({ org, team_slug: addedTeamName }) + .mockResolvedValue({ data: { id: addedTeamId } }) + + await plugin.sync() + + expect(github.paginate).toHaveBeenCalledWith(organizationRolesRoute, { org }) + expect(github.paginate).toHaveBeenCalledWith(organizationRoleTeamsRoute, { org, role_id: securityManagerRoleId }) + expectTeamDeleted(removedTeamName) + expectTeamNotDeleted(securityManagerTeamName) + }) + + it('does not add or update a security manager team even when it is listed in the config', async () => { + const plugin = configure([ + { name: securityManagerTeamName, permission: 'pull' }, + { name: unchangedTeamName, permission: 'push' } + ]) + + when(github.paginate) + .calledWith(organizationRolesRoute, { org }) + .mockResolvedValue({ roles: [{ id: securityManagerRoleId, name: 'Security Manager' }] }) + + when(github.paginate) + .calledWith(organizationRoleTeamsRoute, { org, role_id: securityManagerRoleId }) + .mockResolvedValue({ teams: [{ slug: securityManagerTeamName, name: 'Security Managers' }] }) + + await plugin.sync() + + expect(github.rest.teams.getByName).not.toHaveBeenCalledWith({ org, team_slug: securityManagerTeamName }) + expect(github.rest.teams.addOrUpdateRepoPermissionsInOrg).not.toHaveBeenCalled() + expect(github.request).not.toHaveBeenCalledWith( + 'PUT /orgs/:owner/teams/:team_slug/repos/:owner/:repo', + expect.objectContaining({ team_slug: securityManagerTeamName }) + ) + expectTeamNotDeleted(securityManagerTeamName) + }) + + it('emits an INFO nop command instead of managing a configured security manager team in nop mode', async () => { + const log = { debug: jest.fn(), error: jest.fn(), warn: jest.fn() } + const plugin = new Teams(true, github, { owner: org, repo: 'test' }, [ + { name: securityManagerTeamName, permission: 'pull' } + ], log, []) + + when(github.paginate) + .calledWith(organizationRolesRoute, { org }) + .mockResolvedValue({ roles: [{ id: securityManagerRoleId, name: 'Security Manager' }] }) + + when(github.paginate) + .calledWith(organizationRoleTeamsRoute, { org, role_id: securityManagerRoleId }) + .mockResolvedValue({ teams: [{ slug: securityManagerTeamName, name: 'Security Managers' }] }) + + const result = await plugin.sync() + + expect(Array.isArray(result)).toBe(true) + const flattened = result.flat(Infinity) + expect(flattened.some(c => c && c.type === 'INFO' && /security manager team/i.test(JSON.stringify(c)))).toBe(true) + expect(github.rest.teams.addOrUpdateRepoPermissionsInOrg).not.toHaveBeenCalled() + }) + + it.each(roleFailureStatuses)('skips deletions when organization role lookup fails with %s', async status => { + const plugin = configure([ + { name: unchangedTeamName, permission: 'push' } + ]) + + when(github.paginate) + .calledWith(organizationRolesRoute, { org }) + .mockRejectedValue({ status }) + + await plugin.sync() + + expectNoTeamsDeleted() + }) + + it.each(roleFailureStatuses)('skips deletions when organization role team lookup fails with %s', async status => { + const plugin = configure([ + { name: unchangedTeamName, permission: 'push' } + ]) + + when(github.paginate) + .calledWith(organizationRolesRoute, { org }) + .mockResolvedValue({ roles: [{ id: securityManagerRoleId, slug: 'security_manager' }] }) + + when(github.paginate) + .calledWith(organizationRoleTeamsRoute, { org, role_id: securityManagerRoleId }) + .mockRejectedValue({ status }) + + await plugin.sync() + + expectNoTeamsDeleted() + }) + + it('emits an INFO nop command when skipping deletion in nop mode after discovery failure', async () => { + const log = { debug: jest.fn(), error: jest.fn(), warn: jest.fn() } + const plugin = new Teams(true, github, { owner: org, repo: 'test' }, [ + { name: unchangedTeamName, permission: 'push' } + ], log, []) + + when(github.paginate) + .calledWith(organizationRolesRoute, { org }) + .mockRejectedValue({ status: 500 }) + + const result = await plugin.sync() + + expect(Array.isArray(result)).toBe(true) + const flattened = result.flat(Infinity) + expect(flattened.some(c => c && c.type === 'INFO' && /security manager team discovery failed/i.test(JSON.stringify(c)))).toBe(true) + expectNoTeamsDeleted() + }) + + it('matches configured team names to existing slugs without add or remove churn', async () => { + const formattedTeamName = 'Platform & Security!' + + github.rest.repos.listTeams.mockResolvedValue({ + data: [{ id: unchangedTeamId, slug: 'platform-security', name: formattedTeamName, permission: 'push' }] + }) + + const plugin = configure([ + { name: formattedTeamName, permission: 'push' } + ]) + + await plugin.sync() + + expect(github.rest.teams.getByName).not.toHaveBeenCalled() + expectNoTeamsDeleted() + }) + + it('matches security manager team names against repository team slugs', async () => { + github.rest.repos.listTeams.mockResolvedValue({ + data: [{ id: securityManagerTeamId, slug: securityManagerTeamName, permission: 'admin' }] + }) + + when(github.paginate) + .calledWith(organizationRolesRoute, { org }) + .mockResolvedValue({ roles: [{ id: securityManagerRoleId, name: 'Security Manager' }] }) + + when(github.paginate) + .calledWith(organizationRoleTeamsRoute, { org, role_id: securityManagerRoleId }) + .mockResolvedValue({ teams: [{ name: 'Security Managers' }] }) + + const plugin = configure([]) + + await expect(plugin.find()).resolves.toEqual([]) + }) + + it('uses normalized team slugs when adding configured team names', async () => { + const formattedTeamName = 'Platform & Security!' + + github.rest.repos.listTeams.mockResolvedValue({ data: [] }) + + when(github.rest.teams.getByName) + .calledWith({ org, team_slug: 'platform-security' }) + .mockResolvedValue({ data: { id: addedTeamId, slug: 'platform-security' } }) + + const plugin = configure([ + { name: formattedTeamName, permission: 'pull' } + ]) + + await plugin.sync() + + expect(github.rest.teams.addOrUpdateRepoPermissionsInOrg).toHaveBeenCalledWith({ + org, + team_id: addedTeamId, + team_slug: 'platform-security', + owner: org, + repo: 'test', + permission: 'pull' + }) + }) + + it('returns original teams when the security manager role is absent', async () => { + const plugin = configure([]) + + when(github.paginate) + .calledWith(organizationRolesRoute, { org }) + .mockResolvedValue({ roles: [{ id: any.integer(), name: 'compliance_manager' }] }) + + await expect(plugin.find()).resolves.toEqual(repoTeams) + expect(github.paginate).not.toHaveBeenCalledWith(organizationRoleTeamsRoute, { org, role_id: securityManagerRoleId }) + }) + + it('returns original teams when organization role team lookup fails', async () => { + const plugin = configure([]) + + when(github.paginate) + .calledWith(organizationRolesRoute, { org }) + .mockResolvedValue({ roles: [{ id: securityManagerRoleId, slug: 'security_manager' }] }) + + when(github.paginate) + .calledWith(organizationRoleTeamsRoute, { org, role_id: securityManagerRoleId }) + .mockRejectedValue({ status: 500 }) + + await expect(plugin.find()).resolves.toEqual(repoTeams) + }) + }) + + describe('filtering teams by include/exclude', () => { + beforeEach(() => { + github.rest.repos.listTeams.mockResolvedValue({ data: [] }) + }) + + it('does not add a team when the repo matches an exclude glob', async () => { + const plugin = configure([ + { name: addedTeamName, permission: 'pull', exclude: ['test*'] } + ]) + + await plugin.sync() + + expect(github.rest.teams.addOrUpdateRepoPermissionsInOrg).not.toHaveBeenCalled() + }) + + it('does not add a team when the repo is not in an include glob', async () => { + const plugin = configure([ + { name: addedTeamName, permission: 'pull', include: ['other-*'] } + ]) + + await plugin.sync() + + expect(github.rest.teams.addOrUpdateRepoPermissionsInOrg).not.toHaveBeenCalled() + }) + + it('adds a team when the repo matches an include glob', async () => { + when(github.rest.teams.getByName) + .calledWith({ org, team_slug: addedTeamName }) + .mockResolvedValue({ data: { id: addedTeamId } }) + + const plugin = configure([ + { name: addedTeamName, permission: 'pull', include: ['test*'] } + ]) + + await plugin.sync() + + expect(github.rest.teams.addOrUpdateRepoPermissionsInOrg).toHaveBeenCalledWith({ + org, + team_id: addedTeamId, + team_slug: addedTeamName, + owner: org, + repo: 'test', + permission: 'pull' + }) + }) + }) + + describe('external_group linking', () => { + const externalGroupName = 'Engineering - Expert Services' + const externalGroupId = 42 + + beforeEach(() => { + // request: default to no-current-link (404) so PATCH fires; override per-test as needed. + github.request = jest.fn().mockImplementation((endpoint) => { + if (typeof endpoint === 'string' && endpoint.startsWith('GET /orgs/{org}/teams/')) { + const err = new Error('not found') + err.status = 404 + return Promise.reject(err) + } + return Promise.resolve({ data: {} }) + }) + github.request.endpoint = jest.fn().mockReturnValue({ url: 'endpoint-stub', body: {} }) + + // paginate: route the external-groups list call to a single page; keep + // the original implementation for other paginated endpoints. The real + // production code passes a map-function (3rd arg) that extracts the + // `groups` array from each page response -- we mimic the same response + // shape so that mapFn gets exercised. + const externalGroupsResponse = { + data: { + total_count: 2, + groups: [ + { group_id: externalGroupId, group_name: externalGroupName }, + { group_id: 99, group_name: 'Some Other Group' } + ] + } + } + github.paginate = jest.fn().mockImplementation(async (fetchOrEndpoint, params, mapFn) => { + if (fetchOrEndpoint === 'GET /orgs/{org}/external-groups') { + if (typeof mapFn === 'function') { + return mapFn(externalGroupsResponse) + } + return externalGroupsResponse.data.groups + } + if (typeof fetchOrEndpoint === 'function') { + const response = await fetchOrEndpoint() + return response.data + } + return [] + }) + }) + + it('looks up the group id by name and PATCHes the team link', async () => { + when(github.rest.teams.getByName) + .defaultResolvedValue({}) + .calledWith({ org, team_slug: addedTeamName }) + .mockResolvedValue({ data: { id: addedTeamId } }) + + const plugin = configure([ + { name: unchangedTeamName, permission: 'push' }, + { name: addedTeamName, permission: 'pull', external_group: externalGroupName } + ]) + + await plugin.sync() + + expect(github.paginate).toHaveBeenCalledWith( + 'GET /orgs/{org}/external-groups', + { org, per_page: 100 }, + expect.any(Function) + ) + expect(github.request).toHaveBeenCalledWith( + 'PATCH /orgs/{org}/teams/{team_slug}/external-groups', + { org, team_slug: addedTeamName, group_id: externalGroupId } + ) + expect(plugin.hasChanges).toBe(true) + }) + + it('skips the PATCH when the team is already linked to the same group', async () => { + github.request = jest.fn().mockImplementation((endpoint, params) => { + if (endpoint === 'GET /orgs/{org}/teams/{team_slug}/external-groups') { + return Promise.resolve({ data: { groups: [{ group_id: externalGroupId, group_name: externalGroupName }] } }) + } + return Promise.resolve({ data: {} }) + }) + github.request.endpoint = jest.fn().mockReturnValue({ url: 'endpoint-stub', body: {} }) + + const plugin = configure([ + { name: unchangedTeamName, permission: 'push', external_group: externalGroupName } + ]) + + await plugin.sync() + + expect(github.request).toHaveBeenCalledWith( + 'GET /orgs/{org}/teams/{team_slug}/external-groups', + { org, team_slug: unchangedTeamName } + ) + expect(github.request).not.toHaveBeenCalledWith( + 'PATCH /orgs/{org}/teams/{team_slug}/external-groups', + expect.anything() + ) + }) + + it('logs a warning (not an error) and skips when the external group name is not found', async () => { + const plugin = configure([ + { name: unchangedTeamName, permission: 'push', external_group: 'Nonexistent Group' } + ]) + + await plugin.sync() + + expect(github.request).not.toHaveBeenCalledWith( + 'PATCH /orgs/{org}/teams/{team_slug}/external-groups', + expect.anything() + ) + // Non-fatal: should not push onto the errors array + expect(plugin.errors.some(e => /Nonexistent Group/.test(JSON.stringify(e)))).toBe(false) + }) + + it('in nop mode, emits a WARNING NopCommand when the external group is not found (so it appears in the PR check_run without failing it)', async () => { + const log = { debug: jest.fn(), error: console.error, warn: console.warn } + const errors = [] + const Teams = require('../../../../lib/plugins/teams') + const plugin = new Teams(true, github, { owner: org, repo: 'test' }, [ + { name: unchangedTeamName, permission: 'push', external_group: 'Nonexistent Group' } + ], log, errors) + + const result = await plugin.sync() + + expect(Array.isArray(result)).toBe(true) + const warningCmd = result.find(c => c && c.type === 'WARNING' && /Nonexistent Group/.test(JSON.stringify(c))) + expect(warningCmd).toBeDefined() + expect(result.some(c => c && c.type === 'ERROR')).toBe(false) + expect(github.request).not.toHaveBeenCalledWith( + 'PATCH /orgs/{org}/teams/{team_slug}/external-groups', + expect.anything() + ) + }) + + it('paginates the external-groups list only once per org across multiple syncs sharing the github client', async () => { + when(github.rest.teams.getByName) + .defaultResolvedValue({}) + .calledWith({ org, team_slug: addedTeamName }) + .mockResolvedValue({ data: { id: addedTeamId } }) + + const plugin1 = configure([ + { name: unchangedTeamName, permission: 'push', external_group: externalGroupName } + ]) + const plugin2 = configure([ + { name: addedTeamName, permission: 'pull', external_group: externalGroupName } + ]) + + await plugin1.sync() + await plugin2.sync() + + const listCalls = github.paginate.mock.calls.filter(c => c[0] === 'GET /orgs/{org}/external-groups') + expect(listCalls).toHaveLength(1) + }) + + it('does not call the external-groups list endpoint when no entry uses external_group', async () => { + const plugin = configure([ + { name: unchangedTeamName, permission: 'push' } + ]) + + await plugin.sync() + + const listCalls = github.paginate.mock.calls.filter(c => c[0] === 'GET /orgs/{org}/external-groups') + expect(listCalls).toHaveLength(0) + }) + + it('in nop mode, emits a NopCommand and makes no PATCH', async () => { + const log = { debug: jest.fn(), error: console.error } + const errors = [] + const Teams = require('../../../../lib/plugins/teams') + const plugin = new Teams(true, github, { owner: org, repo: 'test' }, [ + { name: unchangedTeamName, permission: 'push', external_group: externalGroupName } + ], log, errors) + + const result = await plugin.sync() + + expect(Array.isArray(result)).toBe(true) + expect(result.some(c => /external group/.test(c.action) || /external group/.test(JSON.stringify(c)))).toBe(true) + // In nop mode no real linkage should be performed -- neither the + // idempotency GET nor the PATCH should hit the team-external-groups + // endpoint. + expect(github.request).not.toHaveBeenCalledWith( + 'PATCH /orgs/{org}/teams/{team_slug}/external-groups', + expect.anything() + ) + expect(github.request).not.toHaveBeenCalledWith( + 'GET /orgs/{org}/teams/{team_slug}/external-groups', + expect.anything() + ) + }) + }) }) diff --git a/test/unit/lib/plugins/variables.test.js b/test/unit/lib/plugins/variables.test.js index 2784d7afd..91c06239b 100644 --- a/test/unit/lib/plugins/variables.test.js +++ b/test/unit/lib/plugins/variables.test.js @@ -1,78 +1,202 @@ const { when } = require('jest-when') const Variables = require('../../../../lib/plugins/variables') +const NopCommand = require('../../../../lib/nopcommand') describe('Variables', () => { let github const org = 'bkeepers' const repo = 'test' - function fillVariables (variables = []) { - return variables - } - - function configure () { - const log = { debug: console.debug, error: console.error } + function configure (nop = false, entries = [{ name: 'test', value: 'test' }]) { + const log = { debug: jest.fn(), error: console.error } const errors = [] - return new Variables(undefined, github, { owner: org, repo }, [{ name: 'test', value: 'test' }], log, errors) + return new Variables(nop, github, { owner: org, repo }, entries, log, errors) } - beforeAll(() => { + beforeEach(() => { github = { request: jest.fn().mockReturnValue(Promise.resolve(true)) } }) - it('sync', () => { - const plugin = configure() - - when(github.request) - .calledWith('GET /repos/:org/:repo/actions/variables', { org, repo }) - .mockResolvedValue({ - data: { - variables: [ - fillVariables({ - variables: [] - }) - ] - } - }); - - ['variables'].forEach(() => { + describe('constructor', () => { + it('should uppercase entry names', () => { + const plugin = configure(false, [{ name: 'lower_case', value: 'val' }]) + expect(plugin.entries[0].name).toBe('LOWER_CASE') + }) + }) + + describe('find', () => { + it('should return only name and value fields', async () => { when(github.request) .calledWith('GET /repos/:org/:repo/actions/variables', { org, repo }) .mockResolvedValue({ data: { - variables: [{ name: 'DELETE_me', value: 'test' }] + variables: [{ name: 'VAR1', value: 'val1', created_at: '2024-01-01', updated_at: '2024-01-02' }] } }) + + const plugin = configure() + const result = await plugin.find() + + expect(result).toEqual([{ name: 'VAR1', value: 'val1' }]) }) + }) - when(github.request).calledWith('POST /repos/:org/:repo/actions/variables').mockResolvedValue({}) + describe('changed', () => { + it('should return true when values differ', () => { + const plugin = configure() + expect(plugin.changed({ name: 'X', value: 'old' }, { name: 'X', value: 'new' })).toBe(true) + }) - return plugin.sync().then(() => { - expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/actions/variables', { org, repo }); + it('should return false when values match', () => { + const plugin = configure() + expect(plugin.changed({ name: 'X', value: 'same' }, { name: 'X', value: 'same' })).toBe(false) + }) + }) + + describe('sync', () => { + it('should add new and remove stale variables', () => { + const plugin = configure() + + when(github.request) + .calledWith('GET /repos/:org/:repo/actions/variables', { org, repo }) + .mockResolvedValue({ + data: { + variables: [{ name: 'DELETE_ME', value: 'test' }] + } + }) - ['variables'].forEach(() => { - expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/actions/variables', { org, repo }) + return plugin.sync().then(() => { + expect(github.request).toHaveBeenCalledWith( + 'DELETE /repos/:org/:repo/actions/variables/:variable_name', + expect.objectContaining({ org, repo, variable_name: 'DELETE_ME' }) + ) + + expect(github.request).toHaveBeenCalledWith( + 'POST /repos/:org/:repo/actions/variables', + expect.objectContaining({ org, repo, name: 'TEST', value: 'test' }) + ) }) + }) - expect(github.request).toHaveBeenCalledWith( - 'DELETE /repos/:org/:repo/actions/variables/:variable_name', - expect.objectContaining({ - org, - repo, - variable_name: 'DELETE_me' + it('should return NopCommands and not mutate when nop is true', async () => { + const plugin = configure(true) + + when(github.request) + .calledWith('GET /repos/:org/:repo/actions/variables', { org, repo }) + .mockResolvedValue({ + data: { + variables: [{ name: 'EXISTING_VAR', value: 'existing-value' }] + } }) + + const result = await plugin.sync() + + expect(github.request).toHaveBeenCalledWith('GET /repos/:org/:repo/actions/variables', { org, repo }) + expect(github.request).not.toHaveBeenCalledWith( + expect.stringMatching(/^(POST|PATCH|DELETE)/), + expect.anything() ) + expect(Array.isArray(result)).toBe(true) + expect(result.length).toBeGreaterThan(0) + const flat = result.flat() + flat.forEach(cmd => expect(cmd).toBeInstanceOf(NopCommand)) + }) + + it('should return NopCommand results when updating via sync', async () => { + const plugin = configure(true, [{ name: 'TEST', value: 'new-value' }]) + + when(github.request) + .calledWith('GET /repos/:org/:repo/actions/variables', { org, repo }) + .mockResolvedValue({ + data: { + variables: [{ name: 'TEST', value: 'old-value' }] + } + }) + + const result = await plugin.sync() + + expect(github.request).not.toHaveBeenCalledWith( + expect.stringMatching(/^(POST|PATCH|DELETE)/), + expect.anything() + ) + + expect(Array.isArray(result)).toBe(true) + const flat = result.flat() + flat.forEach(cmd => expect(cmd).toBeInstanceOf(NopCommand)) + }) + }) + + describe('add', () => { + it('should return NopCommand array when nop is true', async () => { + const plugin = configure(true) + const result = await plugin.add({ name: 'NEW_VAR', value: 'new-value' }) + + expect(Array.isArray(result)).toBe(true) + expect(result[0]).toBeInstanceOf(NopCommand) + expect(result[0].plugin).toBe('Variables') + expect(github.request).not.toHaveBeenCalled() + }) + + it('should make POST request when nop is false', async () => { + const plugin = configure(false) + await plugin.add({ name: 'NEW_VAR', value: 'new-value' }) + expect(github.request).toHaveBeenCalledWith( 'POST /repos/:org/:repo/actions/variables', - expect.objectContaining({ - org, - repo, - name: 'TEST', - value: 'test' - }) + expect.objectContaining({ org, repo, name: 'NEW_VAR', value: 'new-value' }) + ) + }) + }) + + describe('remove', () => { + it('should return NopCommand array when nop is true', async () => { + const plugin = configure(true) + const result = await plugin.remove({ name: 'EXISTING_VAR', value: 'existing-value' }) + + expect(Array.isArray(result)).toBe(true) + expect(result[0]).toBeInstanceOf(NopCommand) + expect(result[0].plugin).toBe('Variables') + expect(github.request).not.toHaveBeenCalled() + }) + + it('should make DELETE request when nop is false', async () => { + const plugin = configure(false) + await plugin.remove({ name: 'EXISTING_VAR', value: 'existing-value' }) + + expect(github.request).toHaveBeenCalledWith( + 'DELETE /repos/:org/:repo/actions/variables/:variable_name', + expect.objectContaining({ org, repo, variable_name: 'EXISTING_VAR' }) + ) + }) + }) + + describe('update', () => { + it('should return NopCommand array when nop is true', async () => { + const plugin = configure(true) + const result = await plugin.update( + { name: 'VAR1', value: 'old-value' }, + { name: 'VAR1', value: 'new-value' } + ) + + expect(Array.isArray(result)).toBe(true) + expect(result[0]).toBeInstanceOf(NopCommand) + expect(result[0].plugin).toBe('Variables') + expect(github.request).not.toHaveBeenCalled() + }) + + it('should make PATCH request when nop is false', async () => { + const plugin = configure(false) + await plugin.update( + { name: 'VAR1', value: 'old-value' }, + { name: 'VAR1', value: 'new-value' } + ) + + expect(github.request).toHaveBeenCalledWith( + 'PATCH /repos/:org/:repo/actions/variables/:variable_name', + expect.objectContaining({ org, repo, variable_name: 'VAR1', value: 'new-value' }) ) }) }) diff --git a/test/unit/lib/repoSelector.test.js b/test/unit/lib/repoSelector.test.js new file mode 100644 index 000000000..1285100b5 --- /dev/null +++ b/test/unit/lib/repoSelector.test.js @@ -0,0 +1,152 @@ +const RepoSelector = require('../../../lib/repoSelector') + +describe('RepoSelector', () => { + let github + let log + + beforeEach(() => { + log = { + debug: jest.fn(), + error: jest.fn() + } + + github = { + paginate: jest.fn(), + rest: { + teams: { + listReposInOrg: { + endpoint: { + merge: jest.fn().mockReturnValue({}) + } + } + } + }, + request: { + endpoint: jest.fn().mockReturnValue({}) + } + } + }) + + describe('resolve', () => { + it('returns empty set for null criteria', async () => { + const selector = new RepoSelector(github, 'my-org', log) + const result = await selector.resolve(null) + expect(result).toEqual(new Set()) + }) + + it('returns empty set for empty criteria', async () => { + const selector = new RepoSelector(github, 'my-org', log) + const result = await selector.resolve({}) + expect(result).toEqual(new Set()) + }) + }) + + describe('getAllRepos', () => { + it('returns all repo names from installation', async () => { + github.paginate.mockResolvedValue([ + { name: 'repo-a' }, + { name: 'repo-b' }, + { name: 'repo-c' } + ]) + + const selector = new RepoSelector(github, 'my-org', log) + const result = await selector.resolve({ all: true }) + expect(result).toEqual(new Set(['repo-a', 'repo-b', 'repo-c'])) + }) + }) + + describe('resolveByName', () => { + it('returns explicit repo names directly', async () => { + const selector = new RepoSelector(github, 'my-org', log) + const result = await selector.resolve({ names: ['repo-a', 'repo-b'] }) + expect(result).toEqual(new Set(['repo-a', 'repo-b'])) + expect(github.paginate).not.toHaveBeenCalled() + }) + + it('resolves glob patterns against all repos', async () => { + github.paginate.mockResolvedValue([ + { name: 'api-service' }, + { name: 'api-gateway' }, + { name: 'web-frontend' } + ]) + + const selector = new RepoSelector(github, 'my-org', log) + const result = await selector.resolve({ names: ['api-*'] }) + expect(result).toEqual(new Set(['api-service', 'api-gateway'])) + }) + }) + + describe('resolveByTeam', () => { + it('returns repos from team membership', async () => { + github.paginate.mockResolvedValue([ + { name: 'team-repo-1' }, + { name: 'team-repo-2' } + ]) + + const selector = new RepoSelector(github, 'my-org', log) + const result = await selector.resolve({ teams: ['my-team'] }) + expect(result).toEqual(new Set(['team-repo-1', 'team-repo-2'])) + }) + + it('unions repos from multiple teams', async () => { + github.paginate + .mockResolvedValueOnce([{ name: 'repo-a' }, { name: 'repo-b' }]) + .mockResolvedValueOnce([{ name: 'repo-b' }, { name: 'repo-c' }]) + + const selector = new RepoSelector(github, 'my-org', log) + const result = await selector.resolve({ teams: ['team-1', 'team-2'] }) + expect(result).toEqual(new Set(['repo-a', 'repo-b', 'repo-c'])) + }) + }) + + describe('resolveByCustomProperties', () => { + it('returns repos matching property values', async () => { + github.paginate.mockResolvedValue([ + { repository_name: 'prop-repo-1' }, + { repository_name: 'prop-repo-2' } + ]) + + const selector = new RepoSelector(github, 'my-org', log) + const result = await selector.resolve({ + custom_properties: [{ environment: 'production' }] + }) + expect(result).toEqual(new Set(['prop-repo-1', 'prop-repo-2'])) + }) + }) + + describe('combined criteria', () => { + it('unions results from multiple criteria types', async () => { + // First call: teams resolution + github.paginate + .mockResolvedValueOnce([{ name: 'team-repo' }]) + // Second call: custom properties + .mockResolvedValueOnce([{ repository_name: 'prop-repo' }]) + + const selector = new RepoSelector(github, 'my-org', log) + const result = await selector.resolve({ + names: ['explicit-repo'], + teams: ['my-team'], + custom_properties: [{ tier: 'critical' }] + }) + expect(result).toEqual(new Set(['explicit-repo', 'team-repo', 'prop-repo'])) + }) + + it('all=true takes precedence over other criteria', async () => { + github.paginate.mockResolvedValue([ + { name: 'repo-1' }, + { name: 'repo-2' } + ]) + + const selector = new RepoSelector(github, 'my-org', log) + const result = await selector.resolve({ + all: true, + names: ['specific-repo'], + teams: ['my-team'] + }) + // Should return all repos, not filter by names/teams + expect(result).toEqual(new Set(['repo-1', 'repo-2'])) + // paginate called once for getAllRepos, not for teams + expect(github.paginate).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/test/unit/lib/settings.test.js b/test/unit/lib/settings.test.js index 39aac216d..55b4ecf3f 100644 --- a/test/unit/lib/settings.test.js +++ b/test/unit/lib/settings.test.js @@ -1,5 +1,5 @@ /* eslint-disable no-undef */ -const { Octokit } = require('@octokit/core') +class Octokit {} const Settings = require('../../../lib/settings') const yaml = require('js-yaml') // jest.mock('../../../lib/settings', () => { @@ -52,8 +52,10 @@ repository: topics: - frontend `).toString('base64'); - mockOctokit.repos = { - getContent: jest.fn().mockResolvedValue({ data: { content } }) + mockOctokit.rest = { + repos: { + getContent: jest.fn().mockResolvedValue({ data: { content } }) + } } mockOctokit.request = { @@ -310,8 +312,10 @@ repository: Settings.fileCache = {}; stubContext = { octokit: { - repos: { - getContent: jest.fn() + rest: { + repos: { + getContent: jest.fn() + } }, request: jest.fn(), paginate: jest.fn() @@ -334,7 +338,7 @@ repository: // Given const filePath = 'path/to/file.yml'; const content = Buffer.from('key: value').toString('base64'); - jest.spyOn(settings.github.repos, 'getContent').mockResolvedValue({ + jest.spyOn(settings.github.rest.repos, 'getContent').mockResolvedValue({ data: { content }, headers: { etag: 'etag123' } }); @@ -355,14 +359,14 @@ repository: const filePath = 'path/to/file.yml'; const content = Buffer.from('key: value').toString('base64'); Settings.fileCache[`${mockRepo.owner}/${filePath}`] = { etag: 'etag123', data: { content } }; - jest.spyOn(settings.github.repos, 'getContent').mockRejectedValue({ status: 304 }); + jest.spyOn(settings.github.rest.repos, 'getContent').mockRejectedValue({ status: 304 }); // When const result = await settings.loadYaml(filePath); // Then expect(result).toEqual({ key: 'value' }); - expect(settings.github.repos.getContent).toHaveBeenCalledWith( + expect(settings.github.rest.repos.getContent).toHaveBeenCalledWith( expect.objectContaining({ headers: { 'If-None-Match': 'etag123' } }) ); }); @@ -373,7 +377,7 @@ repository: const content = Buffer.from('key: value').toString('base64'); const wrongContent = Buffer.from('wrong: content').toString('base64'); Settings.fileCache['another-org/path/to/file.yml'] = { etag: 'etag123', data: { wrongContent } }; - jest.spyOn(settings.github.repos, 'getContent').mockResolvedValue({ + jest.spyOn(settings.github.rest.repos, 'getContent').mockResolvedValue({ data: { content }, headers: { etag: 'etag123' } }); @@ -388,7 +392,7 @@ repository: it('should return null when the file path is a folder', async () => { // Given const filePath = 'path/to/folder'; - jest.spyOn(settings.github.repos, 'getContent').mockResolvedValue({ + jest.spyOn(settings.github.rest.repos, 'getContent').mockResolvedValue({ data: [] }); @@ -402,7 +406,7 @@ repository: it('should return null when the file is a symlink or submodule', async () => { // Given const filePath = 'path/to/symlink'; - jest.spyOn(settings.github.repos, 'getContent').mockResolvedValue({ + jest.spyOn(settings.github.rest.repos, 'getContent').mockResolvedValue({ data: { content: null } }); @@ -416,7 +420,7 @@ repository: it('should handle 404 errors gracefully and return null', async () => { // Given const filePath = 'path/to/nonexistent.yml'; - jest.spyOn(settings.github.repos, 'getContent').mockRejectedValue({ status: 404 }); + jest.spyOn(settings.github.rest.repos, 'getContent').mockRejectedValue({ status: 404 }); // When const result = await settings.loadYaml(filePath); @@ -428,7 +432,7 @@ repository: it('should throw an error for non-404 exceptions when not in nop mode', async () => { // Given const filePath = 'path/to/error.yml'; - jest.spyOn(settings.github.repos, 'getContent').mockRejectedValue(new Error('Unexpected error')); + jest.spyOn(settings.github.rest.repos, 'getContent').mockRejectedValue(new Error('Unexpected error')); // When / Then await expect(settings.loadYaml(filePath)).rejects.toThrow('Unexpected error'); @@ -438,7 +442,7 @@ repository: // Given const filePath = 'path/to/error.yml'; settings.nop = true; - jest.spyOn(settings.github.repos, 'getContent').mockRejectedValue(new Error('Unexpected error')); + jest.spyOn(settings.github.rest.repos, 'getContent').mockRejectedValue(new Error('Unexpected error')); jest.spyOn(settings, 'appendToResults'); // When @@ -458,4 +462,1292 @@ repository: ); }); }); + + describe('getAllMatchingSubOrgSources', () => { + it('returns an empty set when subOrgConfigs is undefined', () => { + const settings = createSettings({}) + settings.subOrgConfigs = undefined + const result = settings.getAllMatchingSubOrgSources('any-repo') + expect(result).toBeInstanceOf(Set) + expect(result.size).toBe(0) + }) + + it('returns an empty set when no suborg matches', () => { + const settings = createSettings({}) + settings.subOrgConfigs = { + 'frontend-*': { source: '.github/suborgs/frontend.yml' } + } + const result = settings.getAllMatchingSubOrgSources('backend-repo') + expect(result.size).toBe(0) + }) + + it('returns a single-entry set when one suborg glob matches', () => { + const settings = createSettings({}) + settings.subOrgConfigs = { + 'frontend-*': { source: '.github/suborgs/frontend.yml' }, + 'backend-*': { source: '.github/suborgs/backend.yml' } + } + const result = settings.getAllMatchingSubOrgSources('frontend-app') + expect(result.size).toBe(1) + expect(result.has('.github/suborgs/frontend.yml')).toBe(true) + }) + + it('does not alter getSubOrgConfig single-match behavior', () => { + const settings = createSettings({}) + settings.subOrgConfigs = { + 'frontend-*': { source: '.github/suborgs/frontend.yml', tag: 'A' } + } + const before = settings.getSubOrgConfig('frontend-app') + settings.getAllMatchingSubOrgSources('frontend-app') + const after = settings.getSubOrgConfig('frontend-app') + expect(after).toBe(before) + expect(after.tag).toBe('A') + }) + }) + + describe('shouldConsiderReevaluation', () => { + let settings + const repo = { owner: 'o', repo: 'foo' } + beforeEach(() => { + settings = createSettings({}) + settings.repoConfigs = {} + }) + + describe('with changeSignals (preferred path)', () => { + it('returns true when teams plugin reported changes', () => { + expect(settings.shouldConsiderReevaluation(repo, null, { teamsChanged: true })).toBe(true) + }) + + it('returns true when custom_properties plugin reported changes', () => { + expect(settings.shouldConsiderReevaluation(repo, null, { propertiesChanged: true })).toBe(true) + }) + + it('returns true on repository rename', () => { + expect(settings.shouldConsiderReevaluation(repo, null, { renamed: true })).toBe(true) + }) + + it('returns true on repository create', () => { + expect(settings.shouldConsiderReevaluation(repo, null, { created: true })).toBe(true) + }) + + it('returns false when all change signals are false (steady state)', () => { + // Pre-existing team that is already on the repo -> diffable reports no + // changes -> we must NOT trigger a re-eval reload. + settings.repoConfigs = { 'foo.yml': { teams: [{ name: 'core' }] } } + const signals = { teamsChanged: false, propertiesChanged: false, renamed: false, created: false } + expect(settings.shouldConsiderReevaluation(repo, { name: 'foo' }, signals)).toBe(false) + }) + }) + + describe('without changeSignals (fallback)', () => { + it('returns false when there is no repo-yml entry', () => { + expect(settings.shouldConsiderReevaluation(repo, null)).toBe(false) + expect(settings.shouldConsiderReevaluation(repo, undefined)).toBe(false) + }) + + it('returns false when repo-yml has no teams/properties and no rename', () => { + settings.repoConfigs = { 'foo.yml': { repository: { name: 'foo' } } } + expect(settings.shouldConsiderReevaluation(repo, { name: 'foo' })).toBe(false) + }) + + it('returns true when repo-yml has teams', () => { + settings.repoConfigs = { 'foo.yml': { teams: [{ name: 'core' }] } } + expect(settings.shouldConsiderReevaluation(repo, { name: 'foo' })).toBe(true) + }) + + it('returns true when repo-yml has custom_properties', () => { + settings.repoConfigs = { 'foo.yaml': { custom_properties: [{ name: 'EDP', value: 'true' }] } } + expect(settings.shouldConsiderReevaluation(repo, { name: 'foo' })).toBe(true) + }) + + it('returns true on rename via repo.oldname', () => { + expect(settings.shouldConsiderReevaluation({ owner: 'o', repo: 'new', oldname: 'old' }, null)).toBe(true) + }) + + it('returns true on rename via repoConfig.oldname', () => { + expect(settings.shouldConsiderReevaluation(repo, { name: 'new', oldname: 'old' })).toBe(true) + }) + }) + }) + + describe('maybeReevaluateSuborg', () => { + it('is a no-op when reevaluateOnChange is false', async () => { + const settings = createSettings({}) + settings.reevaluateOnChange = false + settings.repoConfigs = { 'r.yml': { teams: [{ name: 'core' }] } } + const reloadSpy = jest.spyOn(settings, 'reloadSubOrgConfigs').mockResolvedValue() + await settings.maybeReevaluateSuborg({ owner: 'o', repo: 'r' }, { name: 'r' }, new Set()) + expect(reloadSpy).not.toHaveBeenCalled() + }) + + it('is a no-op when repo-yml has no triggers (teams/properties/rename)', async () => { + const settings = createSettings({}) + settings.reevaluateOnChange = true + settings.repoConfigs = { 'r.yml': { repository: { name: 'r' } } } + const reloadSpy = jest.spyOn(settings, 'reloadSubOrgConfigs').mockResolvedValue() + await settings.maybeReevaluateSuborg({ owner: 'o', repo: 'r' }, { name: 'r' }, new Set()) + expect(reloadSpy).not.toHaveBeenCalled() + }) + + it('is a no-op when changeSignals report no plugin changes (preexisting team)', async () => { + const settings = createSettings({}) + settings.reevaluateOnChange = true + // repo-yml has teams, but plugin reported no change (team already on repo) + settings.repoConfigs = { 'r.yml': { teams: [{ name: 'core' }] } } + const reloadSpy = jest.spyOn(settings, 'reloadSubOrgConfigs').mockResolvedValue() + const updateSpy = jest.spyOn(settings, 'updateRepos').mockResolvedValue() + const signals = { teamsChanged: false, propertiesChanged: false, renamed: false, created: false } + await settings.maybeReevaluateSuborg({ owner: 'o', repo: 'r' }, { name: 'r' }, new Set(), signals) + expect(reloadSpy).not.toHaveBeenCalled() + expect(updateSpy).not.toHaveBeenCalled() + }) + + it('stops when the matched suborg source set is stable (no new sources)', async () => { + const settings = createSettings({}) + settings.reevaluateOnChange = true + settings.subOrgConfigs = { 'r*': { source: '.github/suborgs/x.yml' } } + const updateSpy = jest.spyOn(settings, 'updateRepos').mockResolvedValue() + jest.spyOn(settings, 'reloadSubOrgConfigs').mockResolvedValue() + // pre = post = {x.yml} -> stable, no recursion + const pre = new Set(['.github/suborgs/x.yml']) + await settings.maybeReevaluateSuborg({ owner: 'o', repo: 'r1' }, { name: 'r1' }, pre, { teamsChanged: true }) + expect(updateSpy).not.toHaveBeenCalled() + }) + + it('recurses once when a new suborg source appears, then stops at depth cap', async () => { + const settings = createSettings({}) + settings.reevaluateOnChange = true + // After reload, a new suborg matches r1 + settings.subOrgConfigs = { 'r*': { source: '.github/suborgs/new.yml' } } + settings.repoConfigs = { 'r1.yml': { teams: [{ name: 't' }] } } + jest.spyOn(settings, 'reloadSubOrgConfigs').mockResolvedValue() + jest.spyOn(settings, 'getRepoConfigs').mockResolvedValue({ 'r1.yml': { teams: [{ name: 't' }] } }) + const updateSpy = jest.spyOn(settings, 'updateRepos').mockResolvedValue() + const pre = new Set() // pre-apply: nothing matched + await settings.maybeReevaluateSuborg({ owner: 'o', repo: 'r1' }, { name: 'r1' }, pre, { teamsChanged: true }) + expect(updateSpy).toHaveBeenCalledTimes(1) + expect(settings.reevaluationDepth.get('r1')).toBe(1) + }) + + it('recurses once when a previously matched suborg source disappears', async () => { + const settings = createSettings({}) + settings.reevaluateOnChange = true + settings.subOrgConfigs = {} + settings.repoConfigs = { 'r1.yml': { custom_properties: [{ property_name: 'team', value: 'other' }] } } + jest.spyOn(settings, 'reloadSubOrgConfigs').mockResolvedValue() + jest.spyOn(settings, 'getRepoConfigs').mockResolvedValue({ 'r1.yml': { custom_properties: [{ property_name: 'team', value: 'other' }] } }) + const updateSpy = jest.spyOn(settings, 'updateRepos').mockResolvedValue() + const pre = new Set(['.github/suborgs/old.yml']) + await settings.maybeReevaluateSuborg({ owner: 'o', repo: 'r1' }, { name: 'r1' }, pre, { propertiesChanged: true }) + expect(updateSpy).toHaveBeenCalledTimes(1) + expect(settings.reevaluationDepth.get('r1')).toBe(1) + }) + + it('respects MAX_REEVALUATION_DEPTH and logs a warning', async () => { + const settings = createSettings({}) + settings.reevaluateOnChange = true + settings.reevaluationDepth.set('r1', 1) // already at cap + settings.repoConfigs = { 'r1.yml': { teams: [{ name: 't' }] } } + stubContext.log.warn = jest.fn() + const reloadSpy = jest.spyOn(settings, 'reloadSubOrgConfigs').mockResolvedValue() + const updateSpy = jest.spyOn(settings, 'updateRepos').mockResolvedValue() + await settings.maybeReevaluateSuborg({ owner: 'o', repo: 'r1' }, { name: 'r1' }, new Set(), { teamsChanged: true }) + expect(reloadSpy).not.toHaveBeenCalled() + expect(updateSpy).not.toHaveBeenCalled() + expect(stubContext.log.warn).toHaveBeenCalledWith(expect.stringContaining('max depth')) + }) + }) + + // ──────────────────────────────────────────────────────────────────────── + // disable_plugins + // ──────────────────────────────────────────────────────────────────────── + describe('disable_plugins', () => { + const DeploymentConfig = require('../../../lib/deploymentConfig') + let savedDeploymentDisable + let savedPlugins + + beforeEach(() => { + savedDeploymentDisable = DeploymentConfig.config && DeploymentConfig.config.disable_plugins + if (DeploymentConfig.config) delete DeploymentConfig.config.disable_plugins + savedPlugins = { ...Settings.PLUGINS } + }) + + afterEach(() => { + if (DeploymentConfig.config) { + if (savedDeploymentDisable !== undefined) { + DeploymentConfig.config.disable_plugins = savedDeploymentDisable + } else { + delete DeploymentConfig.config.disable_plugins + } + } + Object.keys(Settings.PLUGINS).forEach(k => { Settings.PLUGINS[k] = savedPlugins[k] }) + }) + + // ── normalizeDisableEntries ────────────────────────────────────────── + describe('normalizeDisableEntries', () => { + it('1. string shorthand defaults target=all and sets declaredAt', () => { + const settings = createSettings({}) + const out = settings.normalizeDisableEntries(['labels'], 'org') + expect(out).toEqual([{ plugin: 'labels', target: 'all', declaredAt: 'org' }]) + }) + + it('2. object form preserves each of self|children|all', () => { + const settings = createSettings({}) + const out = settings.normalizeDisableEntries([ + { plugin: 'rulesets', target: 'self' }, + { plugin: 'branches', target: 'children' }, + { plugin: 'labels', target: 'all' } + ], 'org') + expect(out).toEqual([ + { plugin: 'rulesets', target: 'self', declaredAt: 'org' }, + { plugin: 'branches', target: 'children', declaredAt: 'org' }, + { plugin: 'labels', target: 'all', declaredAt: 'org' } + ]) + }) + + it('3. unknown plugin name throws descriptive error', () => { + const settings = createSettings({}) + expect(() => settings.normalizeDisableEntries(['nope'], 'org')) + .toThrow(/unknown plugin 'nope'/) + }) + + it('4. invalid target throws', () => { + const settings = createSettings({}) + expect(() => settings.normalizeDisableEntries([{ plugin: 'labels', target: 'bogus' }], 'org')) + .toThrow(/invalid target 'bogus'/) + }) + + it('5. repository and archive are accepted as plugin names', () => { + const settings = createSettings({}) + const out = settings.normalizeDisableEntries(['repository', 'archive'], 'org') + expect(out.map(e => e.plugin).sort()).toEqual(['archive', 'repository']) + }) + + it('6. at declaredAt=repo, target=children normalizes to all', () => { + const settings = createSettings({}) + const out = settings.normalizeDisableEntries([{ plugin: 'labels', target: 'children' }], 'repo') + expect(out).toEqual([{ plugin: 'labels', target: 'all', declaredAt: 'repo' }]) + }) + }) + + // ── computeStripMap ────────────────────────────────────────────────── + describe('computeStripMap', () => { + const repoName = 'my-repo' + + it('7. empty configs produce empty map (all four levels are empty sets)', () => { + const settings = createSettings({}) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + const sm = settings.computeStripMap(repoName) + for (const level of ['deployment', 'org', 'suborg', 'repo']) { + expect(sm.get(level).size).toBe(0) + } + }) + + it('8. org target:self for rulesets strips only the org layer', () => { + const settings = createSettings({ disable_plugins: [{ plugin: 'rulesets', target: 'self' }] }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + const sm = settings.computeStripMap(repoName) + expect([...sm.get('org')]).toEqual(['rulesets']) + expect(sm.get('suborg').size).toBe(0) + expect(sm.get('repo').size).toBe(0) + expect(sm.get('deployment').size).toBe(0) + }) + + it('9. org target:children for branches strips suborg+repo', () => { + const settings = createSettings({ disable_plugins: [{ plugin: 'branches', target: 'children' }] }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + const sm = settings.computeStripMap(repoName) + expect(sm.get('org').size).toBe(0) + expect([...sm.get('suborg')]).toEqual(['branches']) + expect([...sm.get('repo')]).toEqual(['branches']) + }) + + it('10. org target:all for labels strips org+suborg+repo', () => { + const settings = createSettings({ disable_plugins: ['labels'] }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + const sm = settings.computeStripMap(repoName) + expect([...sm.get('org')]).toEqual(['labels']) + expect([...sm.get('suborg')]).toEqual(['labels']) + expect([...sm.get('repo')]).toEqual(['labels']) + }) + + it('11. suborg target:all contributes only when a suborg matches the repo', () => { + const settings = createSettings({}) + settings.subOrgConfigs = { + [repoName]: { disable_plugins: ['teams'], source: '.github/suborgs/x.yml' } + } + settings.repoConfigs = {} + const sm = settings.computeStripMap(repoName) + expect([...sm.get('suborg')]).toEqual(['teams']) + expect([...sm.get('repo')]).toEqual(['teams']) + + const sm2 = settings.computeStripMap('other-repo') + expect(sm2.get('suborg').size).toBe(0) + expect(sm2.get('repo').size).toBe(0) + }) + + it('12. repo-declared target:all only strips repo layer', () => { + const settings = createSettings({}) + settings.subOrgConfigs = {} + settings.repoConfigs = { [`${repoName}.yml`]: { disable_plugins: ['labels'] } } + const sm = settings.computeStripMap(repoName) + expect(sm.get('org').size).toBe(0) + expect(sm.get('suborg').size).toBe(0) + expect([...sm.get('repo')]).toEqual(['labels']) + }) + + it('13. deployment target:children strips org+suborg+repo', () => { + DeploymentConfig.config.disable_plugins = [{ plugin: 'milestones', target: 'children' }] + const settings = createSettings({}) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + const sm = settings.computeStripMap(repoName) + expect(sm.get('deployment').size).toBe(0) + expect([...sm.get('org')]).toEqual(['milestones']) + expect([...sm.get('suborg')]).toEqual(['milestones']) + expect([...sm.get('repo')]).toEqual(['milestones']) + }) + + it('14. union across layers: org self + repo all → org and repo both contain plugin', () => { + const settings = createSettings({ disable_plugins: [{ plugin: 'labels', target: 'self' }] }) + settings.subOrgConfigs = {} + settings.repoConfigs = { [`${repoName}.yml`]: { disable_plugins: ['labels'] } } + const sm = settings.computeStripMap(repoName) + expect([...sm.get('org')]).toEqual(['labels']) + expect(sm.get('suborg').size).toBe(0) + expect([...sm.get('repo')]).toEqual(['labels']) + }) + }) + + // ── childPluginsList integration ───────────────────────────────────── + describe('childPluginsList integration', () => { + it('15. org disables custom_properties (target:all) → not in plugin list even with repo override', () => { + const settings = createSettings({ + disable_plugins: ['custom_properties'], + custom_properties: [{ property_name: 'a', value: '1' }] + }) + settings.subOrgConfigs = {} + settings.repoConfigs = { 'foo.yml': { custom_properties: [{ property_name: 'b', value: '2' }] } } + const list = settings.childPluginsList({ repo: 'foo' }) + const pluginNames = list.map(([P]) => Object.keys(Settings.PLUGINS).find(k => Settings.PLUGINS[k] === P)) + expect(pluginNames).not.toContain('custom_properties') + }) + + it('16. suborg-declared branches + suborg disable_plugins:branches → stripped for matched repo only', () => { + // Per the matrix, suborg target:all strips suborg+repo (NOT org). + // So we put branches only at suborg level for a meaningful test. + const settings = createSettings({}) + settings.subOrgConfigs = { + 'matched-repo': { + disable_plugins: ['branches'], + branches: [{ name: 'main', protection: {} }], + source: '.github/suborgs/x.yml' + }, + 'other-repo': { + // different suborg without disable; still declares branches + branches: [{ name: 'main', protection: {} }], + source: '.github/suborgs/y.yml' + } + } + settings.repoConfigs = {} + const matched = settings.childPluginsList({ repo: 'matched-repo' }).map(([P]) => + Object.keys(Settings.PLUGINS).find(k => Settings.PLUGINS[k] === P)) + const other = settings.childPluginsList({ repo: 'other-repo' }).map(([P]) => + Object.keys(Settings.PLUGINS).find(k => Settings.PLUGINS[k] === P)) + expect(matched).not.toContain('branches') + expect(other).toContain('branches') + }) + + it('17. repo-level labels + repo disable_plugins:labels → stripped for that repo only', () => { + // Repo target:all strips only the repo layer (matrix). To demonstrate + // scoping we put labels in each repo's own yml so the strip is effective. + const settings = createSettings({}) + settings.subOrgConfigs = {} + settings.repoConfigs = { + 'foo.yml': { disable_plugins: ['labels'], labels: { include: [{ name: 'bug' }] } }, + 'bar.yml': { labels: { include: [{ name: 'bug' }] } } + } + const foo = settings.childPluginsList({ repo: 'foo' }).map(([P]) => + Object.keys(Settings.PLUGINS).find(k => Settings.PLUGINS[k] === P)) + const bar = settings.childPluginsList({ repo: 'bar' }).map(([P]) => + Object.keys(Settings.PLUGINS).find(k => Settings.PLUGINS[k] === P)) + expect(foo).not.toContain('labels') + expect(bar).toContain('labels') + }) + + it('18. org target:children for variables: org-level variables still run per-repo (documented nuance)', () => { + // target:children strips from suborg+repo only; merged repo plugin + // config still inherits the org-level variables → plugin DOES run. + const settings = createSettings({ + disable_plugins: [{ plugin: 'variables', target: 'children' }], + variables: [{ name: 'FOO', value: 'bar' }] + }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + const names = settings.childPluginsList({ repo: 'foo' }).map(([P]) => + Object.keys(Settings.PLUGINS).find(k => Settings.PLUGINS[k] === P)) + expect(names).toContain('variables') + }) + + it('19. org target:all for variables: variables plugin is fully suppressed', () => { + const settings = createSettings({ + disable_plugins: [{ plugin: 'variables', target: 'all' }], + variables: [{ name: 'FOO', value: 'bar' }] + }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + const names = settings.childPluginsList({ repo: 'foo' }).map(([P]) => + Object.keys(Settings.PLUGINS).find(k => Settings.PLUGINS[k] === P)) + expect(names).not.toContain('variables') + }) + }) + + // ── updateOrg integration ──────────────────────────────────────────── + describe('updateOrg integration', () => { + function stubPlugin () { + const sync = jest.fn().mockResolvedValue([]) + const ctor = jest.fn().mockImplementation(() => ({ sync })) + return { ctor, sync } + } + + it('20. org disable rulesets (target:self) → rulesets plugin NOT invoked', async () => { + const { ctor } = stubPlugin() + Settings.PLUGINS.rulesets = ctor + const settings = createSettings({ + disable_plugins: [{ plugin: 'rulesets', target: 'self' }], + rulesets: [{ name: 'foo' }] + }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + await settings.updateOrg() + expect(ctor).not.toHaveBeenCalled() + }) + + it('21. org disable custom_repository_roles (shorthand) → plugin NOT invoked', async () => { + const { ctor } = stubPlugin() + Settings.PLUGINS.custom_repository_roles = ctor + const settings = createSettings({ + disable_plugins: ['custom_repository_roles'], + custom_repository_roles: [{ name: 'sec' }] + }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + await settings.updateOrg() + expect(ctor).not.toHaveBeenCalled() + }) + + it('22. deployment disable rulesets overrides org config that wants rulesets', async () => { + DeploymentConfig.config.disable_plugins = ['rulesets'] + const { ctor } = stubPlugin() + Settings.PLUGINS.rulesets = ctor + const settings = createSettings({ rulesets: [{ name: 'foo' }] }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + await settings.updateOrg() + expect(ctor).not.toHaveBeenCalled() + }) + + it('23. org custom_repository_roles receives additive=true when listed in additive_plugins', async () => { + const instances = [] + const ctor = jest.fn().mockImplementation(function () { + this.sync = jest.fn().mockResolvedValue([]) + instances.push(this) + }) + Settings.PLUGINS.custom_repository_roles = ctor + + const settings = createSettings({ + additive_plugins: ['custom_repository_roles'], + custom_repository_roles: [{ name: 'sec' }] + }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + await settings.updateOrg() + + expect(instances).toHaveLength(1) + expect(instances[0].additive).toBe(true) + }) + }) + + // ── updateRepos integration ────────────────────────────────────────── + describe('updateRepos integration', () => { + it('24. org disable repository → RepoPlugin not instantiated', async () => { + const repoSync = jest.fn().mockResolvedValue([]) + const repoCtor = jest.fn().mockImplementation(() => ({ sync: repoSync, renamed: false, created: false })) + Settings.PLUGINS.repository = repoCtor + const settings = createSettings({ + disable_plugins: ['repository'], + repository: { name: 'will-not-be-used' } + }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + // Avoid running child plugins (their internal logic isn't under test). + jest.spyOn(settings, 'childPluginsList').mockReturnValue([]) + await settings.updateRepos({ owner: 'o', repo: 'r' }) + expect(repoCtor).not.toHaveBeenCalled() + }) + + it('24. org disable archive → archive plugin getState NOT invoked', async () => { + const Archive = require('../../../lib/plugins/archive') + const getStateSpy = jest.spyOn(Archive.prototype, 'getState').mockResolvedValue({ shouldArchive: false, shouldUnarchive: false }) + // RepoPlugin still runs; stub it to a no-op constructor. + const repoSync = jest.fn().mockResolvedValue([]) + Settings.PLUGINS.repository = jest.fn().mockImplementation(() => ({ sync: repoSync, renamed: false, created: false })) + const settings = createSettings({ + disable_plugins: ['archive'], + repository: { name: 'r' } + }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + jest.spyOn(settings, 'childPluginsList').mockReturnValue([]) + await settings.updateRepos({ owner: 'o', repo: 'r' }) + expect(getStateSpy).not.toHaveBeenCalled() + getStateSpy.mockRestore() + }) + }) + + // ── cascade enforcement ────────────────────────────────────────────── + describe('cascade enforcement', () => { + it('25. org target:all labels; repo declares empty disable_plugins → labels still disabled', () => { + const settings = createSettings({ + disable_plugins: ['labels'], + labels: { include: [{ name: 'bug' }] } + }) + settings.subOrgConfigs = {} + settings.repoConfigs = { 'foo.yml': { disable_plugins: [] } } + const names = settings.childPluginsList({ repo: 'foo' }).map(([P]) => + Object.keys(Settings.PLUGINS).find(k => Settings.PLUGINS[k] === P)) + expect(names).not.toContain('labels') + }) + }) + + // ── NOP mode ───────────────────────────────────────────────────────── + describe('NOP mode', () => { + it('26. each strip produces a NopCommand with type=INFO and plugin/level info', () => { + const settings = new Settings(true, stubContext, mockRepo, { + disable_plugins: ['labels'], + labels: { include: [{ name: 'bug' }] } + }, mockRef) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + settings.childPluginsList({ repo: 'foo' }) + const nopEntries = settings.results.filter(r => r && r.plugin === 'disable_plugins') + expect(nopEntries.length).toBeGreaterThan(0) + expect(nopEntries[0].type).toBe('INFO') + expect(nopEntries[0].action.msg).toMatch(/labels/) + expect(nopEntries[0].action.msg).toMatch(/declared by/) + }) + + it('27. dedup retains all disable_plugins NopCommands when multiple plugins are disabled for the same repo', () => { + // Disable both labels and teams at org level for all layers. + const settings = new Settings(true, stubContext, mockRepo, { + disable_plugins: ['labels', 'teams'], + labels: [{ name: 'bug', color: 'red' }], + teams: [{ name: 'core', permission: 'push' }] + }, mockRef) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + settings.childPluginsList({ repo: 'foo' }) + const nopEntries = settings.results.filter(r => r && r.plugin === 'disable_plugins') + // Both 'labels' and 'teams' disable messages must survive; the old + // dedup (key = type+repo+plugin+endpoint) would drop one of them + // because they share the same empty endpoint. The new key adds + // action.msg, so each unique message is kept. + const msgs = nopEntries.map(r => r.action.msg) + expect(msgs.some(m => /labels/.test(m))).toBe(true) + expect(msgs.some(m => /teams/.test(m))).toBe(true) + }) + + it.each([ + ['without a check run', {}], + ['without a repository', { check_run: { id: 123 } }] + ])('28. full-sync dry run %s logs a value-free summary instead of updating a check run', async (_description, payload) => { + stubContext.payload = { installation: { id: 123 }, ...payload } + stubContext.octokit.checks = { update: jest.fn().mockResolvedValue({}) } + + const settings = new Settings(true, stubContext, mockRepo, {}, mockRef) + settings.results = [{ + type: 'INFO', + plugin: 'Variables', + repo: 'test/test-repo', + endpoint: '', + action: { + msg: 'Changes found', + additions: {}, + modifications: { MY_VAR: { value: 'plain-value' } }, + deletions: {} + } + }] + + await settings.handleResults() + + expect(stubContext.log.info).toHaveBeenCalledWith(expect.stringContaining('Changes found')) + expect(stubContext.log.info).not.toHaveBeenCalledWith(expect.stringContaining('plain-value')) + expect(stubContext.log.debug).toHaveBeenCalledWith({ results: settings.results }, 'Dry-run results') + expect(stubContext.octokit.checks.update).not.toHaveBeenCalled() + }) + + it('28. base-config filtering preserves org-rulesets informational NopCommands', async () => { + stubContext.payload.repository = { owner: { login: 'test' }, name: 'safe-settings' } + stubContext.payload.check_run = { id: 123, check_suite: { pull_requests: [{ number: 456 }] } } + stubContext.octokit.rest.checks = { update: jest.fn().mockResolvedValue({}) } + stubContext.octokit.rest.issues = { createComment: jest.fn().mockResolvedValue({}) } + + const settings = new Settings(true, stubContext, mockRepo, { + rulesets: [{ name: 'managed', enforcement: 'disabled' }] + }, mockRef) + settings.baseConfig = { + rulesets: [{ name: 'managed', enforcement: 'active' }] + } + settings.results = [{ + type: 'INFO', + plugin: 'Rulesets', + repo: 'test (org)', + endpoint: '', + action: { + msg: 'Additive mode active: 1 deletion(s) suppressed by additive_plugins', + additions: null, + modifications: null, + deletions: null + } + }] + + await settings.handleResults() + + expect(stubContext.octokit.rest.checks.update).toHaveBeenCalled() + const summary = stubContext.octokit.rest.checks.update.mock.calls[0][0].output.summary + expect(summary).toMatch(/Informational messages/) + expect(summary).toMatch(/suppressed by additive_plugins/) + }) + }) + }) + + // ════════════════════════════════════════════════════════════════════════ + describe('additive_plugins', () => { + // ── Settings.ADDITIVE_PLUGINS constant ─────────────────────────────── + describe('Settings.ADDITIVE_PLUGINS', () => { + it('28. contains all 11 additive plugin names', () => { + const expected = new Set([ + 'labels', 'collaborators', 'teams', 'milestones', 'autolinks', + 'environments', 'custom_properties', 'variables', 'rulesets', + 'custom_repository_roles', 'app_installations' + ]) + expect(Settings.ADDITIVE_PLUGINS).toEqual(expected) + }) + + it('29. does NOT include non-Diffable plugins', () => { + expect(Settings.ADDITIVE_PLUGINS.has('repository')).toBe(false) + expect(Settings.ADDITIVE_PLUGINS.has('archive')).toBe(false) + expect(Settings.ADDITIVE_PLUGINS.has('branches')).toBe(false) + expect(Settings.ADDITIVE_PLUGINS.has('validator')).toBe(false) + }) + }) + + // ── normalizeAdditivePlugins ───────────────────────────────────────── + describe('normalizeAdditivePlugins', () => { + it('30. returns empty Set when additive_plugins is absent', () => { + const settings = createSettings({}) + expect(settings.normalizeAdditivePlugins().size).toBe(0) + }) + + it('31. returns correct Set for valid plugin names', () => { + const settings = createSettings({ additive_plugins: ['labels', 'teams', 'milestones'] }) + const result = settings.normalizeAdditivePlugins() + expect(result).toEqual(new Set(['labels', 'teams', 'milestones'])) + }) + + it('32. all 11 additive plugins are accepted without error', () => { + const all = [...Settings.ADDITIVE_PLUGINS] + const settings = createSettings({ additive_plugins: all }) + const logErrorSpy = jest.spyOn(settings, 'logError').mockImplementation(() => {}) + const result = settings.normalizeAdditivePlugins() + expect(result.size).toBe(11) + expect(logErrorSpy).not.toHaveBeenCalled() + logErrorSpy.mockRestore() + }) + + it('33. unknown plugin name logs error and is excluded from Set', () => { + const settings = createSettings({ additive_plugins: ['labels', 'nope-plugin'] }) + const logErrorSpy = jest.spyOn(settings, 'logError').mockImplementation(() => {}) + const result = settings.normalizeAdditivePlugins() + expect(result.has('labels')).toBe(true) + expect(result.has('nope-plugin')).toBe(false) + expect(logErrorSpy).toHaveBeenCalledWith(expect.stringMatching(/unknown or non-Diffable plugin 'nope-plugin'/)) + logErrorSpy.mockRestore() + }) + + it('34. non-Diffable plugin name (branches) logs error and is excluded', () => { + const settings = createSettings({ additive_plugins: ['branches'] }) + const logErrorSpy = jest.spyOn(settings, 'logError').mockImplementation(() => {}) + const result = settings.normalizeAdditivePlugins() + expect(result.has('branches')).toBe(false) + expect(logErrorSpy).toHaveBeenCalledWith(expect.stringMatching(/unknown or non-Diffable plugin 'branches'/)) + logErrorSpy.mockRestore() + }) + + it('35. non-string entries log error and are skipped', () => { + const settings = createSettings({ additive_plugins: ['labels', 42, null] }) + const logErrorSpy = jest.spyOn(settings, 'logError').mockImplementation(() => {}) + const result = settings.normalizeAdditivePlugins() + expect(result).toEqual(new Set(['labels'])) + expect(logErrorSpy).toHaveBeenCalledTimes(2) // 42 + null + logErrorSpy.mockRestore() + }) + + it('36. non-array value logs error and returns empty Set', () => { + const settings = createSettings({ additive_plugins: 'labels' }) + const logErrorSpy = jest.spyOn(settings, 'logError').mockImplementation(() => {}) + const result = settings.normalizeAdditivePlugins() + expect(result.size).toBe(0) + expect(logErrorSpy).toHaveBeenCalledWith(expect.stringMatching(/must be an array/)) + logErrorSpy.mockRestore() + }) + }) + + // ── childPluginsList returns triplets ──────────────────────────────── + describe('childPluginsList triplets', () => { + it('37. each entry includes section name as 3rd element', () => { + const settings = createSettings({ labels: [{ name: 'bug', color: 'red' }] }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + const list = settings.childPluginsList({ repo: 'foo' }) + expect(list.length).toBeGreaterThan(0) + list.forEach(entry => { + expect(entry.length).toBe(3) + expect(typeof entry[2]).toBe('string') + expect(entry[2]).toMatch(/^[a-z_]+$/) + }) + }) + + it('38. section names map to the correct Settings.PLUGINS keys', () => { + const settings = createSettings({ + labels: [{ name: 'bug', color: 'red' }], + teams: [{ name: 'core', permission: 'push' }] + }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + const list = settings.childPluginsList({ repo: 'foo' }) + list.forEach(([Plugin, , section]) => { + expect(Settings.PLUGINS[section]).toBe(Plugin) + }) + }) + }) + + // ── updateRepos integration: additive flag threading ───────────────── + describe('updateRepos integration: additive flag', () => { + it('processes changed repo configs that were not returned by the installation repository list', async () => { + const settings = createSettings({ restrictedRepos: {} }) + const updateReposSpy = jest.spyOn(settings, 'updateRepos').mockResolvedValue([]) + + settings.processedRepoNames = new Set(['existing-repo']) + + await settings.updateChangedRepoConfigs([ + { owner: 'test', repo: 'existing-repo' }, + { owner: 'test', repo: 'new-repo' }, + { owner: 'test', repo: 'new-repo' } + ]) + + expect(updateReposSpy).toHaveBeenCalledTimes(1) + expect(updateReposSpy).toHaveBeenCalledWith({ owner: 'test', repo: 'new-repo' }) + }) + + it('39. plugin listed in additive_plugins has additive=true set before sync()', async () => { + const instances = [] + const syncMock = jest.fn().mockResolvedValue([]) + const LabelsCtor = jest.fn().mockImplementation(function (...args) { + this.sync = syncMock + this.hasChanges = false + instances.push(this) + }) + const savedLabels = Settings.PLUGINS.labels + Settings.PLUGINS.labels = LabelsCtor + + const repoSync = jest.fn().mockResolvedValue([]) + Settings.PLUGINS.repository = jest.fn().mockImplementation(() => ({ + sync: repoSync, renamed: false, created: false + })) + + try { + const settings = createSettings({ + additive_plugins: ['labels'], + labels: [{ name: 'bug', color: 'red' }] + }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + // Clear subOrgConfigMap so the "suborg-change early return" in + // updateRepos does not fire (mockSubOrg='frontend' sets it in ctor). + settings.subOrgConfigMap = null + // Mock childPluginsList to return just the labels triplet so we can + // control what updateRepos sees without mocking all other plugins. + jest.spyOn(settings, 'childPluginsList').mockReturnValue([ + [LabelsCtor, [{ name: 'bug', color: 'red' }], 'labels'] + ]) + jest.spyOn(settings, 'maybeReevaluateSuborg').mockResolvedValue(undefined) + await settings.updateRepos({ owner: 'o', repo: 'r' }) + expect(instances.length).toBeGreaterThan(0) + // Every labels instance must have additive=true + instances.forEach(inst => expect(inst.additive).toBe(true)) + } finally { + Settings.PLUGINS.labels = savedLabels + } + }) + + it('40. plugin NOT in additive_plugins has additive=false (default)', async () => { + const instances = [] + const syncMock = jest.fn().mockResolvedValue([]) + const TeamsCtor = jest.fn().mockImplementation(function (...args) { + this.sync = syncMock + this.hasChanges = false + instances.push(this) + }) + const savedTeams = Settings.PLUGINS.teams + Settings.PLUGINS.teams = TeamsCtor + + Settings.PLUGINS.repository = jest.fn().mockImplementation(() => ({ + sync: jest.fn().mockResolvedValue([]), renamed: false, created: false + })) + + try { + const settings = createSettings({ + additive_plugins: ['labels'], // teams is NOT listed + teams: [{ name: 'core', permission: 'push' }] + }) + settings.subOrgConfigs = {} + settings.repoConfigs = {} + // Clear subOrgConfigMap so the "suborg-change early return" does not fire. + settings.subOrgConfigMap = null + jest.spyOn(settings, 'childPluginsList').mockReturnValue([ + [TeamsCtor, [{ name: 'core', permission: 'push' }], 'teams'] + ]) + jest.spyOn(settings, 'maybeReevaluateSuborg').mockResolvedValue(undefined) + await settings.updateRepos({ owner: 'o', repo: 'r' }) + expect(instances.length).toBeGreaterThan(0) + instances.forEach(inst => expect(inst.additive).toBe(false)) + } finally { + Settings.PLUGINS.teams = savedTeams + } + }) + }) + + // ── Diffable.sync() additive behaviour ─────────────────────────────── + describe('Diffable.sync() additive behaviour', () => { + const Diffable = require('../../../lib/plugins/diffable') + + // Minimal concrete Diffable subclass for testing. + class TestDiffable extends Diffable { + constructor (nop, entries) { + super(nop, {}, { owner: 'o', repo: 'r' }, entries, { debug: jest.fn(), info: jest.fn(), error: jest.fn() }, []) + } + + find () { return Promise.resolve(this._existing || []) } + comparator (a, b) { return a.name === b.name } + changed (a, b) { return a.value !== b.value } + add (attrs) { return Promise.resolve([]) } + update (existing, attrs) { return Promise.resolve([]) } + remove (existing) { return Promise.resolve([]) } + } + + it('41. additive=false → remove() is called for unmatched existing entries', async () => { + const plugin = new TestDiffable(false, [{ name: 'keep', value: '1' }]) + plugin._existing = [ + { name: 'keep', value: '1' }, + { name: 'gone', value: '2' } // this one has no match in entries + ] + plugin.additive = false + const removeSpy = jest.spyOn(plugin, 'remove').mockResolvedValue([]) + await plugin.sync() + expect(removeSpy).toHaveBeenCalledTimes(1) + expect(removeSpy).toHaveBeenCalledWith(expect.objectContaining({ name: 'gone' })) + removeSpy.mockRestore() + }) + + it('42. additive=true → remove() is NOT called even when existing entries have no YAML match', async () => { + const plugin = new TestDiffable(false, [{ name: 'keep', value: '1' }]) + plugin._existing = [ + { name: 'keep', value: '1' }, + { name: 'gone', value: '2' } + ] + plugin.additive = true + const removeSpy = jest.spyOn(plugin, 'remove').mockResolvedValue([]) + await plugin.sync() + expect(removeSpy).not.toHaveBeenCalled() + removeSpy.mockRestore() + }) + + it('43. additive=true → add() is still called for new YAML entries', async () => { + const plugin = new TestDiffable(false, [ + { name: 'existing', value: '1' }, + { name: 'new-entry', value: '2' } + ]) + plugin._existing = [{ name: 'existing', value: '1' }] + plugin.additive = true + const addSpy = jest.spyOn(plugin, 'add').mockResolvedValue([]) + const removeSpy = jest.spyOn(plugin, 'remove').mockResolvedValue([]) + await plugin.sync() + expect(addSpy).toHaveBeenCalledWith(expect.objectContaining({ name: 'new-entry' })) + expect(removeSpy).not.toHaveBeenCalled() + addSpy.mockRestore() + removeSpy.mockRestore() + }) + + it('44. additive=true → update() is still called for changed entries', async () => { + const plugin = new TestDiffable(false, [{ name: 'item', value: 'new' }]) + plugin._existing = [{ name: 'item', value: 'old' }] + plugin.additive = true + const updateSpy = jest.spyOn(plugin, 'update').mockResolvedValue([]) + const removeSpy = jest.spyOn(plugin, 'remove').mockResolvedValue([]) + await plugin.sync() + expect(updateSpy).toHaveBeenCalledWith( + expect.objectContaining({ name: 'item', value: 'old' }), + expect.objectContaining({ name: 'item', value: 'new' }) + ) + expect(removeSpy).not.toHaveBeenCalled() + updateSpy.mockRestore() + removeSpy.mockRestore() + }) + + it('45. NOP mode + additive=true + deletions present → INFO NopCommand about suppressed deletions', async () => { + const plugin = new TestDiffable(true, [{ name: 'keep', value: '1' }]) + plugin._existing = [ + { name: 'keep', value: '1' }, + { name: 'gone', value: '2' } + ] + plugin.additive = true + const result = await plugin.sync() + const suppressed = result.flat().filter(cmd => + cmd && cmd.type === 'INFO' && /suppressed by additive_plugins/i.test(cmd.action.msg) + ) + expect(suppressed.length).toBeGreaterThan(0) + expect(suppressed[0].action.msg).toMatch(/1 deletion/) + }) + + it('46. NOP mode + additive=true + NO deletions → no suppressed message emitted', async () => { + const plugin = new TestDiffable(true, [{ name: 'item', value: '1' }]) + plugin._existing = [{ name: 'item', value: '1' }] // identical → no changes at all + plugin.additive = true + const result = await plugin.sync() + if (result) { + const suppressed = result.flat().filter(cmd => + cmd && cmd.action && /suppressed by additive_plugins/i.test(cmd.action.msg) + ) + expect(suppressed.length).toBe(0) + } + // result may be undefined (no changes) which is also correct + }) + }) + }) + + describe('getReposRemovedFromSubOrgTargeting', () => { + let settings + + beforeEach(() => { + stubConfig = { restrictedRepos: {} } + settings = createSettings(stubConfig) + }) + + it('returns empty result when no changedSubOrgs provided', async () => { + const result = await settings.getReposRemovedFromSubOrgTargeting([], 'prev-sha') + expect(result.repos).toEqual([]) + expect(result.previousPluginSections).toEqual([]) + }) + + it('returns empty result when no baseRef provided', async () => { + const result = await settings.getReposRemovedFromSubOrgTargeting([{ path: '.github/suborgs/frontend.yml' }], null) + expect(result.repos).toEqual([]) + expect(result.previousPluginSections).toEqual([]) + }) + + it('identifies repos removed from suborgrepos targeting', async () => { + // Previous config had repo-a and repo-b in suborgrepos + const previousContent = Buffer.from(yaml.dump({ + suborgrepos: ['repo-a', 'repo-b'], + teams: [{ name: 'core', permission: 'push' }] + })).toString('base64') + + stubContext.octokit.rest.repos.getContent = jest.fn().mockImplementation((params) => { + if (params.ref === 'prev-sha') { + return Promise.resolve({ data: { content: previousContent } }) + } + // Current config: default mock (has new-repo in suborgrepos) + const currentContent = Buffer.from(yaml.dump({ + suborgrepos: ['repo-b'], + teams: [{ name: 'core', permission: 'push' }] + })).toString('base64') + return Promise.resolve({ data: { content: currentContent } }) + }) + + // Mock installation repos for glob resolution + stubContext.octokit.paginate = jest.fn().mockResolvedValue([ + { name: 'repo-a', owner: { login: 'test' } }, + { name: 'repo-b', owner: { login: 'test' } } + ]) + + // Current subOrgConfigs only has repo-b (repo-a was removed from targeting) + settings.subOrgConfigs = { + 'repo-b': { source: '.github/suborgs/frontend.yml' } + } + + const result = await settings.getReposRemovedFromSubOrgTargeting( + [{ path: '.github/suborgs/frontend.yml', name: 'frontend' }], + 'prev-sha' + ) + + expect(result.repos).toContain('repo-a') + expect(result.repos).not.toContain('repo-b') + expect(result.previousPluginSections).toContain('teams') + }) + + it('identifies repos removed from suborgrepos glob targeting', async () => { + const previousContent = Buffer.from(yaml.dump({ + suborgrepos: ['team-*'], + teams: [{ name: 'core', permission: 'push' }] + })).toString('base64') + + stubContext.octokit.rest.repos.getContent = jest.fn().mockImplementation((params) => { + if (params.ref === 'prev-sha') { + return Promise.resolve({ data: { content: previousContent } }) + } + return Promise.resolve({ data: { content: previousContent } }) + }) + + stubContext.octokit.paginate = jest.fn().mockResolvedValue([ + { owner: { login: 'test' }, name: 'team-a1' }, + { owner: { login: 'test' }, name: 'team-b1' }, + { owner: { login: 'test' }, name: 'other' } + ]) + + // Current targeting only matches team-a* + settings.subOrgConfigs = { + 'team-a*': { source: '.github/suborgs/frontend.yml' } + } + + const result = await settings.getReposRemovedFromSubOrgTargeting( + [{ path: '.github/suborgs/frontend.yml', name: 'frontend' }], + 'prev-sha' + ) + + expect(result.repos).toContain('team-b1') + expect(result.repos).not.toContain('team-a1') + expect(result.repos).not.toContain('other') + }) + + it('identifies repos removed from suborgteams targeting', async () => { + // Previous config used suborgteams: [team-a] + const previousContent = Buffer.from(yaml.dump({ + suborgteams: ['team-a'], + teams: [{ name: 'core', permission: 'push' }] + })).toString('base64') + + stubContext.octokit.rest.repos.getContent = jest.fn().mockImplementation((params) => { + if (params.ref === 'prev-sha') { + return Promise.resolve({ data: { content: previousContent } }) + } + return Promise.resolve({ data: { content: previousContent } }) + }) + + // Mock getReposForTeam to return repos for team-a + settings.getReposForTeam = jest.fn().mockResolvedValue([ + { name: 'team-repo-1' }, + { name: 'team-repo-2' } + ]) + + // Current subOrgConfigs: only team-repo-1 still matches (team-repo-2 was removed) + settings.subOrgConfigs = { + 'team-repo-1': { source: '.github/suborgs/frontend.yml' } + } + + const result = await settings.getReposRemovedFromSubOrgTargeting( + [{ path: '.github/suborgs/frontend.yml', name: 'frontend' }], + 'prev-sha' + ) + + expect(result.repos).toContain('team-repo-2') + expect(result.repos).not.toContain('team-repo-1') + }) + + it('identifies repos removed from suborgproperties targeting', async () => { + // Previous config used suborgproperties + const previousContent = Buffer.from(yaml.dump({ + suborgproperties: [{ EDP: true }], + teams: [{ name: 'core', permission: 'push' }] + })).toString('base64') + + stubContext.octokit.rest.repos.getContent = jest.fn().mockImplementation((params) => { + if (params.ref === 'prev-sha') { + return Promise.resolve({ data: { content: previousContent } }) + } + return Promise.resolve({ data: { content: previousContent } }) + }) + + // Mock getSubOrgRepositories to return repos with the property + settings.getSubOrgRepositories = jest.fn().mockResolvedValue([ + { repository_name: 'prop-repo-1' }, + { repository_name: 'prop-repo-2' } + ]) + + // Current subOrgConfigs: only prop-repo-1 still matches + settings.subOrgConfigs = { + 'prop-repo-1': { source: '.github/suborgs/frontend.yml' } + } + + const result = await settings.getReposRemovedFromSubOrgTargeting( + [{ path: '.github/suborgs/frontend.yml', name: 'frontend' }], + 'prev-sha' + ) + + expect(result.repos).toContain('prop-repo-2') + expect(result.repos).not.toContain('prop-repo-1') + }) + + it('deduplicates removed repos across multiple suborg files', async () => { + const previousContent = Buffer.from(yaml.dump({ + suborgrepos: ['repo-a', 'repo-b'] + })).toString('base64') + + stubContext.octokit.rest.repos.getContent = jest.fn().mockResolvedValue({ + data: { content: previousContent } + }) + + // Mock installation repos for glob resolution + stubContext.octokit.paginate = jest.fn().mockResolvedValue([ + { name: 'repo-a', owner: { login: 'test' } }, + { name: 'repo-b', owner: { login: 'test' } } + ]) + + // Neither repo matches current targeting + settings.subOrgConfigs = {} + + const result = await settings.getReposRemovedFromSubOrgTargeting( + [ + { path: '.github/suborgs/frontend.yml', name: 'frontend' }, + { path: '.github/suborgs/frontend.yml', name: 'frontend' } // duplicate + ], + 'prev-sha' + ) + + // Should be deduplicated + const repoACount = result.repos.filter(r => r === 'repo-a').length + expect(repoACount).toBe(1) + }) + + it('handles 404 gracefully when previous file does not exist', async () => { + stubContext.octokit.rest.repos.getContent = jest.fn().mockRejectedValue( + Object.assign(new Error('Not Found'), { status: 404 }) + ) + + settings.subOrgConfigs = {} + + const result = await settings.getReposRemovedFromSubOrgTargeting( + [{ path: '.github/suborgs/new-suborg.yml', name: 'new-suborg' }], + 'prev-sha' + ) + + expect(result).toEqual({ repos: [], previousPluginSections: [] }) + }) + }) + + describe('_buildAppChangesFromDelta', () => { + let settings + const AppOctokitClient = require('../../../lib/appOctokitClient') + const RepoSelector = require('../../../lib/repoSelector') + + beforeEach(() => { + stubConfig = { restrictedRepos: {} } + settings = createSettings(stubConfig) + // Map app slug -> installation id + jest.spyOn(AppOctokitClient.prototype, 'listOrgInstallations').mockResolvedValue([ + { app_slug: 'my-app', id: 42 } + ]) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('skips an app when suborg targeting and app_installations are unchanged', async () => { + // Same targeting resolves to the same repos in both versions + jest.spyOn(RepoSelector.prototype, 'resolve').mockResolvedValue(new Set(['repo-a', 'repo-b'])) + + // Current suborg config: app present + settings.subOrgConfigs = { + frontend: { + suborgrepos: ['repo-a', 'repo-b'], + app_installations: [{ app_slug: 'my-app' }] + } + } + // Previous version (baseRef): identical app_installations + settings.loadYamlFromRef = jest.fn().mockResolvedValue({ + suborgrepos: ['repo-a', 'repo-b'], + app_installations: [{ app_slug: 'my-app' }] + }) + + const result = await settings._buildAppChangesFromDelta( + settings.github, + 'my-enterprise', + [{ repo: 'frontend', path: '.github/suborgs/frontend.yml' }], + [], + 'prev-sha' + ) + + // No churn: nothing to add or remove + expect(result).toEqual([]) + }) + + it('emits only the targeting diff when suborg repos change', async () => { + // previous: repo-a, repo-b ; current: repo-b, repo-c + jest.spyOn(RepoSelector.prototype, 'resolve') + .mockResolvedValueOnce(new Set(['repo-b', 'repo-c'])) // current + .mockResolvedValueOnce(new Set(['repo-a', 'repo-b'])) // previous + + settings.subOrgConfigs = { + frontend: { + suborgrepos: ['repo-b', 'repo-c'], + app_installations: [{ app_slug: 'my-app' }] + } + } + settings.loadYamlFromRef = jest.fn().mockResolvedValue({ + suborgrepos: ['repo-a', 'repo-b'], + app_installations: [{ app_slug: 'my-app' }] + }) + + const result = await settings._buildAppChangesFromDelta( + settings.github, + 'my-enterprise', + [{ repo: 'frontend', path: '.github/suborgs/frontend.yml' }], + [], + 'prev-sha' + ) + + expect(result).toHaveLength(1) + expect(result[0].app_slug).toBe('my-app') + expect(result[0].repository_selection.sort()).toEqual(['repo-c']) + expect(result[0].repository_unselection.sort()).toEqual(['repo-a']) + }) + + it('skips apps configured at the org level (org entry implies all repos)', async () => { + jest.spyOn(RepoSelector.prototype, 'resolve').mockResolvedValue(new Set(['repo-a'])) + + settings.config = { + ...settings.config, + app_installations: [{ app_slug: 'my-app' }] + } + settings.subOrgConfigs = { + frontend: { suborgrepos: ['repo-a'], app_installations: [{ app_slug: 'my-app' }] } + } + settings.loadYamlFromRef = jest.fn().mockResolvedValue({}) + + const result = await settings._buildAppChangesFromDelta( + settings.github, + 'my-enterprise', + [{ repo: 'frontend', path: '.github/suborgs/frontend.yml' }], + [], + 'prev-sha' + ) + + expect(result).toEqual([]) + }) + }) }) // Settings Tests diff --git a/test/unit/lib/settingsGenerator.test.js b/test/unit/lib/settingsGenerator.test.js new file mode 100644 index 000000000..9da7f5280 --- /dev/null +++ b/test/unit/lib/settingsGenerator.test.js @@ -0,0 +1,265 @@ +/* eslint-disable no-undef */ +const SettingsGenerator = require('../../../lib/settingsGenerator') +const { + intersectConfigs, + deepEqual, + pruneEmpty, + stripNoise, + parsePropertyValue, + toYaml +} = SettingsGenerator + +const silentLog = { debug () {}, info () {}, warn () {}, error () {}, trace () {}, child () { return this } } + +function makeGenerator (github = {}) { + return new SettingsGenerator(github, 'my-org', { log: silentLog }) +} + +describe('SettingsGenerator helpers', () => { + describe('deepEqual', () => { + it('compares scalars, arrays and objects', () => { + expect(deepEqual(1, 1)).toBe(true) + expect(deepEqual('a', 'b')).toBe(false) + expect(deepEqual([1, 2], [1, 2])).toBe(true) + expect(deepEqual([1, 2], [2, 1])).toBe(false) + expect(deepEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true) + expect(deepEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false) + }) + }) + + describe('stripNoise', () => { + it('removes API-only keys recursively and drops nulls', () => { + const input = { + id: 5, + node_id: 'abc', + name: 'keep', + created_at: 'x', + nested: { url: 'u', value: 1, gone: null }, + list: [{ id: 1, ok: true }] + } + expect(stripNoise(input)).toEqual({ + name: 'keep', + nested: { value: 1 }, + list: [{ ok: true }] + }) + }) + }) + + describe('pruneEmpty', () => { + it('removes empty arrays, empty objects, null and undefined', () => { + expect(pruneEmpty({ + a: [], + b: {}, + c: null, + d: undefined, + e: [1], + f: { x: 1 }, + g: 'value' + })).toEqual({ e: [1], f: { x: 1 }, g: 'value' }) + }) + }) + + describe('parsePropertyValue', () => { + it('parses name=value', () => { + expect(parsePropertyValue('Team=backend')).toEqual({ name: 'Team', value: 'backend' }) + }) + it('parses name:value', () => { + expect(parsePropertyValue('Team:backend')).toEqual({ name: 'Team', value: 'backend' }) + }) + it('uses propertyName when provided', () => { + expect(parsePropertyValue('backend', 'Team')).toEqual({ name: 'Team', value: 'backend' }) + }) + it('throws when value cannot be parsed', () => { + expect(() => parsePropertyValue('backend')).toThrow(/name=value/) + }) + }) + + describe('toYaml', () => { + it('serializes config to YAML', () => { + const out = toYaml({ repository: { name: 'test' } }) + expect(out).toContain('repository:') + expect(out).toContain('name: test') + }) + }) +}) + +describe('intersectConfigs', () => { + it('returns the single config unchanged when only one provided', () => { + const cfg = { repository: { has_issues: true } } + expect(intersectConfigs([cfg])).toBe(cfg) + }) + + it('keeps only sections present in all configs', () => { + const a = { repository: { has_issues: true }, labels: [{ name: 'bug' }] } + const b = { repository: { has_issues: true } } + expect(intersectConfigs([a, b])).toEqual({ repository: { has_issues: true } }) + }) + + it('keeps only scalar object keys that match across all configs', () => { + const a = { repository: { has_issues: true, has_wiki: true } } + const b = { repository: { has_issues: true, has_wiki: false } } + expect(intersectConfigs([a, b])).toEqual({ repository: { has_issues: true } }) + }) + + it('keeps array items present (by identity + value) in every config', () => { + const a = { labels: [{ name: 'bug', color: 'f00' }, { name: 'wip', color: '0f0' }] } + const b = { labels: [{ name: 'bug', color: 'f00' }, { name: 'done', color: '00f' }] } + expect(intersectConfigs([a, b])).toEqual({ labels: [{ name: 'bug', color: 'f00' }] }) + }) + + it('drops array items whose value differs even if identity matches', () => { + const a = { labels: [{ name: 'bug', color: 'f00' }] } + const b = { labels: [{ name: 'bug', color: '00f' }] } + expect(intersectConfigs([a, b])).toEqual({ labels: [] }) + }) +}) + +describe('SettingsGenerator extractors', () => { + it('repository() selects only configurable fields', async () => { + const github = { + rest: { + repos: { + get: jest.fn().mockResolvedValue({ + data: { + id: 1, + node_id: 'x', + name: 'test', + description: 'desc', + has_issues: true, + stargazers_count: 99, + topics: ['a', 'b'], + default_branch: 'main' + } + }) + } + } + } + const generator = makeGenerator(github) + const result = await generator.repository({ owner: 'my-org', repo: 'test' }) + expect(result).toEqual({ + name: 'test', + description: 'desc', + has_issues: true, + default_branch: 'main', + topics: ['a', 'b'] + }) + }) + + it('labels() sanitizes to name/color/description', async () => { + const generator = makeGenerator() + generator.findExisting = jest.fn().mockResolvedValue([ + { id: 1, node_id: 'n', url: 'u', name: 'bug', color: 'cc0000', description: 'A bug', default: false } + ]) + expect(await generator.labels({ owner: 'my-org', repo: 'r' })).toEqual([ + { name: 'bug', color: 'cc0000', description: 'A bug' } + ]) + }) + + it('teams() maps slug and permission', async () => { + const generator = makeGenerator() + generator.findExisting = jest.fn().mockResolvedValue([ + { id: 1, slug: 'core', name: 'Core Team', permission: 'push' } + ]) + expect(await generator.teams({ owner: 'my-org', repo: 'r' })).toEqual([ + { name: 'core', permission: 'push' } + ]) + }) + + it('rulesets() strips source/source_type and noise', async () => { + const generator = makeGenerator() + generator.findExisting = jest.fn().mockResolvedValue([ + { id: 7, node_id: 'n', source: 'my-org/r', source_type: 'Repository', name: 'main', enforcement: 'active' } + ]) + expect(await generator.rulesets({ owner: 'my-org', repo: 'r' }, 'repo')).toEqual([ + { name: 'main', enforcement: 'active' } + ]) + }) + + it('reformatBranchProtection flattens enabled wrappers', () => { + const generator = makeGenerator() + const out = generator.reformatBranchProtection({ + url: 'noise', + enforce_admins: { enabled: true }, + required_linear_history: { enabled: false }, + required_pull_request_reviews: { required_approving_review_count: 2 } + }) + expect(out).toEqual({ + enforce_admins: true, + required_linear_history: false, + required_pull_request_reviews: { required_approving_review_count: 2 } + }) + }) +}) + +describe('SettingsGenerator.buildSubOrgConfig', () => { + it('prepends suborgproperties and intersects matching repos', async () => { + const generator = makeGenerator() + generator.findReposByProperty = jest.fn().mockResolvedValue(['repo-a', 'repo-b']) + generator.buildRepoConfig = jest.fn() + .mockResolvedValueOnce({ repository: { has_issues: true, has_wiki: true } }) + .mockResolvedValueOnce({ repository: { has_issues: true, has_wiki: false } }) + + const result = await generator.buildSubOrgConfig('Team', 'backend') + expect(result).toEqual({ + suborgproperties: [{ Team: 'backend' }], + repository: { has_issues: true } + }) + }) + + it('returns just the selector when no repos match', async () => { + const generator = makeGenerator() + generator.findReposByProperty = jest.fn().mockResolvedValue([]) + const result = await generator.buildSubOrgConfig('Team', 'backend') + expect(result).toEqual({ suborgproperties: [{ Team: 'backend' }] }) + }) +}) + +describe('SettingsGenerator.generate', () => { + it('resolves repo source to repos/.yml', async () => { + const generator = makeGenerator() + generator.buildRepoConfig = jest.fn().mockResolvedValue({ repository: { name: 'r' } }) + const { filePath, config, yaml } = await generator.generate({ sourceType: 'repo', sourceValue: 'r' }) + expect(filePath).toBe('.github/repos/r.yml') + expect(config).toEqual({ repository: { name: 'r' } }) + expect(yaml).toContain('name: r') + }) + + it('resolves org source to settings.yml', async () => { + const generator = makeGenerator() + generator.buildOrgConfig = jest.fn().mockResolvedValue({ rulesets: [] }) + const { filePath } = await generator.generate({ sourceType: 'org', sourceValue: 'my-org' }) + expect(filePath).toBe('.github/settings.yml') + }) + + it('resolves custom-property source to suborgs/_.yml', async () => { + const generator = makeGenerator() + generator.buildSubOrgConfig = jest.fn().mockResolvedValue({ suborgproperties: [{ Team: 'backend' }] }) + const { filePath } = await generator.generate({ sourceType: 'custom-property', sourceValue: 'Team=backend' }) + expect(filePath).toBe('.github/suborgs/Team_backend.yml') + }) + + it('throws on unsupported source type', async () => { + const generator = makeGenerator() + await expect(generator.generate({ sourceType: 'bogus', sourceValue: 'x' })).rejects.toThrow(/Unsupported source type/) + }) +}) + +describe('SettingsGenerator.findReposByProperty', () => { + it('queries the org properties values API and returns repo names', async () => { + const paginate = jest.fn().mockResolvedValue([ + { repository_name: 'repo-a' }, + { repository_name: 'repo-b' }, + { repository_name: null } + ]) + const github = { + request: { endpoint: jest.fn().mockReturnValue({ url: 'endpoint' }) }, + paginate + } + const generator = makeGenerator(github) + const repos = await generator.findReposByProperty('Team', 'backend') + expect(github.request.endpoint).toHaveBeenCalledWith( + expect.stringContaining('/orgs/my-org/properties/values?repository_query=') + ) + expect(repos).toEqual(['repo-a', 'repo-b']) + }) +})
MsgPluginRepoAdditionsDeletionsModifications
❗ ${y.action.msg} ${y.plugin} ${prettify(y.repo)} ${prettify(y.action.additions)} ${prettify(y.action.deletions)} ${prettify(y.action.modifications)}
${y.plugin} ${prettify(y.repo)} ${prettify(y.action.additions)} ${prettify(y.action.deletions)} ${prettify(y.action.modifications)}