diff --git a/.c8rc.json b/.c8rc.json deleted file mode 100644 index 0c61fde4..00000000 --- a/.c8rc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "include": ["src/*.js"], - "exclude": ["**/*.spec.js"], - "reporter": ["lcov", "text"], - "all": false, - "cache": true -} diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 00000000..b85b0010 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,5 @@ +# Changesets + +This directory contains [**Changesets**](https://github.com/changesets/changesets) which are markdown files that describe package changes for the next release. + +For guidance on when and how to add changesets, checkout the [Maintainer's Guide](../.github/maintainers_guide.md#changesets). diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 00000000..ad6f18a1 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "restricted", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..0ed969f8 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,12 @@ +# Salesforce Open Source project configuration +# Learn more: https://github.com/salesforce/oss-template +#ECCN:Open Source +#GUSINFO:Open Source,Open Source Workflow + +# @slackapi/slack-github-action +# are code reviewers for all changes in this repo. +* @slackapi/slack-github-action + +# @slackapi/developer-education +# are code reviewers for changes in the `/docs` directory. +/docs/ @slackapi/developer-education diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8d31c61e..6188e456 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,11 +1,15 @@ version: 2 updates: - - package-ecosystem: 'npm' - directory: '/' + - package-ecosystem: "npm" + directory: "/" schedule: - interval: 'monthly' - - - package-ecosystem: 'github-actions' - directory: '/' + interval: "monthly" + ignore: + - dependency-name: "@types/node" + versions: + - "25.x" + - "26.x" + - package-ecosystem: "github-actions" + directory: "/" schedule: - interval: 'monthly' + interval: "monthly" diff --git a/.github/maintainers_guide.md b/.github/maintainers_guide.md index 2ab71aab..7e6ef8e8 100644 --- a/.github/maintainers_guide.md +++ b/.github/maintainers_guide.md @@ -18,31 +18,88 @@ Information on setting up and configuring mocked events can be found in [`.githu ### Testing -When testing locally, ensure at least linting and unit tests pass by running `npm test`. -Additionally, sending a PR is highly recommended with every change as there are several GitHub -Actions jobs that execute what are effectively integration tests for this GitHub Action. - -#### Checks on PRs - -Actions that run the integration tests on PRs from a fork will require approval before running. -These checks use stored secrets so the changes should be reviewed before approving the workflow to -avoid accidently leaking tokens! - -### Releasing - -* Check the status of this project's GitHub Milestone to be released for issues that should be shipped with the release. - - If all issues have been closed, continue with the release. - - If issues are still open, discuss with the team about whether the open issues should be moved to a future release or if the release should be held off until the issues are resolved. - - Take a look at all issues under the Milestone to make sure that the type of issues included aligns with the Milestone name based on [semantic versioning](https://semver.org/). If the issues do not align with the naming of the Milestone (ex: if the issues are all bug fixes, but the Milestone is labeled as a minor release), then you can tweak the Milestone name to reflect the correct versioning. -* Update the version in `package.json`. -* Update all references to versions in the README and in the workflow files under `example-workflows/` to refer to the latest release. -* Run all tests using `npm test` to make sure the tests pass. -* Commit the changes on your `main` branch. -* Create a git tag for the new version. Should be in the format `v1.4.0`. `git tag v1.4.0`. -* Push changes up to GitHub `git push origin main --tags`. -* Create a GitHub Release based on the tag you just pushed up - this will trigger the publishing - GitHub workflow. -* Once released, make sure to close the relevant GitHub Milestone for the version you released. +Expected behaviors are confirmed with both unit tests and integration tests. Our unit tests run fast without secrets, while integration tests use webhooks and tokens for sending data to Slack across various techniques. + +#### Unit tests + +Run the following scripts to confirm tests pass before opening a PR: + +```sh +$ npm test # Unit tests +$ npm run lint # Lint and format +$ npm run check # Typecheck +``` + +The `test.yml` workflow runs these scripts for pull requests and changes to the `main` branch. + +#### Integration tests + +The `integration.yml` workflow uses this action in interactions with Slack using secrets saved to the `staging` environment. + +A PR from a forked branch will fail this workflow until a maintainer reviews the code and [dispatches](https://github.com/slackapi/slack-github-action/actions/workflows/integration.yml) a test run that points to the most recent commit using the following format: + +``` +pull//head +``` + +### Documentation + +This repo contains two types of docs files: + +- markdown files +- sidebar.json + +The private repo containing the docs.slack.dev site pulls these in at build time. + +Maintainers need to use the `run workflow` button associated with the `deploy` workflow in that private repo to update the docs with changes from here. + +#### Markdown Files + +The markdown files here are secretly mdx files in disguise. + +If you'd like to add images to pages, add the image files to the same folder the md file is in. + +We appreciate markdown edits from anyone!!! + +#### Sidebar + +`_sidebar.json` sets the slack github action docs sidebar + +sidebar values take the form of "slack-github-action/path-within-docs/" + +or, in other words - full path but remove "docs": +path: slack-github-action/docs/sending-variables.md +value: slack-github-action/sending-variables + +for info on syntax see https://docusaurus.io/docs/sidebar + +this file is copied into slackapi.github.io/slack-github-action/sidebar.js it is then called in slackapi.github.io/sidebars.js + +### Changesets + +This project uses [Changesets](https://github.com/changesets/changesets) to track changes and automate releases. + +Each changeset describes a change to the package and its [semver](https://semver.org/) impact, and a new changeset should be added when updating the package with some change that affects consumers: + +```sh +npm run changeset +``` + +Updates to documentation, tests, or CI might not require new entries. + +When a PR containing changesets is merged to `main`, a different PR is opened or updated using [changesets/action](https://github.com/changesets/action) which consumes the pending changesets, bumps the package version, and updates the `CHANGELOG` in preparation to release. + +### Releases + +New versions are published when the release PR created from changesets is merged and the publish workflow is approved. Follow these steps to build confidence: + +1. **Check GitHub Milestones**: Before merging the release PR please check the relevant [Milestones](https://github.com/slackapi/slack-github-action/milestones). If issues or pull requests are still open either decide to postpone the release or save those changes for a future update. + +2. **Review the release PR**: Verify that the version bump matches expectations, `CHANGELOG` entries are clear, and CI checks pass. + +3. **Merge and approve**: Merge the release PR, then approve the publish workflow to release the action. Update the [release](https://github.com/slackapi/slack-github-action/releases) to share to the [marketplace](https://github.com/marketplace/actions/the-slack-github-action). + +4. **Update Milestones**: Close the relevant [Milestones](https://github.com/slackapi/slack-github-action/milestones) and rename these to match the released version. Open a new Milestone for the next version. ## Workflow @@ -60,6 +117,8 @@ As a maintainer, the development you do will be almost entirely off of your fork `main` is where active development occurs. +`release/vX.Y.Z` has the packaged distribution of a particular version based from the changes on `main`. This is created using a workflow when new releases are published. + When developing, branches should be created off of your fork and not directly off of this repository. If working on a long-running feature and in collaboration with others, a corresponding branch of the same name is permitted. This makes collaboration on a single branch possible, as contributors working on the same feature cannot push commits to others' open Pull Requests. After a major version increment, there also may be maintenance branches created specifically for supporting older major versions. @@ -68,18 +127,17 @@ After a major version increment, there also may be maintenance branches created Labels are used to run issues through an organized workflow. Here are the basic definitions: -* `bug`: A confirmed bug report. A bug is considered confirmed when reproduction steps have been - documented and the issue has been reproduced. -* `enhancement`: A feature request for something this package might not already do. -* `docs`: An issue that is purely about documentation work. -* `tests`: An issue that is purely about testing work. -* `needs feedback`: An issue that may have claimed to be a bug but was not reproducible, or was otherwise missing some information. -* `discussion`: An issue that is purely meant to hold a discussion. Typically the maintainers are looking for feedback in this issues. -* `question`: An issue that is like a support request because the user's usage was not correct. -* `semver:major|minor|patch`: Metadata about how resolving this issue would affect the version number. -* `security`: An issue that has special consideration for security reasons. -* `good first contribution`: An issue that has a well-defined relatively-small scope, with clear expectations. It helps when the testing approach is also known. -* `duplicate`: An issue that is functionally the same as another issue. Apply this only if you've linked the other issue by number. +- `bug`: A confirmed bug report. A bug is considered confirmed when reproduction steps have been documented and the issue has been reproduced. +- `enhancement`: A feature request for something this package might not already do. +- `docs`: An issue that is purely about documentation work. +- `tests`: An issue that is purely about testing work. +- `needs feedback`: An issue that may have claimed to be a bug but was not reproducible, or was otherwise missing some information. +- `discussion`: An issue that is purely meant to hold a discussion. Typically the maintainers are looking for feedback in this issues. +- `question`: An issue that is like a support request because the user's usage was not correct. +- `semver:major|minor|patch`: Metadata about how resolving this issue would affect the version number. +- `security`: An issue that has special consideration for security reasons. +- `good first contribution`: An issue that has a well-defined relatively-small scope, with clear expectations. It helps when the testing approach is also known. +- `duplicate`: An issue that is functionally the same as another issue. Apply this only if you've linked the other issue by number. **Triage** is the process of taking new issues that aren't yet "seen" and marking them with a basic level of information with labels. An issue should have **one** of the following labels applied: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7945c926..f75c08d3 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,7 +2,7 @@ Describe the goal of this PR. Mention any related Issue numbers. -### Requirements (place an `x` in each `[ ]`) +### Requirements -* [ ] I've read and understood the [Contributing Guidelines](https://github.com/slackapi/slack-github-action/blob/main/.github/contributing.md) and have done my best effort to follow them. -* [ ] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct). \ No newline at end of file +- [ ] I've read and understood the [Contributing Guidelines](https://github.com/slackapi/slack-github-action/blob/main/.github/contributing.md) and have done my best effort to follow them. +- [ ] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct). diff --git a/.github/resources/.env.example b/.github/resources/.env.example index c638f894..f423d514 100644 --- a/.github/resources/.env.example +++ b/.github/resources/.env.example @@ -4,4 +4,5 @@ export SLACK_BOT_TOKEN=xoxb-01010101-example export SLACK_CHANNEL_ID=C0123456789 export SLACK_INCOMING_WEBHOOK=https://hooks.slack.com/services/T0123456789/B0123456789/abcdefghijklmnopqrstuvwxyz +export SLACK_SERVICE_TOKEN=xoxp-01010101-example export SLACK_WEBHOOK_TRIGGER=https://hooks.slack.com/triggers/T0123456789/00000000000/abcdefghijklmnopqrstuvwxyz diff --git a/.github/resources/.slack/config.json b/.github/resources/.slack/config.json index 6de199bc..2aee8db3 100644 --- a/.github/resources/.slack/config.json +++ b/.github/resources/.slack/config.json @@ -1,5 +1,7 @@ { - "experiments": ["bolt"], + "manifest": { + "source": "local" + }, "project_id": "c4805b41-d1ce-4ea0-b297-ed2f8c64c267" } diff --git a/.github/resources/slack.json b/.github/resources/.slack/hooks.json similarity index 78% rename from .github/resources/slack.json rename to .github/resources/.slack/hooks.json index 89e6f776..512438d5 100644 --- a/.github/resources/slack.json +++ b/.github/resources/.slack/hooks.json @@ -1,7 +1,7 @@ { "runtime": "actions", "hooks": { - "deploy": "open https://api.slack.com/apps || true", + "deploy": "echo https://api.slack.com/apps", "get-manifest": "cat ./.slack/manifest.json #" }, "config": { diff --git a/.github/resources/README.md b/.github/resources/README.md index 4b5dfeb2..dbdc7471 100644 --- a/.github/resources/README.md +++ b/.github/resources/README.md @@ -6,7 +6,7 @@ experiments with the [Slack CLI][cli]. ## Overview -This app showcases all three techniques of sending data into Slack and follows +This app showcases all four techniques of interacting with Slack and follows patterns found in the integration tests. - **Technique 1** Slack Workflow Builder: Use a Slack webhook trigger to start a @@ -15,6 +15,8 @@ patterns found in the integration tests. data provided through the GitHub workflow. - **Technique 3** Incoming webhook: Post a message to a Slack channel using an incoming webhook. +- **Technique 4** Slack CLI Command: Install and run Slack CLI commands such as + `deploy` or `manifest` using a service token. Configurations for the Slack app and workflow, and the GitHub Actions workflow are found in the following files: @@ -22,6 +24,9 @@ are found in the following files: - Slack app setup: [`.github/resources/.slack/manifest.json`][slacktion] - GitHub Actions steps: [`.github/workflows/develop.yml`][develop] +> **Note:** During CLI integration tests, `.github/resources/.slack` is moved to +> `.slack` at the project root so the Slack CLI can discover the app manifest. + Either the techniques or app setup and workflow steps can be adjusted during testing and development. For experimenting with new changes, we recommend using the [steps for development](#experimenting-for-development) while the @@ -76,6 +81,8 @@ tested. Required values include: https://hooks.slack.com/services/T0123456789/B0123456789/abcdefghijklmnopqrstuvwxyz - `SLACK_WEBHOOK_TRIGGER`: https://hooks.slack.com/triggers/T0123456789/00000000000/abcdefghijklmnopqrstuvwxyz +- `SLACK_SERVICE_TOKEN`: xoxp-service-token-example (secret — for CLI commands) +- `SLACK_APP_ID`: A0123456789 (variable — used with `--app` flag in CLI deploy) ### Experimenting for development @@ -98,6 +105,7 @@ export SLACK_BOT_TOKEN=xoxb-01010101-example export SLACK_CHANNEL_ID=C0123456789 export SLACK_INCOMING_WEBHOOK=https://hooks.slack.com/services/T0123456789/B0123456789/abcdefghijklmnopqrstuvwxyz export SLACK_WEBHOOK_TRIGGER=https://hooks.slack.com/triggers/T0123456789/00000000000/abcdefghijklmnopqrstuvwxyz +export SLACK_SERVICE_TOKEN=xoxp-service-token-example ``` Environment variables and credentials should be set in the created `.env` file @@ -134,8 +142,8 @@ of the `.github/resources/.actions/event.json` file. Reference: https://docs.github.com/en/webhooks/webhook-events-and-payloads -[cli]: https://api.slack.com/automation/cli/commands +[cli]: https://docs.slack.dev/tools/slack-cli/ [develop]: ../workflows/develop.yml -[manifest]: https://api.slack.com/concepts/manifests +[manifest]: https://docs.slack.dev/app-manifests/ [secrets]: https://github.com/slackapi/slack-github-action/settings/secrets/actions [slacktion]: ./.slack/manifest.json diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml new file mode 100644 index 00000000..aafc2c0a --- /dev/null +++ b/.github/workflows/dependencies.yml @@ -0,0 +1,29 @@ +name: Merge updates to dependencies +on: + pull_request: +jobs: + dependabot: + name: "@dependabot" + if: github.event.pull_request.user.login == 'dependabot[bot]' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Collect metadata + id: metadata + uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + - name: Approve + if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor' + run: gh pr review --approve "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GH_TOKEN: ${{secrets.GITHUB_TOKEN}} + - name: Automerge + if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor' + run: gh pr merge --auto --squash "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 97dda648..5d25dd40 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -8,13 +8,15 @@ on: jobs: run: runs-on: ubuntu-latest + permissions: + contents: read steps: - name: Checkout action - uses: actions/checkout@v4 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Setup the Node runtime for this project - uses: actions/setup-node@v4 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: npm cache-dependency-path: package-lock.json diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 00000000..1e5227db --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,340 @@ +name: Integration tests +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + inputs: + ref: + description: "The branch, tag, or SHA to checkout" + required: true + +jobs: + integration: + name: Run API tests + runs-on: ubuntu-latest + environment: staging + permissions: + contents: read + steps: + - name: "check: require maintainer approval" + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository + run: | + echo "::error::Integration tests from forked branches require maintainer approval." + echo "::notice::Dispatch a test run at ${{ github.server_url }}/${{ github.repository }}/actions/workflows/integration.yml with ref 'pull/${{ github.event.pull_request.number }}/head'" + exit 1 + + - name: "build: checkout the latest changes" + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + ref: ${{ inputs.ref || github.event.pull_request.head.sha || github.sha }} + + - name: "build: setup the node runtime" + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + cache: npm + cache-dependency-path: package-lock.json + node-version-file: .nvmrc + + - name: "build: install the required dependencies" + run: npm ci + + - name: "build: package code for distribution" + run: npm run build + + - name: "pretest(inputs): save the push event trigger commit URL" + if: github.event_name == 'push' + id: push + run: | + echo "url=$URL" >> "$GITHUB_OUTPUT" + env: + URL: ${{ github.event.head_commit.url }} + + - name: "pretest(inputs): save the pull request event trigger URL" + if: github.event_name == 'pull_request' + id: pull_request + run: | + echo "url=$URL" >> "$GITHUB_OUTPUT" + env: + URL: ${{ github.event.pull_request.html_url }} + + - name: "pretest(inputs): save the workflow dispatch event trigger commit URL" + if: github.event_name == 'workflow_dispatch' + id: workflow_dispatch + run: | + echo "url=https://github.com/${GITHUB_REPOSITORY}/commit/${INPUT_REF}" >> "$GITHUB_OUTPUT" + env: + INPUT_REF: ${{ inputs.ref }} + + - name: "test(wfb): send a payload to workflow builder via webhook trigger" + id: wfb + uses: ./ + with: + errors: true + webhook: ${{ secrets.SLACK_WEBHOOK_TRIGGER }} + webhook-type: webhook-trigger + payload: | + author: ${{ github.event.sender.login }} + channel_id: ${{ secrets.SLACK_CHANNEL_ID }} + event_url: ${{ steps.push.outputs.url || steps.pull_request.outputs.url || steps.workflow_dispatch.outputs.url }} + repo_name: ${{ github.event.repository.full_name }} + status: ${{ job.status }} + + - name: "test(wfb): confirm a payload was sent" + run: test -n "$WFB_OUTPUT_TIME" + env: + WFB_OUTPUT_TIME: ${{ steps.wfb.outputs.time }} + + - name: "test(api): post a message to channel" + id: message + uses: ./ + with: + errors: true + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + channel: ${{ secrets.SLACK_CHANNEL_ID }} + text: ":checkered_flag: Action happens at " + + - name: "test(api): confirm a message was posted" + run: test -n "$MESSAGE_OUTPUT_TS" + env: + MESSAGE_OUTPUT_TS: ${{ steps.message.outputs.ts }} + + - name: "test(api): post a multiline message" # https://github.com/slackapi/slack-github-action/issues/637 + id: multiline + uses: ./ + with: + errors: true + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + channel: ${{ secrets.SLACK_CHANNEL_ID }} + text: ":checkered_flag: A multiline message wraps across lines + + with a blank line before " + + - name: "test(api): confirm the multiline message was posted" + run: test -n "$MULTILINE_OUTPUT_TS" + env: + MULTILINE_OUTPUT_TS: ${{ steps.multiline.outputs.ts }} + + - name: "test(api): post a message with blocks" + id: blocks + uses: ./ + with: + errors: true + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + channel: ${{ secrets.SLACK_CHANNEL_ID }} + text: ":eyes: Event received..." + attachments: + - color: "dbab09" + fields: + - title: "Status" + short: true + value: "Processing" + + - name: "test(api): confirm the blocks were posted" + run: test -n "$BLOCKS_OUTPUT_TS" + env: + BLOCKS_OUTPUT_TS: ${{ steps.blocks.outputs.ts }} + + - name: "test(api): post a threaded message" + id: timer + uses: ./ + with: + errors: true + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + channel: ${{ secrets.SLACK_CHANNEL_ID }} + text: "Started at `${{ steps.blocks.outputs.time }}`" + thread_ts: "${{ steps.blocks.outputs.ts }}" + + - name: "test(api): confirm the thread started" + run: test -n "$TIMER_OUTPUT_TIME" + env: + TIMER_OUTPUT_TIME: ${{ steps.timer.outputs.time }} + + - name: "test(api): wait to mock event processing" + run: sleep 3 + + - name: "test(api): update the original message" + id: finished + uses: ./ + with: + errors: true + method: chat.update + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + channel: ${{ secrets.SLACK_CHANNEL_ID }} + ts: "${{ steps.blocks.outputs.ts }}" + text: ":gear: Event processed!" + attachments: + - color: "28a745" + fields: + - title: "Status" + short: true + value: "Completed" + + - name: "test(api): post another threaded message" + id: done + uses: ./ + with: + errors: true + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + channel: ${{ steps.blocks.outputs.channel_id }} + text: "Finished at `${{ steps.finished.outputs.time }}`" + thread_ts: "${{ steps.timer.outputs.thread_ts }}" + + - name: "test(api): post a file into a channel" + id: file + uses: ./ + with: + errors: true + method: files.uploadV2 + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + channel_id: ${{ secrets.SLACK_CHANNEL_ID }} + initial_comment: ":robot_face: The codes exists here" + file: .github/workflows/integration.yml + filename: integration.yml + + - name: "test(api): react to the completed update message" + uses: ./ + with: + errors: true + method: reactions.add + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + channel: ${{ secrets.SLACK_CHANNEL_ID }} + timestamp: ${{ steps.blocks.outputs.ts }} + name: "tada" + + - name: "test(api): confirm the thread ended" + run: test -n "$DONE_OUTPUT_TIME" + env: + DONE_OUTPUT_TIME: ${{ steps.done.outputs.time }} + + - name: "test(incoming): post a message via incoming webhook" + id: incoming + uses: ./ + with: + errors: true + webhook: ${{ secrets.SLACK_INCOMING_WEBHOOK }} + webhook-type: incoming-webhook + payload: | + text: "Incoming webhook test for the Slack GitHub Action" + blocks: + - type: section + text: + type: plain_text + text: ":link: A message was received via incoming webhook" + emoji: true + + - name: "test(incoming): confirm a webhook was posted" + run: test -n "$INCOMING_WEBHOOK_OUTPUT_TIME" + env: + INCOMING_WEBHOOK_OUTPUT_TIME: ${{ steps.incoming.outputs.time }} + + - name: "test(incoming): reveal contents of the github payload" + run: echo "$JSON" + env: + JSON: ${{ toJSON(github) }} + + - name: "test(incoming): post a message via payload file" + id: payload_file + uses: ./ + with: + errors: true + payload-file-path: ./.github/resources/.slack/incoming-webhook.json + payload-templated: true + webhook: ${{ secrets.SLACK_INCOMING_WEBHOOK }} + webhook-type: incoming-webhook + env: + JOB_STATUS: ${{ job.status }} + ATTACHMENT_COLOR: ${{ (job.status == 'success' && 'good') || (job.status == 'failure' && 'danger') || 'warning' }} + + - name: "test(incoming): confirm a payload file was posted" + run: test -n "$PAYLOAD_FILE_OUTPUT_TIME" + env: + PAYLOAD_FILE_OUTPUT_TIME: ${{ steps.payload_file.outputs.time }} + + cli: + name: Run CLI tests + runs-on: ${{ matrix.os }} + environment: staging + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest + permissions: + contents: read + steps: + - name: "build: checkout the latest changes" + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + ref: ${{ inputs.ref || github.event.pull_request.head.sha || github.sha }} + + - name: "test(cli): run a slack cli version check" + id: version + uses: ./cli + with: + command: "version" + + - name: "test(cli): confirm the version check outputs" + shell: bash + run: | + set -ex + [ "$CLI_OK" = "true" ] + echo "$CLI_TIME" | grep -qE '^[0-9]+$' + [ -n "$CLI_RESPONSE" ] + env: + CLI_OK: ${{ steps.version.outputs.ok }} + CLI_RESPONSE: ${{ steps.version.outputs.response }} + CLI_TIME: ${{ steps.version.outputs.time }} + + - name: "test(cli): run an unknown command" + id: unknown + continue-on-error: true + uses: ./cli + with: + command: "off" + + - name: "test(cli): confirm the unknown command outputs" + shell: bash + run: | + set -ex + [ "$CLI_OK" = "false" ] + echo "$CLI_TIME" | grep -qE '^[0-9]+$' + [ -n "$CLI_RESPONSE" ] + env: + CLI_OK: ${{ steps.unknown.outputs.ok }} + CLI_RESPONSE: ${{ steps.unknown.outputs.response }} + CLI_TIME: ${{ steps.unknown.outputs.time }} + + - name: "chore: configure the actioneering application" + shell: bash + run: mv .github/resources/.slack .slack + + - name: "test(cli): validate the app manifest" + uses: ./cli + with: + command: "manifest" + token: ${{ secrets.SLACK_SERVICE_TOKEN }} + + - name: "test(cli): deploy the app" + uses: ./cli + with: + command: "deploy --app ${{ vars.SLACK_APP_ID }} --hide-triggers --force" + token: ${{ secrets.SLACK_SERVICE_TOKEN }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index f3d012ca..00000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Publish - -on: - release: - types: [published, edited] - -jobs: - build: - name: Build and tag a new version - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout the current code - uses: actions/checkout@v4 - with: - persist-credentials: false - ref: ${{ github.event.release.tag_name }} - - - name: Configure the runtime node - uses: actions/setup-node@v4 - with: - cache: npm - cache-dependency-path: package-lock.json - node-version-file: .nvmrc - - - name: Install project dependencies - run: npm ci - - - name: Build a production release - run: npm run build - - - name: Distribute the latest tagged release - uses: teunmooij/github-versioned-release@v1.2.1 - with: - template: javascript-action - env: - GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..76ce9a99 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,163 @@ +name: Release + +on: + push: + branches: + - main + workflow_dispatch: + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +jobs: + changelog: + name: Changelog + runs-on: ubuntu-latest + outputs: + hasChangesets: ${{ steps.changesets.outputs.hasChangesets }} + releaseExists: ${{ steps.published.outputs.exists }} + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout repo + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: true + + - name: Setup Node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + cache: npm + cache-dependency-path: package-lock.json + node-version-file: .nvmrc + + - name: Install dependencies + run: npm ci --verbose + + - name: Update release PR + id: changesets + uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0 + with: + commit: "chore: release" + prDraft: create + title: "chore: release" + version: npm run version + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Check if publish is needed + id: published + if: steps.changesets.outputs.hasChangesets == 'false' + run: | + VERSION=$(node -p "require('./package.json').version") + if gh release view "v${VERSION}" &>/dev/null; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish: + name: Publish + needs: changelog + if: needs.changelog.outputs.hasChangesets == 'false' && needs.changelog.outputs.releaseExists == 'false' + runs-on: ubuntu-latest + environment: publish + permissions: + contents: write + steps: + - name: Gather credentials + id: credentials + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.GH_APP_CLIENT_ID }} + private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: ${{ github.event.repository.name }} + permission-contents: write + + - name: Checkout repo + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: true + token: ${{ steps.credentials.outputs.token }} + + - name: Setup Node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + cache: npm + cache-dependency-path: package-lock.json + node-version-file: .nvmrc + + - name: Install dependencies + run: npm ci --verbose + + - name: Build a production release + run: npm run build + + - name: Gather version + id: version + run: | + VERSION="v$(node -p 'require("./package.json").version')" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "major=$(echo "$VERSION" | cut -d. -f1)" >> "$GITHUB_OUTPUT" + echo "minor=$(echo "$VERSION" | cut -d. -f1-2)" >> "$GITHUB_OUTPUT" + echo "$VERSION" + + - name: Check if release exists + id: check + run: | + if gh release view "$VERSION" &>/dev/null; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + env: + VERSION: ${{ steps.version.outputs.version }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create release artifacts + if: steps.check.outputs.exists == 'false' + id: release + run: | + awk '/^## /{if(n++)exit}n' CHANGELOG.md | tail -n +3 > RELEASE.md + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -b "release/$VERSION" + git rm -rf . + git checkout HEAD -- LICENSE + git checkout HEAD -- README.md + git checkout HEAD -- action.yml + git checkout HEAD -- cli/ + git add --force dist/ + git commit -m "chore: release" + git push origin "release/$VERSION" + SHA=$(git rev-parse HEAD) + git tag -f "$MAJOR" "$SHA" + git tag -f "$MINOR" "$SHA" + git push -f origin "$MAJOR" "$MINOR" + echo "sha=$SHA" >> "$GITHUB_OUTPUT" + env: + VERSION: ${{ steps.version.outputs.version }} + MAJOR: ${{ steps.version.outputs.major }} + MINOR: ${{ steps.version.outputs.minor }} + + - name: Create GitHub release + if: steps.check.outputs.exists == 'false' + run: gh release create "$VERSION" --target "$SHA" --title "Slack GitHub Action $VERSION" --notes-file RELEASE.md + env: + VERSION: ${{ steps.version.outputs.version }} + SHA: ${{ steps.release.outputs.sha }} + GH_TOKEN: ${{ steps.credentials.outputs.token }} + + - name: Notify Slack + if: steps.check.outputs.exists == 'false' + uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5 + with: + webhook: ${{ secrets.SLACK_RELEASE_ANNOUNCEMENTS_WEBHOOK_URL }} + webhook-type: webhook-trigger + payload: | + action_url: "https://github.com/${{ github.repository }}/releases/tag/${{ steps.version.outputs.version }}" + message: "Slack GitHub Action ${{ steps.version.outputs.version }}" + repository: "${{ github.repository }}" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 29f7ce43..c36406bb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,6 @@ name: Tests on: - pull_request_target: + pull_request: push: branches: - main @@ -8,20 +8,19 @@ on: jobs: tests: - name: Run tests + name: Run unit tests runs-on: ubuntu-latest - environment: staging permissions: checks: write + contents: read steps: - name: "build: checkout the latest changes" - uses: actions/checkout@v4 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - ref: ${{ github.event.pull_request.head.sha }} - name: "build: setup the node runtime" - uses: actions/setup-node@v4 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: npm cache-dependency-path: package-lock.json @@ -40,194 +39,16 @@ jobs: run: npm test - name: "unit(test): upload coverage to CodeCov" - uses: codecov/codecov-action@v5.4.0 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: - directory: ./coverage + files: test/coverage.txt token: ${{ secrets.CODECOV_TOKEN }} - name: "build: package code for distribution" run: npm run build - - name: "pretest(inputs): save the push event trigger commit URL" - if: "contains(github.event_name, 'push')" - run: | - url=${{ github.event.head_commit.url }} - echo "EVENT_URL=$url" >> "$GITHUB_ENV" - - - name: "pretest(inputs): save the pull request event trigger commit URL" - if: "contains(github.event_name, 'pull_request')" - run: | - url=${{ github.event.pull_request.html_url }} - echo "EVENT_URL=$url" >> "$GITHUB_ENV" - - - name: "integration(wfb): send a payload to workflow builder via webhook trigger" - id: wfb - uses: ./ - with: - errors: true - webhook: ${{ secrets.SLACK_WEBHOOK_TRIGGER }} - webhook-type: webhook-trigger - payload: | - author: ${{ github.event.sender.login }} - channel_id: ${{ secrets.SLACK_CHANNEL_ID }} - event_url: ${{ env.EVENT_URL}} - repo_name: ${{ github.event.repository.full_name }} - status: ${{ job.status }} - - - name: "integration(wfb): confirm a payload was sent" - run: test -n "${{ steps.wfb.outputs.time }}" - - - name: "integration(botToken): post a message to channel" - id: message - uses: ./ - with: - errors: true - method: chat.postMessage - token: ${{ secrets.SLACK_BOT_TOKEN }} - payload: | - channel: ${{ secrets.SLACK_CHANNEL_ID }} - text: ":checkered_flag: Action happens at " - - - name: "integration(method): confirm a message was posted" - run: test -n "${{ steps.message.outputs.ts }}" - - - name: "integration(method): post a message with blocks" - id: blocks - uses: ./ - with: - errors: true - method: chat.postMessage - token: ${{ secrets.SLACK_BOT_TOKEN }} - payload: | - channel: ${{ secrets.SLACK_CHANNEL_ID }} - text: ":eyes: Event received..." - attachments: - - color: "dbab09" - fields: - - title: "Status" - short: true - value: "Processing" - - - name: "integration(method): confirm the blocks were posted" - run: test -n "${{ steps.blocks.outputs.ts }}" - - - name: "integration(method): post a threaded message" - id: timer - uses: ./ - with: - errors: true - method: chat.postMessage - token: ${{ secrets.SLACK_BOT_TOKEN }} - payload: | - channel: ${{ secrets.SLACK_CHANNEL_ID }} - text: "Started at `${{ steps.blocks.outputs.time }}`" - thread_ts: "${{ steps.blocks.outputs.ts }}" - - - name: "integration(incoming): confirm the thread started" - run: test -n "${{ steps.timer.outputs.time }}" - - - name: "integration(method): wait to mock event processing" - run: sleep 3 - - - name: "integration(method): update the original message" - id: finished - uses: ./ - with: - errors: true - method: chat.update - token: ${{ secrets.SLACK_BOT_TOKEN }} - payload: | - channel: ${{ secrets.SLACK_CHANNEL_ID }} - ts: "${{ steps.blocks.outputs.ts }}" - text: ":gear: Event processed!" - attachments: - - color: "28a745" - fields: - - title: "Status" - short: true - value: "Completed" - - - name: "integration(method): post another threaded message" - id: done - uses: ./ - with: - errors: true - method: chat.postMessage - token: ${{ secrets.SLACK_BOT_TOKEN }} - payload: | - channel: ${{ steps.blocks.outputs.channel_id }} - text: "Finished at `${{ steps.finished.outputs.time }}`" - thread_ts: "${{ steps.timer.outputs.thread_ts }}" - - - name: "integration(method): post a file into a channel" - id: file - uses: ./ - with: - errors: true - method: files.uploadV2 - token: ${{ secrets.SLACK_BOT_TOKEN }} - payload: | - channel_id: ${{ secrets.SLACK_CHANNEL_ID }} - initial_comment: ":robot_face: The codes exists here" - file: .github/workflows/test.yml - filename: action.yml - - - name: "integration(method): react to the completed update message" - uses: ./ - with: - errors: true - method: reactions.add - token: ${{ secrets.SLACK_BOT_TOKEN }} - payload: | - channel: ${{ secrets.SLACK_CHANNEL_ID }} - timestamp: ${{ steps.blocks.outputs.ts }} - name: "tada" - - - name: "integration(method): confirm the thread ended" - run: test -n "${{ steps.done.outputs.time }}" - - - name: "integration(incoming): post a message via incoming webhook" - id: incoming - uses: ./ - with: - errors: true - webhook: ${{ secrets.SLACK_INCOMING_WEBHOOK }} - webhook-type: incoming-webhook - payload: | - text: "Incoming webhook test for slack send" - blocks: - - type: section - text: - type: plain_text - text: ":link: A message was received via incoming webhook" - emoji: true - - - name: "integration(incoming): confirm a webhook was posted" - run: test -n "${{ steps.incoming.outputs.time }}" - - - name: "integration(incoming): reveal contents of the github payload" - run: echo $JSON - env: - JSON: ${{ toJSON(github) }} - - - name: "integration(incoming): post a message via payload file" - id: payload_file - uses: ./ - with: - errors: true - payload-file-path: ./.github/resources/.slack/incoming-webhook.json - payload-templated: true - webhook: ${{ secrets.SLACK_INCOMING_WEBHOOK }} - webhook-type: incoming-webhook - env: - JOB_STATUS: ${{ job.status }} - ATTACHMENT_COLOR: ${{ (job.status == 'success' && 'good') || (job.status == 'failure' && 'danger') || 'warning' }} - - - name: "integration(incoming): confirm a payload file was posted" - run: test -n "${{ steps.payload_file.outputs.time }}" - - name: "chore(health): check up on recent changes to the health score" - uses: slackapi/slack-health-score@v0.1.1 + uses: slackapi/slack-health-score@d58a419f15cdaff97e9aa7f09f95772830ab66f7 # v0.1.1 with: codecov_token: ${{ secrets.CODECOV_API_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 0b4c043f..acd325c4 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ node_modules .DS_Store # Testing remnants -coverage +coverage.txt diff --git a/.mocharc.json b/.mocharc.json deleted file mode 100644 index ce5d6526..00000000 --- a/.mocharc.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "require": [ - "@actions/core", - "@actions/github", - "@slack/web-api", - "axios", - "axios-retry", - "flat", - "https-proxy-agent", - "markup-js", - "mocha-suppress-logs" - ], - "recursive": true, - "timeout": 3000 -} diff --git a/.nvmrc b/.nvmrc index 209e3ef4..a45fd52c 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20 +24 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..4dcf339f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,47 @@ +# slack-github-action + +## 4.0.0 + +### Major Changes + +- b1974f0: build: parse yaml with more strict multiline indentation rules + + Internal dependencies of [`js-yaml@v5`](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md#500---2026-06-20) make YAML parsing more strict and compliant with the YAML specification. Indentation is now required for values that span multiple lines against the base value. + + See the YAML [line prefixes](https://yaml.org/spec/1.2.2/#63-line-prefixes) spec for the expected indentation rule: + + ```diff + channel: "C0123" + text: "first line + + - second line" + + second line" + ``` + +### Patch Changes + +- 654bb72: chore: provide global fetch proxied configurations with updates to web api and webhook packages + +## 3.0.5 + +### Patch Changes + +- 96fddbe: fix: revert multiline yaml parsing indentation change + +## 3.0.4 + +### Patch Changes + +- fa03fe4: refactor: send webhooks with the [`@slack/webhook`](https://docs.slack.dev/tools/node-slack-sdk/webhook) package + +## 3.0.3 + +### Patch Changes + +- 66834e4: feat: add instrumentation to address error rates + +## 3.0.2 + +### Patch Changes + +- 79529d7: fix: resolve url.parse deprecation warning for webhook techniques diff --git a/README.md b/README.md index 30c32bd5..69058958 100644 --- a/README.md +++ b/README.md @@ -1,545 +1,25 @@ -# Slack Send GitHub Action +# Slack GitHub Action -> the GitHub Action for sending data to Slack +> the Slack GitHub Action is for sending data to Slack and running commands [![codecov](https://codecov.io/gh/slackapi/slack-github-action/graph/badge.svg?token=OZNX7FHN78)](https://codecov.io/gh/slackapi/slack-github-action) -Table of contents: +Comprehensive documentation is available at [docs.slack.dev/tools/slack-github-action](https://docs.slack.dev/tools/slack-github-action). -- [**Example workflows**](#example-workflows): Find inspiration in steps. -- [**Sending variables**](#sending-variables): Provide inputs to be sent. -- [**Sending techniques**](#sending-techniques): Choose how to send inputs. -- [**Additional configurations**](#additional-configurations): Customize - options. -- [**Expected outputs**](#expected-outputs): Retrieve response values. -- [**Project details**](#project-details): Information on the codebase. +Use this GitHub Action to: -## Example workflows - -For examples on how to leverage this Action in workflows, check out -[example workflows we have][examples] available. - -## Sending variables - -There are different [techniques to send data](#sending-techniques) into Slack -and whichever one is chosen will require a certain set of customized inputs, as -described later. - -You can provide data to send to Slack from this GitHub Action and either source: - -- The default event [context][event-context] with a [payload][event-payload] - matching the GitHub event. -- A custom payload with optional [variables][variables] provided in the GitHub - Action step. - -These input options are valid for all techniques, but some techniques require -specific constraints with certain requirements for valid inputs. - -Additional [configurations](#additional-configurations) and other details are -also available for more customizations to the provided payload. - -## Sending techniques - -This Action offers three different techniques to send data to Slack: - -1. [**Technique 1**](#technique-1-slack-workflow-builder): Send data with a - webhook to start a workflow in [Workflow Builder][wfb]. -2. [**Technique 2**](#technique-2-slack-api-method): Send data using - [a Slack API method][methods] and a secret token with required scopes. -3. [**Technique 3**](#technique-3-slack-incoming-webhook): Send data as a - message with a Slack [incoming webhook][incoming-webhook] URL. - -### Technique 1: Slack Workflow Builder - -> :memo: This technique requires [a Slack paid plan][plans] to use Workflow -> Builder. - -This technique sends data to Slack using a webhook to start a workflow created -using Slack [Workflow Builder][wfb]. - -#### Setup - -Start in Slack to create a Slack workflow: - -1. [Create a Slack workflow][wfb-create] that starts from a webhook. -2. Copy the webhook URL and [add it as a repository secret][repo-secret] called - `SLACK_WEBHOOK_URL`. -3. [Add this Action as a step][job-step] to your GitHub workflow and provide an - input payload to send to the webhook. -4. Configure your Slack workflow to use the payload variables sent from the - GitHub Action. You can then update the steps of the Slack workflow to use - these values in creative and clever ways. - -The webhook URL will resemble something like so: - -```txt -https://hooks.slack.com/triggers/T0123456789/3141592653589/c6e6c0d868b3054ca0f4611a5dbadaf -``` - -#### Usage - -Update the input payloads sent from this GitHub Action to your Slack workflow -using the following options: - -##### Sending values from the default GitHub event context - -In the example below, the default GitHub event [context][event-context] and -event [payload][event-payload] associated with the job that started the GitHub -workflow are sent to the provided webhook URL: - -```yaml -- name: Send GitHub Action data to a Slack workflow - uses: slackapi/slack-github-action@v2.0.0 - with: - payload-delimiter: "_" - webhook: ${{ secrets.SLACK_WEBHOOK_URL }} - webhook-type: webhook-trigger -``` - -Accessing variables sent to [Workflow Builder][wfb] with a webhook require that -the payload variables are flattened with stringified values. Nested variables in -the provided payload can be both flattened and also stringified with the -`payload-delimiter` option or changed with other -[configurations](#additional-configurations) to match this format expected from -Workflow Builder. - -##### Providing parsed payload information as strings - -Provided input values for payload information are sent to the webhook URL after -the job is started: - -```yaml -- name: Send custom event details to a Slack workflow - uses: slackapi/slack-github-action@v2.0.0 - with: - webhook: ${{ secrets.SLACK_WEBHOOK_URL }} - webhook-type: webhook-trigger - payload: | - status: "${{ job.status }}" - option: "false" -``` - -##### Gathering details of the payload from a saved file - -Input values for the payload to be sent can also be provided in a file, either -in JSON or YAML format: - -```yaml -- name: Send a saved artifact to a Slack workflow - uses: slackapi/slack-github-action@v2.0.0 - with: - payload-file-path: "./artifacts.json" - webhook: ${{ secrets.SLACK_WEBHOOK_URL }} - webhook-type: webhook-trigger -``` - -### Technique 2: Slack API method - -A bot token or user token or [token of some other kind][tokens] must be used to -call one of [the Slack API methods][methods] with this technique. - -#### Setup - -Different Slack API [methods][methods] require different [scopes][scopes], but -setup should be similar for all methods: - -1. [Create a Slack app][apps-new] for your workspace or use an existing app. -2. Depending on the Slack API [method][methods] you wish to call, add the - required **scopes** to your app under the **OAuth & Permissions** page on - [app settings][apps]. -3. Install the app to your workspace using the **Install App** page. -4. Once your app is installed to a workspace, a new [token][tokens] with your - app's specified scopes will be minted for that workspace. It is worth noting - that tokens are only valid for a single workspace! Find the token on the - **OAuth & Permissions** page. -5. Add the token as [a repository secret][repo-secret] called `SLACK_BOT_TOKEN` - or something similar and memorable. -6. [Add this Action as a step][job-step] to your GitHub workflow and provide an - input payload to send to the method. - -Methods that require an app configuration token should gather this token from -the [app configuration token][config-tokens] settings instead of from a specific -app since this token is associated with the workspace. - -#### Usage - -Choosing inputs for these steps is left as an exercise for the actioneer since -each of the Slack API methods requires certain values and specific parameters, -but these snippets might be helpful when starting. - -##### Posting a message with text - -Posting a message with the [`chat.postMessage`][chat.postMessage] method can be -achieved by adding this step to a job in your GitHub workflow and inviting the -bot associated with your app to the channel for posting: - -```yaml -- name: Post text to a Slack channel - uses: slackapi/slack-github-action@v2.0.0 - with: - method: chat.postMessage - token: ${{ secrets.SLACK_BOT_TOKEN }} - payload: | - channel: ${{ secrets.SLACK_CHANNEL_ID }} - text: "howdy <@channel>!" -``` - -##### Posting a message with blocks - -More complex message layouts, such as messages made with [Block Kit][block-kit] -blocks, can also be sent with one of the Slack API methods: - -```yaml -- name: Post blocks to a Slack channel - uses: slackapi/slack-github-action@v2.0.0 - with: - method: chat.postMessage - token: ${{ secrets.SLACK_BOT_TOKEN }} - payload: | - channel: ${{ secrets.SLACK_CHANNEL_ID }} - text: "GitHub Action build result: ${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}" - blocks: - - type: "section" - text: - type: "mrkdwn" - text: "GitHub Action build result: ${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}" -``` - -##### Updating a message - -Updating a message after it's posted can be done with the -[`chat.update`][chat.update] method and chaining multiple steps together using -outputs from past steps as inputs to current ones: - -```yaml -- name: Initiate the deployment launch sequence - id: launch_sequence - uses: slackapi/slack-github-action@v2.0.0 - with: - method: chat.postMessage - token: ${{ secrets.SLACK_BOT_TOKEN }} - payload: | - channel: ${{ secrets.SLACK_CHANNEL_ID }} - text: "Deployment started :eyes:" - attachments: - - color: "dbab09" - fields: - - title: "Status" - short: true - value: "In Progress" -- name: Countdown until launch - run: sleep 10 -- name: Update the original message with success - uses: slackapi/slack-github-action@v2.0.0 - with: - method: chat.update - token: ${{ secrets.SLACK_BOT_TOKEN }} - payload: | - channel: ${{ secrets.SLACK_CHANNEL_ID }} - ts: "${{ steps.launch_sequence.outputs.ts }}" - text: "Deployment finished! :rocket:" - attachments: - - color: "28a745" - fields: - - title: "Status" - short: true - value: "Completed" -``` - -##### Replying to a message - -Posting [threaded replies to a message][messaging-threads] from a past job can -be done by including the `thread_ts` attribute of the parent message in the -`payload`: - -```yaml -- name: Initiate a deployment - uses: slackapi/slack-github-action@v2.0.0 - id: deployment_message - with: - method: chat.postMessage - token: ${{ secrets.SLACK_BOT_TOKEN }} - payload: | - channel: ${{ secrets.SLACK_CHANNEL_ID }} - text: "Deployment started :eyes:" -- name: Conclude the deployment - uses: slackapi/slack-github-action@v2.0.0 - with: - method: chat.postMessage - token: ${{ secrets.SLACK_BOT_TOKEN }} - payload: | - channel: ${{ secrets.SLACK_CHANNEL_ID }} - thread_ts: "${{ steps.deployment_message.outputs.ts }}" - text: "Deployment finished! :rocket:" -``` - -##### Uploading a file - -Calling [a Slack API method][methods] with [`@slack/web-api`][slack-web-api] -makes [uploading a file][files.upload] just another API call with all of the -convenience of the [`files.uploadV2`][files.uploadV2] method: - -```yaml -- name: Share a file to that channel - uses: slackapi/slack-github-action@v2.0.0 - with: - method: files.uploadV2 - token: ${{ secrets.SLACK_BOT_TOKEN }} - payload: | - channel_id: ${{ secrets.SLACK_CHANNEL_ID }} - initial_comment: "the results are in!" - file: "./path/to/results.out" - filename: "results-${{ github.sha }}.out" -``` - -### Technique 3: Slack incoming webhook - -This technique uses this Action to post a message to a channel or direct message -with [incoming webhooks][incoming-webhook] and a Slack app. - -Incoming webhooks follow the same [formatting][formatting] patterns as other -Slack messaging APIs. Posted messages can be as short as a single line of text, -include additional interactivity with [interactive components][interactivity], -or be formatted with [Block Kit][block-kit] to build visual components. - -#### Setup - -Gather a Slack incoming webhook URL: - -1. [Create a Slack app][apps-new] for your workspace or use an existing app. -2. Add the [`incoming-webhook`][incoming-webhook-scope] bot scope under **OAuth - & Permissions** page on [app settings][apps]. -3. Install the app to your workspace and select a channel to notify from the - **Install App** page. -4. Create additional webhooks from the **Incoming Webhooks** page. -5. Add the generated incoming webhook URL as [a repository secret][repo-secret] - called `SLACK_WEBHOOK_URL`. -6. [Add this Action as a step][job-step] to your GitHub workflow and provide an - input payload to send as a message. - -The webhook URL will resemble something like so: - -```txt -https://hooks.slack.com/services/T0123456789/B1001010101/7IsoQTrixdUtE971O1xQTm4T -``` - -#### Usage - -Add the collected webhook from above to a GitHub workflow and configure the step -using [`mrkdwn`][mrkdwn] formatting values for a message or -[Block Kit][block-kit] blocks: - -```yaml -- name: Post a message in a channel - uses: slackapi/slack-github-action@v2.0.0 - with: - webhook: ${{ secrets.SLACK_WEBHOOK_URL }} - webhook-type: incoming-webhook - payload: | - text: "*GitHub Action build result*: ${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}" - blocks: - - type: "section" - text: - type: "mrkdwn" - text: "GitHub Action build result: ${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}" -``` - -## Additional configurations - -Not all of the above settings serve every customization of a workflow, so these -options might be useful. - -### Exiting with errors - -Invalid API requests or unexpected webhook payloads cause a failing response -that can be used to fail the GitHub Actions step with the `errors` option. - -The `errors` option defaults to `false` so failed requests do not cause the step -to fail. This result can still be gathered from the `ok` output. - -```yaml -- name: Attempt to call an unknown method - uses: slackapi/slack-github-action@v2.0.0 - with: - errors: true - method: chat.reverse - token: ${{ secrets.SLACK_BOT_TOKEN }} - payload: | - text: "palindrome" -``` - -Invalid inputs to the Action, such as not including a payload, will always cause -the GitHub step to fail. - -### Flattening nested payloads - -Variables and data provided in the payload might contain nested fields that need -to be flattened before being sent with a -[webhook trigger](#technique-1-slack-workflow-builder) to match the expected -input format of [Workflow Builder][wfb]. - -The `payload-delimiter` option will flatten the input payload using the provided -delimiter and will also make values stringified: - -```yaml -- name: Flatten the default GitHub payload - uses: slackapi/slack-github-action@v2.0.0 - with: - payload-delimiter: "_" - webhook: ${{ secrets.SLACK_WEBHOOK_URL }} - webhook-type: webhook-trigger -``` - -Reference to the flattening implementation is available for exploration from -within the [`flat`][flat] package. - -### Parsing templated variables - -Additional variables provided in the Github event [context][event-context] and -event [payload][event-payload] can be used to replace templated variables in the -input payload with the `payload-templated` option: - -```yaml -- name: Send custom JSON data to Slack workflow - uses: slackapi/slack-github-action@v2.0.0 - with: - payload-file-path: "./payload-slack-content.json" - payload-templated: true - webhook: ${{ secrets.SLACK_WEBHOOK_URL }} - webhook-type: webhook-trigger -``` - -This replaces variables templated as `${{ github.payload.repository.html_url }}` -with the values found in the GitHub Action event [payload][event-payload]. - -### Proxying HTTPS requests - -If you need to use a proxy to connect to Slack, you can use the `proxy` option. -In this example we use the technique that calls a Slack API method, but -configuring a proxy is the same for all techniques: - -```yaml -- name: Post to a Slack channel via a proxy - uses: slackapi/slack-github-action@v2.0.0 - with: - method: chat.postMessage - proxy: "http://proxy.example.org:8080" # Change this to a custom value - token: ${{ secrets.SLACK_BOT_TOKEN }} - payload: | - channel: ${{ secrets.SLACK_CHANNEL_ID }} - text: "This message was sent through a proxy" -``` - -The `proxy` option can also be provided with the `HTTPS_PROXY` or `https_proxy` -[environment variable][github-environment] from within the GitHub Actions step. - -### Retrying failed requests - -Sometimes outgoing requests fail due to [rate limits][rate-limits] or similar -[HTTP responses][retry-after] and can be retried later. - -The `retries` option can be configured to the needs of your workflow with one of -these values: - -- `0`: No retries, just hope that things go alright. -- `5`: Five retries in five minutes. **Default**. -- `10`: Ten retries in about thirty minutes. -- `RAPID`: A burst of retries to keep things running fast. - -```yaml -- name: Attempt a burst of requests - uses: slackapi/slack-github-action@v2.0.0 - with: - method: chat.postMessage - retries: RAPID - token: ${{ secrets.SLACK_BOT_TOKEN }} - payload: | - channel: ${{ secrets.SLACK_CHANNEL_ID }} - text: "status: all things are going good" -``` - -Behind the scenes, [automatic retries][retries] are handled with the -[`@slack/web-api`][slack-web-api] package for Slack API methods, and -[`axios-retry`][axios-retry] when sending with a webhook. - -### Sending to a custom API URL - -In certain circumstances, such as testing the sent payload, a -[custom API URL][custom-api-url] can be used to change where `method` requests -are sent: - -```yaml -- name: Send to a custom API URL - uses: slackapi/slack-github-action@v2.0.0 - with: - api: http://localhost:8080 - method: chat.postMessage - token: ${{ secrets.SLACK_BOT_TOKEN }} - payload: | - channel: ${{ secrets.SLACK_CHANNEL_ID }} - text: "What's happening on localhost?" -``` - -The default value of `api` is "https://slack.com/api/" for steps using `method`. - -## Expected outputs - -Each technique above [outputs values][github-outputs] that can be used as inputs -in following steps of a GitHub workflow. - -The following outputs are returned with each of the techniques: - -- `time`: `number`. The Unix [epoch time][epoch] that the step completed. -- `ok`: `boolean`. If the request completed with success. -- `response`: `string`. The [response][response] from the request as stringified - JSON. - -While these outputs are returned with certain Slack API methods: - -- `channel_id`: `string`. The [channel ID][conversation] included in the - response. -- `ts`: `string`. The [timestamp][messaging-timestamp] of the Slack event or - message. -- `thread_ts`: `string`. The [timestamp][messaging-timestamp] of a parent Slack - message with [threaded replies][messaging-parents]. - -### Example responses - -The following snippet shows how multiple steps can be chained together to create -a Slack channel before posting a message: - -```yaml -- name: Create a new Slack channel for recent changes - id: conversation - uses: slackapi/slack-github-action@v2.0.0 - with: - method: conversations.create - token: ${{ secrets.SLACK_BOT_TOKEN }} - payload: | - name: pull-request-review-${{ github.sha }} -- name: Send the pull request link into the Slack channel - if: ${{ steps.conversation.outputs.ok }} - uses: slackapi/slack-github-action@v2.0.0 - with: - method: chat.postMessage - token: ${{ secrets.SLACK_BOT_TOKEN }} - payload: | - channel: ${{ steps.conversation.outputs.channel_id }} - text: "A PR was created : ${{ github.event.pull_request.html_url }}" -``` +- [Send data with a webhook to start a workflow in Workflow Builder](https://docs.slack.dev/tools/slack-github-action/sending-data-webhook-slack-workflow) +- [Send data using a Slack API method and a secret token with required scopes](https://docs.slack.dev/tools/slack-github-action/sending-data-slack-api-method/) +- [Send data as a message with a Slack incoming webhook URL](https://docs.slack.dev/tools/slack-github-action/sending-data-slack-incoming-webhook/) +- [Run Slack CLI commands with a service token](https://docs.slack.dev/tools/slack-github-action/running-slack-cli-commands/) ## Project details ### Versioning -We recommend using the latest version of this Action for the most recent updates -and fixes. - -Migration guides are available in the [release notes][releases] with breaking -changes noted between versions. +We recommend using the latest version of this Action for the most recent updates and fixes. -Changes required when upgrading from `@v1` to `@v2` are included in this -[migration guide][v2.0.0]. +Changes are logged in the [release notes](https://github.com/slackapi/slack-github-action/releases) with migration guides available for updating from `@v1` to the [`@v2`](https://github.com/slackapi/slack-github-action/releases/tag/v2.0.0) and [`@v3`](https://github.com/slackapi/slack-github-action/releases/tag/v3.0.0) or [`@v4`](https://github.com/slackapi/slack-github-action/releases/tag/v4.0.0) releases. ### License @@ -547,51 +27,4 @@ This project is licensed under the [MIT license](LICENSE). ### Contributing -All contributions are encouraged! Check out the -[contributor's guide][contributing] to learn more. - -[apps]: https://api.slack.com/apps -[apps-new]: https://api.slack.com/apps/new -[axios-retry]: https://www.npmjs.com/package/axios-retry -[block-kit]: https://api.slack.com/surfaces/messages#complex_layouts -[chat.postMessage]: https://api.slack.com/methods/chat.postMessage -[chat.update]: https://api.slack.com/methods/chat.update -[chat:write]: https://api.slack.com/scopes/chat:write -[config-tokens]: https://api.slack.com/reference/manifests#config-tokens -[contributing]: .github/contributing.md -[conversation]: https://api.slack.com/types/conversation -[custom-api-url]: https://tools.slack.dev/node-slack-sdk/web-api/#custom-api-url -[epoch]: https://en.wikipedia.org/wiki/Unix_time -[event-context]: https://github.com/actions/toolkit/blob/main/packages/github/src/context.ts#L6 -[event-payload]: https://docs.github.com/en/webhooks/webhook-events-and-payloads -[examples]: https://github.com/slackapi/slack-github-action/tree/main/example-workflows -[files.upload]: https://api.slack.com/messaging/files#upload -[files.uploadV2]: https://tools.slack.dev/node-slack-sdk/web-api/#upload-a-file -[flat]: https://www.npmjs.com/package/flat -[formatting]: https://api.slack.com/reference/surfaces/formatting -[github-environment]: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables -[github-outputs]: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/passing-information-between-jobs -[github-variables]: https://docs.github.com/en/actions/learn-github-actions/variables -[incoming-webhook]: https://api.slack.com/messaging/webhooks -[incoming-webhook-scope]: https://api.slack.com/scopes/incoming-webhook -[interactivity]: https://api.slack.com/messaging/interactivity -[job-step]: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idsteps -[messaging-parents]: https://api.slack.com/messaging/retrieving#finding_threads -[messaging-threads]: https://api.slack.com/messaging/sending#threading -[messaging-timestamp]: https://api.slack.com/messaging/retrieving#individual_messages -[methods]: https://api.slack.com/methods -[mrkdwn]: https://api.slack.com/reference/surfaces/formatting -[plans]: https://slack.com/pricing -[rate-limits]: https://api.slack.com/apis/rate-limits -[releases]: https://github.com/slackapi/slack-github-action/releases -[repo-secret]: https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository -[response]: https://api.slack.com/web#responses -[retries]: https://tools.slack.dev/node-slack-sdk/web-api/#automatic-retries -[retry-after]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After -[scopes]: https://api.slack.com/scopes -[slack-web-api]: https://tools.slack.dev/node-slack-sdk/web-api -[tokens]: https://api.slack.com/concepts/token-types -[v2.0.0]: https://github.com/slackapi/slack-github-action/releases/tag/v2.0.0 -[variables]: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables -[wfb]: https://slack.com/features/workflow-automation -[wfb-create]: https://slack.com/help/articles/360041352714-Build-a-workflow--Create-a-workflow-that-starts-outside-of-Slack +All contributions are encouraged! Check out the [contributor's guide](.github/contributing.md) to learn more. diff --git a/action.yml b/action.yml index d5ed8fff..13d3dd40 100644 --- a/action.yml +++ b/action.yml @@ -1,6 +1,6 @@ -name: "Slack: Send to Slack" +name: "The Slack GitHub Action" author: "slackapi" -description: "Send data to Slack to start a Slack workflow in Workflow Builder, call a Slack API method, or post a message into a channel" +description: "Post a message, call a Web API method, start a Slack workflow, or run a Slack CLI command from GitHub Actions" inputs: api: description: "A custom API URL to send Slack API method requests to." @@ -16,7 +16,7 @@ inputs: description: "Attributes that create the content of the request using JSON or YAML." required: false payload-delimiter: - description: "Field seperator for nested attributes in the input payload." + description: "Field separator for nested attributes in the input payload." required: false payload-file-path: description: "Path to a file containing a valid input payload made of JSON or YAML." @@ -54,5 +54,5 @@ outputs: ts: description: "The timestamp of a Slack message or event in the response." runs: - using: "node20" + using: "node24" main: "dist/index.js" diff --git a/biome.json b/biome.json index 8c15ed51..ef675750 100644 --- a/biome.json +++ b/biome.json @@ -1,7 +1,7 @@ { "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "files": { - "ignore": ["*.json"], + "includes": ["**", "!**/*.json", "!coverage", "!dist"], "ignoreUnknown": true }, "formatter": { @@ -10,11 +10,27 @@ "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "style": { + "noParameterAssign": "error", + "useAsConstAssertion": "error", + "useDefaultParameterLast": "error", + "useEnumInitializers": "error", + "useSelfClosingElements": "error", + "useSingleVarDeclarator": "error", + "noUnusedTemplateLiteral": "error", + "useNumberNamespace": "error", + "noInferrableTypes": "error", + "noUselessElse": "error" + } } }, - "organizeImports": { - "enabled": true + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } }, "vcs": { "enabled": true, diff --git a/cli/action.yml b/cli/action.yml new file mode 100644 index 00000000..5fd8249a --- /dev/null +++ b/cli/action.yml @@ -0,0 +1,150 @@ +name: "The Slack GitHub Action: CLI" +author: "slackapi" +description: "Install the Slack CLI and run a command in a GitHub Actions workflow" +inputs: + command: + description: "A Slack CLI command to run without the 'slack' prefix." + required: true + token: + description: "A service token passed as the '--token' flag to the CLI command." + required: false + version: + description: "The version of the Slack CLI to install. Defaults to the latest." + required: false +outputs: + ok: + description: "If the command completed with a '0' exit code." + value: ${{ steps.slack-cli-unix.outputs.ok || steps.slack-cli-windows.outputs.ok }} + response: + description: "The standard output from the CLI command." + value: ${{ steps.slack-cli-unix.outputs.response || steps.slack-cli-windows.outputs.response }} + time: + description: "The Unix epoch time that the step completed." + value: ${{ steps.slack-cli-unix.outputs.time || steps.slack-cli-windows.outputs.time }} +runs: + using: composite + steps: + - name: Check if Slack CLI already exists + id: slack-cli-check + shell: bash + run: | + if command -v slack &> /dev/null; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + - name: Cache Slack CLI + if: steps.slack-cli-check.outputs.exists != 'true' && inputs.version != '' + id: cache-cli + uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + with: + path: | + ${{ runner.os == 'Windows' && '~/AppData/Local/slack-cli' || '~/.slack/bin' }} + key: slack-cli-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }} + + - name: Bash Add Slack CLI to PATH + if: steps.slack-cli-check.outputs.exists != 'true' && runner.os != 'Windows' + shell: bash + run: echo "$HOME/.slack/bin" >> "$GITHUB_PATH" # zizmor: ignore[github-env] + + - name: Pwsh Add Slack CLI to PATH + if: steps.slack-cli-check.outputs.exists != 'true' && runner.os == 'Windows' + shell: pwsh + run: Add-Content -Path $env:GITHUB_PATH -Value "$env:USERPROFILE\AppData\Local\slack-cli\bin" # zizmor: ignore[github-env] + + - name: Bash Install Slack CLI + if: >- + steps.slack-cli-check.outputs.exists != 'true' && + steps.cache-cli.outputs.cache-hit != 'true' && + runner.os != 'Windows' + shell: bash + run: | + if [ -n "$SLACK_CLI_VERSION" ]; then + curl -fsSL https://downloads.slack-edge.com/slack-cli/install.sh | bash -s -- -v "$SLACK_CLI_VERSION" + else + curl -fsSL https://downloads.slack-edge.com/slack-cli/install.sh | bash -s + fi + env: + SLACK_CLI_VERSION: ${{ inputs.version }} + + - name: Pwsh Install Slack CLI + if: >- + steps.slack-cli-check.outputs.exists != 'true' && + steps.cache-cli.outputs.cache-hit != 'true' && + runner.os == 'Windows' + shell: pwsh + run: | + if ($env:SLACK_CLI_VERSION) { + $installer = Join-Path $env:TEMP "install-slack-cli.ps1" + Invoke-WebRequest -Uri "https://downloads.slack-edge.com/slack-cli/install-windows.ps1" -OutFile $installer + & $installer -v $env:SLACK_CLI_VERSION + } else { + irm https://downloads.slack-edge.com/slack-cli/install-windows.ps1 | iex + } + env: + SLACK_CLI_VERSION: ${{ inputs.version }} + + - name: Bash Run Slack CLI command + id: slack-cli-unix + if: runner.os != 'Windows' + shell: bash + env: + SLACK_COMMAND: ${{ inputs.command }} + SLACK_TOKEN: ${{ inputs.token }} + run: | + args="$SLACK_COMMAND --skip-update" + if [ "$RUNNER_DEBUG" = "1" ]; then + args="$args --verbose" + fi + if [ -n "$SLACK_TOKEN" ]; then + args="$args --token $SLACK_TOKEN" + fi + + set +e + output=$(slack $args 2>&1 | tee /dev/stderr) + exit_code=$? + set -e + + echo "ok=$([ $exit_code -eq 0 ] && echo 'true' || echo 'false')" >> "$GITHUB_OUTPUT" + echo "time=$(date +%s)" >> "$GITHUB_OUTPUT" + + echo "response<> "$GITHUB_OUTPUT" + echo "$output" >> "$GITHUB_OUTPUT" + echo "SLACKCLIEOF" >> "$GITHUB_OUTPUT" + + if [ $exit_code -ne 0 ]; then + exit $exit_code + fi + + - name: Pwsh Run Slack CLI command + id: slack-cli-windows + if: runner.os == 'Windows' + shell: pwsh + env: + SLACK_COMMAND: ${{ inputs.command }} + SLACK_TOKEN: ${{ inputs.token }} + run: | + $cliArgs = "$env:SLACK_COMMAND --skip-update" + if ($env:RUNNER_DEBUG -eq "1") { + $cliArgs = "$cliArgs --verbose" + } + if ($env:SLACK_TOKEN) { + $cliArgs = "$cliArgs --token $env:SLACK_TOKEN" + } + + $outputFile = New-TemporaryFile + slack $cliArgs.Split(' ') 2>&1 | Tee-Object -FilePath $outputFile.FullName + $exit_code = $LASTEXITCODE + $output = Get-Content $outputFile.FullName -Raw + + "ok=$( if ($exit_code -eq 0) { 'true' } else { 'false' } )" >> $env:GITHUB_OUTPUT + "time=$([DateTimeOffset]::UtcNow.ToUnixTimeSeconds())" >> $env:GITHUB_OUTPUT + + "response<> $env:GITHUB_OUTPUT + $output >> $env:GITHUB_OUTPUT + "SLACKCLIEOF" >> $env:GITHUB_OUTPUT + + if ($exit_code -ne 0) { + exit $exit_code + } diff --git a/docs/_sidebar.json b/docs/_sidebar.json new file mode 100644 index 00000000..eb712770 --- /dev/null +++ b/docs/_sidebar.json @@ -0,0 +1,15 @@ +[ + { + "type": "doc", + "id": "tools/slack-github-action/slack-github-action", + "label": "Slack GitHub Action", + "className": "sidebar-title" + }, + { "type": "html", "value": "
" }, + "tools/slack-github-action/gathering-variables", + "tools/slack-github-action/sending-data-webhook-slack-workflow", + "tools/slack-github-action/sending-data-slack-api-method", + "tools/slack-github-action/sending-data-slack-incoming-webhook", + "tools/slack-github-action/running-slack-cli-commands", + "tools/slack-github-action/additional-configurations" +] diff --git a/docs/additional-configurations.md b/docs/additional-configurations.md new file mode 100644 index 00000000..08045f4b --- /dev/null +++ b/docs/additional-configurations.md @@ -0,0 +1,99 @@ +# Additional configurations + +There are some additional, possibly useful, customization options for workflows. + +## Exiting with errors + +Invalid API requests or unexpected webhook payloads cause a failing response that can be used to fail the GitHub Actions step with the `errors` option. + +The `errors` option defaults to `false` so failed requests do not cause the step to fail. This result can still be gathered from the `ok` output. + +```yaml +- name: Attempt to call an unknown method + uses: slackapi/slack-github-action@v4.0.0 + with: + errors: true + method: chat.reverse + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + text: "palindrome" +``` + +Invalid inputs to the GitHub Action, such as not including a payload, will always cause the GitHub step to fail. + +## Parsing templated variables + +Additional variables provided in the GitHub event [context](https://github.com/actions/toolkit/blob/main/packages/github/src/context.ts#L6) and event [payload](https://docs.github.com/en/webhooks/webhook-events-and-payloads) can be used to replace templated variables in the input payload with the `payload-templated` option: + +```yaml +- name: Send custom JSON data to Slack workflow + uses: slackapi/slack-github-action@v4.0.0 + with: + payload-file-path: "./payload-slack-content.json" + payload-templated: true + webhook: ${{ secrets.SLACK_WEBHOOK_URL }} + webhook-type: webhook-trigger +``` + +This replaces variables templated as `${{ github.payload.repository.html_url }}` with the values found in the GitHub Action event [payload](https://docs.github.com/en/webhooks/webhook-events-and-payloads). + +## Proxying HTTPS requests + +If you need to use a proxy to connect to Slack, you can use the `proxy` option. In this example we use the technique that calls a Slack API method, but configuring a proxy is the same for all techniques: + +```yaml +- name: Post to a Slack channel via a proxy + uses: slackapi/slack-github-action@v4.0.0 + with: + method: chat.postMessage + proxy: "http://proxy.example.org:8080" # Change this to a custom value + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + channel: ${{ secrets.SLACK_CHANNEL_ID }} + text: "This message was sent through a proxy" +``` + +The `proxy` option can also be provided with the `HTTPS_PROXY` or `https_proxy` [environment variable](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables) from within the GitHub Actions step. + +## Retrying failed requests + +Sometimes outgoing requests fail due to [rate limits](/apis/web-api/rate-limits) or similar [HTTP responses](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) and can be retried later. + +The `retries` option can be configured to the needs of your workflow with one of these values: + +- `0`: No retries, just hope that things go alright. +- `5`: Five retries in five minutes. **Default**. +- `10`: Ten retries in about thirty minutes. +- `RAPID`: A burst of retries to keep things running fast. + +```yaml +- name: Attempt a burst of requests + uses: slackapi/slack-github-action@v4.0.0 + with: + method: chat.postMessage + retries: RAPID + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + channel: ${{ secrets.SLACK_CHANNEL_ID }} + text: "status: all things are going good" +``` + +Behind the scenes, [automatic retries](/tools/node-slack-sdk/web-api/#automatic-retries) are handled with the [`@slack/web-api`](/tools/node-slack-sdk/web-api/) package for Slack API methods, and [`axios-retry`](https://www.npmjs.com/package/axios-retry) when sending with a webhook. + +## Sending to a custom API URL + +In certain circumstances, such as testing the sent payload, a [custom API URL](/tools/node-slack-sdk/web-api/#custom-api-url) can be used to change where `method` requests are sent: + +```yaml +- name: Send to a custom API URL + uses: slackapi/slack-github-action@v4.0.0 + with: + api: http://localhost:8080 + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + channel: ${{ secrets.SLACK_CHANNEL_ID }} + text: "What's happening on localhost?" +``` + +The default value of `api` is `https://slack.com/api/` for steps using `method`. diff --git a/docs/gathering-variables.md b/docs/gathering-variables.md new file mode 100644 index 00000000..c4df36ef --- /dev/null +++ b/docs/gathering-variables.md @@ -0,0 +1,12 @@ +# Gathering variables + +There are different techniques to send data into Slack and whichever one is chosen will require a certain set of customized inputs, as described later. + +You can provide data to send to Slack from this GitHub Action and either source: + +- The default event [context](https://github.com/actions/toolkit/blob/main/packages/github/src/context.ts#L6) with a [payload](https://docs.github.com/en/webhooks/webhook-events-and-payloads) matching the GitHub event. +- A custom payload with optional [variables](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables) provided in the GitHub Action step. + +These input options are valid for all techniques, but some techniques require specific constraints with certain requirements for valid inputs. + +Additional [configurations](/tools/slack-github-action/additional-configurations) and other details are also available for more customizations to the provided payload. \ No newline at end of file diff --git a/docs/running-slack-cli-commands.md b/docs/running-slack-cli-commands.md new file mode 100644 index 00000000..51bd8b2b --- /dev/null +++ b/docs/running-slack-cli-commands.md @@ -0,0 +1,128 @@ +# Running Slack CLI commands + +The Slack CLI technique installs and runs [Slack CLI](/tools/slack-cli/) commands directly from a GitHub Actions workflow. + +This is useful for automating tasks such as deploying apps, validating an app manifest, or interacting with Slack platform features that are available with the CLI. + +## Setup + +### Authentication + +Pass a [service token](/authentication/tokens/) via the `token` input. This is appended as `--token ` to the CLI command. The [`slack auth token`](/tools/slack-cli/reference/commands/slack_auth_token) command can be used to gather this. + +### CLI version + +By default, the latest version of the Slack CLI is installed. To pin a specific version, use the `version` input: + +```yaml +- uses: slackapi/slack-github-action/cli@v4.0.0 + with: + command: "version" + version: "3.14.0" +``` + +If the `slack` command already exists on `PATH`, installation is skipped entirely. + +## Usage + +Provide a `command` input with the Slack CLI command to run, omitting the `slack` prefix. + +```yaml +- uses: slackapi/slack-github-action/cli@v4.0.0 + with: + command: "version" +``` + +## Debug logging + +When a workflow is re-run with **Enable debug logging**, the action automatically appends `--verbose` to the CLI command. You can also include `--verbose` in your `command` input manually at any time. + +```yaml +- uses: slackapi/slack-github-action/cli@v4.0.0 + with: + command: "deploy --app ${{ vars.SLACK_APP_ID }} --verbose" + token: ${{ secrets.SLACK_SERVICE_TOKEN }} +``` + +## Outputs + +The following outputs are available after a CLI command runs: + +| Output | Type | Description | +| ---------- | --------- | --------------------------------------------------------------------------------------- | +| `ok` | `boolean` | If the command completed with a `0` exit code. | +| `response` | `string` | The standard output from the CLI command. | +| `time` | `number` | The Unix [epoch time](https://en.wikipedia.org/wiki/Unix_time) that the step completed. | + +## Examples + +### Check the installed CLI version + +```yaml +steps: + - uses: slackapi/slack-github-action/cli@v4.0.0 + id: slack + with: + command: "version" + - run: echo "${{ steps.slack.outputs.response }}" +``` + +### Validate the app manifest + +```yaml +steps: + - uses: actions/checkout@v7 + - uses: slackapi/slack-github-action/cli@v4.0.0 + with: + command: "manifest validate --app ${{ vars.SLACK_APP_ID }}" + token: ${{ secrets.SLACK_SERVICE_TOKEN }} +``` + +
+Workflow: Validate a manifest + +This workflow validates the app manifest on pull requests to catch configuration issues early. + +```js reference +https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_4_Slack_CLI_Command/manifest.yml +``` + +
+ +### Deploy an app with a service token + +```yaml +steps: + - uses: actions/checkout@v7 + - uses: slackapi/slack-github-action/cli@v4.0.0 + with: + command: "deploy --app ${{ vars.SLACK_APP_ID }} --force" + token: ${{ secrets.SLACK_SERVICE_TOKEN }} +``` + +
+Workflow: Deploy an app + +This workflow deploys a Slack app when changes are pushed to the main branch. It uses a service token to authenticate the deploy command. + +```js reference +https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_4_Slack_CLI_Command/deploy.yml +``` + +
+ +### Manage collaborators + +
+ +Workflow: Manage collaborators + +This workflow adds or removes an app collaborator using a manually triggered workflow. + +This example combines the Slack API technique ([`users.lookupByEmail`](https://docs.slack.dev/reference/methods/users.lookupByEmail), [`chat.postMessage`](https://docs.slack.dev/reference/methods/chat.postMessage)) with the CLI technique ([`collaborators add`](https://docs.slack.dev/tools/slack-cli/reference/commands/slack_collaborators_add)/[`remove`](https://docs.slack.dev/tools/slack-cli/reference/commands/slack_collaborators_remove)) to look up a user by email, update collaborators, and post a confirmation message. + +```js reference +https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_4_Slack_CLI_Command/collaborators.yml +``` + +
\ No newline at end of file diff --git a/docs/sending-data-slack-api-method.md b/docs/sending-data-slack-api-method.md new file mode 100644 index 00000000..5ea2d382 --- /dev/null +++ b/docs/sending-data-slack-api-method.md @@ -0,0 +1,184 @@ +# Sending data using a Slack API method + +A bot token or user token or [token of some other kind](/authentication/tokens) must be used to call one of [the Slack API methods](/reference/methods) with this technique. + +## Setup + +Different [Slack API methods](/reference/methods) require different [scopes](/reference/scopes), but setup should be similar for all methods: + +1. [Create a Slack app](https://api.slack.com/apps/new) for your workspace or use an existing app. +2. Depending on the Slack API [method](/reference/methods) you wish to call, add the required **scopes** to your app under the **OAuth & Permissions** page on [app settings](https://api.slack.com/apps). +3. Install the app to your workspace using the **Install App** page. +4. Once your app is installed to a workspace, a new [token](/authentication/tokens) with your app's specified scopes will be minted for that workspace. It is worth noting that tokens are only valid for a single workspace! Find the token on the **OAuth & Permissions** page. +5. Add the token as [a repository secret](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository) called `SLACK_BOT_TOKEN` or something similar and memorable. +6. [Add this Action as a step](https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idsteps) to your GitHub workflow and provide an input payload to send to the method. + +Methods that require an app configuration token should gather this token from the [app configuration token](/app-manifests/configuring-apps-with-app-manifests#config-tokens) settings instead of from a specific app since this token is associated with the workspace. + +## Usage + +Choosing inputs for these steps is left as an exercise for the actioneer since each of the Slack API methods requires certain values and specific parameters, but these snippets might be helpful when starting. + +### Posting a message with text + +Posting a message with the [`chat.postMessage`](/reference/methods/chat.postMessage) method can be achieved by adding this step to a job in your GitHub workflow and inviting the bot associated with your app to the channel for posting: + +```yaml +- name: Post text to a Slack channel + uses: slackapi/slack-github-action@v4.0.0 + with: + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + channel: ${{ secrets.SLACK_CHANNEL_ID }} + text: "howdy !" +``` + +### Posting a message with blocks + +More complex message layouts, such as messages made with [Block Kit](/block-kit/) blocks, can also be sent with one of the Slack API methods: + +```yaml +- name: Post blocks to a Slack channel + uses: slackapi/slack-github-action@v4.0.0 + with: + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + channel: ${{ secrets.SLACK_CHANNEL_ID }} + text: "GitHub Action build result: ${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}" + blocks: + - type: "section" + text: + type: "mrkdwn" + text: "GitHub Action build result: ${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}" +``` + +### Updating a message + +Updating a message after it's posted can be done with the [`chat.update`](/reference/methods/chat.update) method and chaining multiple steps together using outputs from past steps as inputs to current ones: + +```yaml +- name: Initiate the deployment launch sequence + id: launch_sequence + uses: slackapi/slack-github-action@v4.0.0 + with: + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + channel: ${{ secrets.SLACK_CHANNEL_ID }} + text: "Deployment started :eyes:" + attachments: + - color: "dbab09" + fields: + - title: "Status" + short: true + value: "In Progress" +- name: Countdown until launch + run: sleep 10 +- name: Update the original message with success + uses: slackapi/slack-github-action@v4.0.0 + with: + method: chat.update + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + channel: ${{ secrets.SLACK_CHANNEL_ID }} + ts: "${{ steps.launch_sequence.outputs.ts }}" + text: "Deployment finished! :rocket:" + attachments: + - color: "28a745" + fields: + - title: "Status" + short: true + value: "Completed" +``` + +### Replying to a message + +Posting [threaded replies to a message](/messaging/#threading) from a past job can be done by including the `thread_ts` attribute of the parent message in the `payload`: + +```yaml +- name: Initiate a deployment + uses: slackapi/slack-github-action@v4.0.0 + id: deployment_message + with: + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + channel: ${{ secrets.SLACK_CHANNEL_ID }} + text: "Deployment started :eyes:" +- name: Conclude the deployment + uses: slackapi/slack-github-action@v4.0.0 + with: + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + channel: ${{ secrets.SLACK_CHANNEL_ID }} + thread_ts: "${{ steps.deployment_message.outputs.ts }}" + text: "Deployment finished! :rocket:" +``` + +### Uploading a file + +Calling [a Slack API method](/reference/methods) with [`@slack/web-api`](/tools/node-slack-sdk/web-api/) makes [uploading a file](/messaging/working-with-files#uploading_files) just another API call with all of the convenience of the [`files.uploadV2`](/tools/node-slack-sdk/web-api/#upload-a-file) method: + +```yaml +- name: Checkout an imagined project + uses: actions/checkout@v7 +- name: Share a file to that channel + uses: slackapi/slack-github-action@v4.0.0 + with: + method: files.uploadV2 + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + channel_id: ${{ secrets.SLACK_CHANNEL_ID }} + initial_comment: "the results are in!" + file: "./path/to/results.out" + filename: "results-${{ github.sha }}.out" +``` + +The [checkout](https://github.com/actions/checkout) step makes existing content available to upload from a workflow. + +## Expected outputs + +The technique, like all Slack Github Action techniques, [outputs values](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/passing-information-between-jobs) that can be used as inputs in following steps of a GitHub workflow. + +The following outputs are returned with each of the techniques: + +| Output | Type | Description| +|---|---|---| +|`time` | `number` | The Unix [epoch time](https://en.wikipedia.org/wiki/Unix_time) that the step completed. +| `ok` | `boolean` | If the request completed with success. +| `response` | `string` | The [response](/apis/web-api/#responses) from the request as stringified JSON. + +While these outputs are returned with certain Slack API methods: + +| Output | Type | Description| +|---|---|---| +|`channel_id` | `string` | The [channel ID](/reference/objects/conversation-object) included in the response. +| `ts`| `string` | The [timestamp](/messaging/retrieving-messages#individual_messages) of the Slack event or message. +| `thread_ts` | `string` | The [timestamp](/messaging/retrieving-messages#individual_messages) of a parent Slack message with [threaded replies](/messaging/retrieving-messages#pulling_threads). + +## Example workflows + +
+Direct message the author + +This workflow sends a direct message to the user that pushed the most recent commits. It does so by grabbing the email of the pusher. + +```js reference +https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_2_Slack_API_Method/author.yml +``` + +
+ +
+Invite a usergroup to channel + +This workflow creates a channel after a bug is reported and add members of a usergroup by chaining multiple Slack API method calls together. + +```js reference +https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_2_Slack_API_Method/invite.yml +``` + +
diff --git a/docs/sending-data-slack-incoming-webhook.md b/docs/sending-data-slack-incoming-webhook.md new file mode 100644 index 00000000..521665ce --- /dev/null +++ b/docs/sending-data-slack-incoming-webhook.md @@ -0,0 +1,96 @@ +# Sending data as a message with a Slack incoming webhook URL + +This technique uses this Action to post a message to a channel or direct message with [incoming webhooks](/messaging/sending-messages-using-incoming-webhooks) and a Slack app. + +Incoming webhooks follow the same [formatting](/apis/) patterns as other Slack messaging APIs. Posted messages can be as short as a single line of text, include additional interactivity with [interactive components](/messaging/creating-interactive-messages), or be formatted with [Block Kit](/block-kit/) to build visual components. + +## Setup + +Gather a Slack incoming webhook URL: + +1. [Create a Slack app](https://api.slack.com/apps/new) for your workspace or use an existing app. +2. Add the [`incoming-webhook`](/reference/scopes/incoming-webhook) bot scope under **OAuth & Permissions** page on [app settings](https://api.slack.com/apps). +3. Install the app to your workspace and select a channel to notify from the **Install App** page. +4. Create additional webhooks from the **Incoming Webhooks** page. +5. Add the generated incoming webhook URL as [a repository secret](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository) called `SLACK_WEBHOOK_URL`. +6. [Add this Action as a step](https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idsteps) to your GitHub workflow and provide an input payload to send as a message. + +The webhook URL will resemble something like so: + +```txt +https://hooks.slack.com/services/T0123456789/B1001010101/7IsoQTrixdUtE971O1xQTm4T +``` + +## Usage + +Add the collected webhook from above to a GitHub workflow and configure the step using [`mrkdwn`](/messaging/formatting-message-text) formatting values for a message or [Block Kit](/block-kit/) blocks: + +```yaml +- name: Post a message in a channel + uses: slackapi/slack-github-action@v4.0.0 + with: + webhook: ${{ secrets.SLACK_WEBHOOK_URL }} + webhook-type: incoming-webhook + payload: | + text: "*GitHub Action build result*: ${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}" + blocks: + - type: "section" + text: + type: "mrkdwn" + text: "GitHub Action build result: ${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}" +``` + +## Expected outputs + +The technique, like all Slack Github Action techniques, [outputs values](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/passing-information-between-jobs) that can be used as inputs in following steps of a GitHub workflow. + +The following outputs are returned with each of the techniques: + +| Output | Type | Description| +|---|---|---| +|`time` | `number` | The Unix [epoch time](https://en.wikipedia.org/wiki/Unix_time) that the step completed. +| `ok` | `boolean` | If the request completed with success. +| `response` | `string` | The [response](/apis/web-api/#responses) from the request as stringified JSON. + +## Example workflows + +
+Post an inline text message + +This workflow uses incoming webhooks to post a plain text message. + +```js reference +https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_3_Slack_Incoming_Webhook/text.yml +``` + +
+ +
+Post an inline block message + +This workflow uses incoming webhooks to post a message with Block Kit. + +```js reference +https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_3_Slack_Incoming_Webhook/blocks.yml +``` + +
+ +
+Post blocks found in a file + +This workflow uses file data when posting to an incoming webhook. It links to the GitHub Actions job in progress. + +Payload file being sent + +```js reference +https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_3_Slack_Incoming_Webhook/saved.data.json +``` + +### Workflow + +```js reference +https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_3_Slack_Incoming_Webhook/saved.gha.yml +``` + +
\ No newline at end of file diff --git a/docs/sending-data-webhook-slack-workflow.md b/docs/sending-data-webhook-slack-workflow.md new file mode 100644 index 00000000..c170e629 --- /dev/null +++ b/docs/sending-data-webhook-slack-workflow.md @@ -0,0 +1,192 @@ +# Sending data via a webhook to start a Slack workflow + +:::info[This technique requires [a Slack paid plan](https://slack.com/pricing) to use Workflow Builder.] +::: + +This technique sends data to Slack using a webhook to start a workflow created using Slack [Workflow Builder](https://slack.com/features/workflow-automation). + +## Setup + +Start in Slack to create a Slack workflow: + +1. [Create a Slack workflow](https://slack.com/help/articles/360041352714-Build-a-workflow--Create-a-workflow-that-starts-outside-of-Slack) that starts from a webhook. +2. Copy the webhook URL and [add it as a repository secret](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository) called `SLACK_WEBHOOK_URL`. +3. [Add this Action as a step](https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idsteps) to your GitHub workflow and provide an input payload to send to the webhook. +4. Configure your Slack workflow to use the payload variables sent from the GitHub Action. You can then update the steps of the Slack workflow to use these values in creative and clever ways. + +The webhook URL will resemble something like so: + +```txt +https://hooks.slack.com/triggers/T0123456789/3141592653589/c6e6c0d868b3054ca0f4611a5dbadaf +``` + +## Usage + +Update the input payloads sent from this GitHub Action to your Slack workflow using the following options: + +### Sending values from the default GitHub event context + +In the example below, the default GitHub event [context](https://github.com/actions/toolkit/blob/main/packages/github/src/context.ts#L6) and event [payload](https://docs.github.com/en/webhooks/webhook-events-and-payloads) associated with the job that started the GitHub workflow are sent to the provided webhook URL: + +```yaml +- name: Send GitHub Action data to a Slack workflow + uses: slackapi/slack-github-action@v4.0.0 + with: + payload-delimiter: "_" + webhook: ${{ secrets.SLACK_WEBHOOK_URL }} + webhook-type: webhook-trigger +``` + +Accessing variables sent to [Workflow Builder](https://slack.com/features/workflow-automation) with a webhook require that the payload variables are flattened with stringified values. Nested variables in the provided payload can be both flattened and also stringified with the `payload-delimiter` option or changed with other [configurations](/tools/slack-github-action/additional-configurations) to match this format expected from Workflow Builder. + +### Providing parsed payload information as strings + +Provided input values for payload information are sent to the webhook URL after the job is started: + +```yaml +- name: Send custom event details to a Slack workflow + uses: slackapi/slack-github-action@v4.0.0 + with: + webhook: ${{ secrets.SLACK_WEBHOOK_URL }} + webhook-type: webhook-trigger + payload: | + status: "${{ job.status }}" + option: "false" +``` + +### Gathering details of the payload from a saved file + +Input values for the payload to be sent can also be provided in a file, either in JSON or YAML format: + +```yaml +- name: Send a saved artifact to a Slack workflow + uses: slackapi/slack-github-action@v4.0.0 + with: + payload-file-path: "./artifacts.json" + webhook: ${{ secrets.SLACK_WEBHOOK_URL }} + webhook-type: webhook-trigger +``` + +### Flattening nested payloads + +Variables and data provided in the payload might contain nested fields that need to be flattened before being sent with a [webhook trigger](/tools/slack-github-action/sending-data-webhook-slack-workflow) to match the expected input format of [Workflow Builder](https://slack.com/features/workflow-automation). + +The `payload-delimiter` option will flatten the input payload using the provided delimiter and will also make values stringified: + +```yaml +- name: Flatten the default GitHub payload + uses: slackapi/slack-github-action@v4.0.0 + with: + payload-delimiter: "_" + webhook: ${{ secrets.SLACK_WEBHOOK_URL }} + webhook-type: webhook-trigger +``` + +Reference to the flattening implementation is available for exploration from within the [`flat`](https://www.npmjs.com/package/flat) package. + + +## Expected outputs + +The technique, like all Slack Github Action techniques, [outputs values](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/passing-information-between-jobs) that can be used as inputs in following steps of a GitHub workflow. + +The following outputs are returned with each of the techniques: + +| Output | Type | Description| +|---|---|---| +|`time` | `number` | The Unix [epoch time](https://en.wikipedia.org/wiki/Unix_time) that the step completed. +| `ok` | `boolean` | If the request completed with success. +| `response` | `string` | The [response](/apis/web-api/#responses) from the request as stringified JSON. + + +## Example workflows + +
+Format generated files + +This workflow converts build outputs from earlier GitHub Action steps into a Slack message. + +This example uses data from a payload file to [send a message](/tools/deno-slack-sdk/reference/slack-functions/send_message/) to a hardcoded channel. + +Payload file being sent + +```js reference +https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_1_Slack_Workflow_Builder/builds.data.json +``` + +GitHub Actions workflow + +```js reference +https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_1_Slack_Workflow_Builder/builds.gha.yml +``` + +Slack app manifest + +```js reference +https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_1_Slack_Workflow_Builder/builds.manifest.json +``` + +Slack webhook trigger + +```js reference +https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_1_Slack_Workflow_Builder/builds.trigger.json +``` + +
+ +
+Post release announcements + +This workflow allows you to select a channel to post news about the most recent release to. + +This example uses [Slack functions](/tools/deno-slack-sdk/guides/creating-slack-functions/) and inline inputs to do the following: + +1. Open a form to select a channel. +2. Send a message to the selected channel. +3. React with a `:tada:` emoji. + +GitHub Actions workflow + +```js reference +https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_1_Slack_Workflow_Builder/announcements.gha.yml +``` + +Slack app manifest + +```js reference +https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_1_Slack_Workflow_Builder/announcements.manifest.json +``` + +Slack webhook trigger + +```js reference +https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_1_Slack_Workflow_Builder/announcements.trigger.json +``` + +
+ +
+Update a channel topic + +This workflow shows the latest commit status in the header of a channel. + +This example uses the default GitHub event [context](https://github.com/actions/toolkit/blob/main/packages/github/src/context.ts#L6) and [payload](https://docs.github.com/en/webhooks/webhook-events-and-payloads) to [update a channel topic](/tools/deno-slack-sdk/reference/slack-functions/update_channel_topic/). + +GitHub Actions workflow + +```js reference +https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_1_Slack_Workflow_Builder/topic.gha.yml +``` + +Slack app manifest + +```js reference +https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_1_Slack_Workflow_Builder/topic.manifest.json +``` + +Slack webhook trigger + +```js reference +https://github.com/slackapi/slack-github-action/blob/main/example-workflows/Technique_1_Slack_Workflow_Builder/topic.trigger.json +``` + +
\ No newline at end of file diff --git a/docs/slack-github-action.md b/docs/slack-github-action.md new file mode 100644 index 00000000..dcb8af1d --- /dev/null +++ b/docs/slack-github-action.md @@ -0,0 +1,26 @@ +# Slack GitHub Action + +The Slack GitHub Action is for sending data to Slack and running commands. + +The Slack GitHub Action offers four different techniques for sending data to and interacting with Slack: + +- [Send data with a webhook to start a workflow in Workflow Builder](/tools/slack-github-action/sending-data-webhook-slack-workflow). +- [Send data using a Slack API method and a secret token with required scopes](/tools/slack-github-action/sending-data-slack-api-method/). +- [Send data as a message with a Slack incoming webhook URL](/tools/slack-github-action/sending-data-slack-incoming-webhook/). +- [Run Slack CLI commands](/tools/slack-github-action/running-slack-cli-commands). + +--- + +## Versioning + +We recommend using the latest version of this GitHub Action for the most recent updates and fixes. + +Changes are logged in the [release notes](https://github.com/slackapi/slack-github-action/releases) with migration guides available for updating from `@v1` to the [`@v2`](https://github.com/slackapi/slack-github-action/releases/tag/v2.0.0) and [`@v3`](https://github.com/slackapi/slack-github-action/releases/tag/v3.0.0) or [`@v4`](https://github.com/slackapi/slack-github-action/releases/tag/v4.0.0) releases. + +## License + +This project is licensed under the [MIT license](https://github.com/slackapi/slack-github-action/blob/main/LICENSE). + +## Contributing + +All contributions are encouraged! Check out the [contributor's guide](https://github.com/slackapi/slack-github-action/blob/main/.github/contributing.md) to learn more. diff --git a/example-workflows/Technique_1_Slack_Workflow_Builder/README.md b/example-workflows/Technique_1_Slack_Workflow_Builder/README.md index 14413975..3a2d6bc2 100644 --- a/example-workflows/Technique_1_Slack_Workflow_Builder/README.md +++ b/example-workflows/Technique_1_Slack_Workflow_Builder/README.md @@ -11,8 +11,7 @@ using Slack [Workflow Builder][wfb]. The Slack app manifest included with each example showcases steps of a workflow in [Workflow Builder][wfb] along with the [webhook trigger][triggers] inputs. -For details on how to setup this technique in GitHub Actions, read the -[`README.md`][setup]. +For details on how to setup this technique in GitHub Actions, read the [setup][setup] section of the docs. ## Example workflows @@ -71,11 +70,11 @@ This example uses the default GitHub event [context][event-context] and [event-context]: https://github.com/actions/toolkit/blob/main/packages/github/src/context.ts#L6 [event-payload]: https://docs.github.com/en/webhooks/webhook-events-and-payloads -[functions]: https://api.slack.com/automation/functions +[functions]: https://docs.slack.dev/tools/deno-slack-sdk/guides/creating-slack-functions/ [giphy]: https://giphy.com -[send_message]: https://api.slack.com/reference/functions/send_message +[send_message]: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/send_message [plans]: https://slack.com/pricing -[setup]: https://github.com/slackapi/slack-github-action/blob/main/README.md#technique-1-slack-workflow-builder -[triggers]: https://api.slack.com/automation/triggers/webhook -[update_channel_topic]: https://api.slack.com/reference/functions/update_channel_topic +[setup]: https://docs.slack.dev/tools/slack-github-action/sending-techniques/sending-data-webhook-slack-workflow/ +[triggers]: https://docs.slack.dev/tools/deno-slack-sdk/guides/creating-webhook-triggers/ +[update_channel_topic]: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/update_channel_topic [wfb]: https://slack.com/features/workflow-automation diff --git a/example-workflows/Technique_1_Slack_Workflow_Builder/announcements.gha.yml b/example-workflows/Technique_1_Slack_Workflow_Builder/announcements.gha.yml index 412c7424..dc721403 100644 --- a/example-workflows/Technique_1_Slack_Workflow_Builder/announcements.gha.yml +++ b/example-workflows/Technique_1_Slack_Workflow_Builder/announcements.gha.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Draft with these release notes details - uses: slackapi/slack-github-action@v2.0.0 + uses: slackapi/slack-github-action@v4.0.0 with: webhook: ${{ secrets.SLACK_WEBHOOK_URL }} webhook-type: webhook-trigger diff --git a/example-workflows/Technique_1_Slack_Workflow_Builder/builds.gha.yml b/example-workflows/Technique_1_Slack_Workflow_Builder/builds.gha.yml index 92843100..7a5eb51b 100644 --- a/example-workflows/Technique_1_Slack_Workflow_Builder/builds.gha.yml +++ b/example-workflows/Technique_1_Slack_Workflow_Builder/builds.gha.yml @@ -11,12 +11,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: persist-credentials: false - name: Start the Slack workflow - uses: slackapi/slack-github-action@v2.0.0 + uses: slackapi/slack-github-action@v4.0.0 with: payload-file-path: "./example-workflows/Technique_1_Slack_Workflow_Builder/builds.data.json" webhook: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/example-workflows/Technique_1_Slack_Workflow_Builder/topic.gha.yml b/example-workflows/Technique_1_Slack_Workflow_Builder/topic.gha.yml index a1194be2..62d8dcc8 100644 --- a/example-workflows/Technique_1_Slack_Workflow_Builder/topic.gha.yml +++ b/example-workflows/Technique_1_Slack_Workflow_Builder/topic.gha.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Update the channel topic - uses: slackapi/slack-github-action@v2.0.0 + uses: slackapi/slack-github-action@v4.0.0 with: payload-delimiter: "_" webhook: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/example-workflows/Technique_2_Slack_API_Method/README.md b/example-workflows/Technique_2_Slack_API_Method/README.md index 8e1f0205..74787fc0 100644 --- a/example-workflows/Technique_2_Slack_API_Method/README.md +++ b/example-workflows/Technique_2_Slack_API_Method/README.md @@ -5,8 +5,7 @@ A [token][tokens], such as a bot or user token, must be used to call one of ## Setup -For details on how to set up this technique in GitHub Actions, read the -[`README.md`][setup]. +For details on how to setup this technique in GitHub Actions, read the [setup][setup] section of the docs. ## Example workflows @@ -35,6 +34,6 @@ This example chains multiple Slack API methods together to help fix bugs fast. - [`invite.yml`](./invite.yml): GitHub Actions workflow. -[methods]: https://api.slack.com/methods -[setup]: https://github.com/slackapi/slack-github-action?tab=readme-ov-file#technique-2-slack-api-method -[tokens]: https://api.slack.com/concepts/token-types +[methods]: https://docs.slack.dev/reference/methods/ +[setup]: https://docs.slack.dev/tools/slack-github-action/sending-techniques/sending-data-slack-api-method/ +[tokens]: https://docs.slack.dev/authentication/tokens/ diff --git a/example-workflows/Technique_2_Slack_API_Method/author.yml b/example-workflows/Technique_2_Slack_API_Method/author.yml index 0e62d513..1caf5c76 100644 --- a/example-workflows/Technique_2_Slack_API_Method/author.yml +++ b/example-workflows/Technique_2_Slack_API_Method/author.yml @@ -9,15 +9,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: persist-credentials: false - name: Find correspondences id: email - uses: slackapi/slack-github-action@v2.0.0 + uses: slackapi/slack-github-action@v4.0.0 with: - method: users.lookupByEmail # https://api.slack.com/methods/users.lookupByEmail + method: users.lookupByEmail # https://docs.slack.dev/reference/methods/users.lookupByEmail/ token: ${{ secrets.SLACK_BOT_TOKEN }} payload: | email: ${{ github.event.pusher.email }} @@ -30,10 +30,10 @@ jobs: - name: Send a direct message if: ${{ steps.email.outputs.ok }} - uses: slackapi/slack-github-action@v2.0.0 + uses: slackapi/slack-github-action@v4.0.0 with: errors: true - method: chat.postMessage # https://api.slack.com/methods/chat.postMessage + method: chat.postMessage # https://docs.slack.dev/reference/methods/chat.postMessage/ token: ${{ secrets.SLACK_BOT_TOKEN }} payload: | "channel": "${{ env.SLACK_USER_ID }}", diff --git a/example-workflows/Technique_2_Slack_API_Method/invite.yml b/example-workflows/Technique_2_Slack_API_Method/invite.yml index e19f7b9a..7253eb93 100644 --- a/example-workflows/Technique_2_Slack_API_Method/invite.yml +++ b/example-workflows/Technique_2_Slack_API_Method/invite.yml @@ -13,18 +13,19 @@ jobs: steps: - name: Create a new Slack channel for communications id: conversation - uses: slackapi/slack-github-action@v2.0.0 + uses: slackapi/slack-github-action@v4.0.0 with: errors: true - method: conversations.create # https://api.slack.com/methods/conversations.create + method: conversations.create # https://docs.slack.dev/reference/methods/conversations.create/ token: ${{ secrets.SLACK_BOT_TOKEN }} payload: | name: issue-${{ github.event.issue.number }} - name: Send the issue link into the Slack channel - uses: slackapi/slack-github-action@v2.0.0 + uses: slackapi/slack-github-action@v4.0.0 with: - method: chat.postMessage # https://api.slack.com/methods/chat.postMessage + errors: true + method: chat.postMessage # https://docs.slack.dev/reference/methods/chat.postMessage/ token: ${{ secrets.SLACK_BOT_TOKEN }} payload: | channel: ${{ steps.conversation.outputs.channel_id }} @@ -32,10 +33,10 @@ jobs: - name: Gather information of those to add id: members - uses: slackapi/slack-github-action@v2.0.0 + uses: slackapi/slack-github-action@v4.0.0 with: errors: true - method: usergroups.users.list # https://api.slack.com/methods/usergroups.users.list + method: usergroups.users.list # https://docs.slack.dev/reference/methods/usergroups.users.list/ token: ${{ secrets.SLACK_BOT_TOKEN }} payload: | usergroup: ${{ secrets.SLACK_USERGROUP_ID }} @@ -46,10 +47,10 @@ jobs: echo "SLACK_USERGROUP_USER_IDS=$SLACK_USERGROUP_USER_IDS" >> $GITHUB_ENV - name: Add the usergroup to the channel - uses: slackapi/slack-github-action@v2.0.0 + uses: slackapi/slack-github-action@v4.0.0 with: errors: true - method: conversations.invite # https://api.slack.com/methods/conversations.invite + method: conversations.invite # https://docs.slack.dev/reference/methods/conversations.invite/ token: ${{ secrets.SLACK_BOT_TOKEN }} payload: | channel: ${{ steps.conversation.outputs.channel_id }} diff --git a/example-workflows/Technique_3_Slack_Incoming_Webhook/README.md b/example-workflows/Technique_3_Slack_Incoming_Webhook/README.md index 5c388cbb..e4f46095 100644 --- a/example-workflows/Technique_3_Slack_Incoming_Webhook/README.md +++ b/example-workflows/Technique_3_Slack_Incoming_Webhook/README.md @@ -10,8 +10,7 @@ or be formatted with [Block Kit][block-kit] to build visual components. ## Setup -For details on how to setup this technique in GitHub Actions, read the -[`README.md`][setup]. +For details on how to setup this technique in GitHub Actions, read the [setup][setup] section of the docs. ## Example workflows @@ -50,8 +49,8 @@ This example uses file data when posting to an incoming webhook. - [`saved.data.json`](./saved.data.json): Payload file being sent. - [`saved.gha.yml`](./saved.gha.yml): GitHub Actions workflow. -[block-kit]: https://api.slack.com/surfaces/messages#complex_layouts -[formatting]: https://api.slack.com/reference/surfaces/formatting -[incoming-webhook]: https://api.slack.com/messaging/webhooks -[interactivity]: https://api.slack.com/messaging/interactivity -[setup]: https://github.com/slackapi/slack-github-action?tab=readme-ov-file#technique-3-slack-incoming-webhook +[block-kit]: https://docs.slack.dev/messaging/#complex_layouts +[formatting]: https://docs.slack.dev/messaging/formatting-message-text/ +[incoming-webhook]: https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/ +[interactivity]: https://docs.slack.dev/messaging/creating-interactive-messages/ +[setup]: https://docs.slack.dev/tools/slack-github-action/sending-techniques/sending-data-slack-incoming-webhook/ diff --git a/example-workflows/Technique_3_Slack_Incoming_Webhook/blocks.yml b/example-workflows/Technique_3_Slack_Incoming_Webhook/blocks.yml index b6909a98..92414796 100644 --- a/example-workflows/Technique_3_Slack_Incoming_Webhook/blocks.yml +++ b/example-workflows/Technique_3_Slack_Incoming_Webhook/blocks.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Write the review - uses: slackapi/slack-github-action@v2.0.0 + uses: slackapi/slack-github-action@v4.0.0 with: webhook: ${{ secrets.SLACK_WEBHOOK_URL }} webhook-type: incoming-webhook diff --git a/example-workflows/Technique_3_Slack_Incoming_Webhook/saved.gha.yml b/example-workflows/Technique_3_Slack_Incoming_Webhook/saved.gha.yml index dfd755a3..0dbb3f7d 100644 --- a/example-workflows/Technique_3_Slack_Incoming_Webhook/saved.gha.yml +++ b/example-workflows/Technique_3_Slack_Incoming_Webhook/saved.gha.yml @@ -11,12 +11,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: persist-credentials: false - name: Forward a saved message - uses: slackapi/slack-github-action@v2.0.0 + uses: slackapi/slack-github-action@v4.0.0 with: payload-file-path: "./example-workflows/Technique_3_Slack_Incoming_Webhook/saved.data.json" payload-templated: true diff --git a/example-workflows/Technique_3_Slack_Incoming_Webhook/text.yml b/example-workflows/Technique_3_Slack_Incoming_Webhook/text.yml index d92af4ef..07464bac 100644 --- a/example-workflows/Technique_3_Slack_Incoming_Webhook/text.yml +++ b/example-workflows/Technique_3_Slack_Incoming_Webhook/text.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Greet the reader - uses: slackapi/slack-github-action@v2.0.0 + uses: slackapi/slack-github-action@v4.0.0 with: webhook: ${{ secrets.SLACK_WEBHOOK_URL }} webhook-type: incoming-webhook diff --git a/example-workflows/Technique_4_Slack_CLI_Command/README.md b/example-workflows/Technique_4_Slack_CLI_Command/README.md new file mode 100644 index 00000000..23038487 --- /dev/null +++ b/example-workflows/Technique_4_Slack_CLI_Command/README.md @@ -0,0 +1,37 @@ +# Technique 4: Slack CLI Command + +A [service token](https://docs.slack.dev/authentication/tokens/#service) is used to install and run [Slack CLI](https://docs.slack.dev/tools/slack-cli/) commands directly from a GitHub Actions workflow with this technique. + +## Setup + +For details on how to setup this technique in GitHub Actions, read the [setup](https://docs.slack.dev/tools/slack-github-action/sending-techniques/running-slack-cli-commands/) section of the docs. + +## Example workflows + +1. [**Deploy an app**](#deploy-an-app): Deploy to Slack on push to the main branch. +2. [**Validate the app manifest**](#validate-the-app-manifest): Check the app manifest on pull requests. +3. [**Manage a collaborator**](#manage-a-collaborator): Add or remove an app collaborator using CLI and API techniques together. + +### Deploy an app + +Deploy a Slack app when changes are pushed to the main branch. This example uses a service token to authenticate the deploy command. + +**Related files**: + +- [`deploy.yml`](./deploy.yml): GitHub Actions workflow. + +### Validate the app manifest + +Run manifest validation on pull requests to catch configuration issues early. This example checks the app manifest file in the repository. + +**Related files**: + +- [`manifest.yml`](./manifest.yml): GitHub Actions workflow. + +### Manage a collaborator + +Add or remove an app collaborator using a manually triggered workflow with [`workflow_dispatch`](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/manually-run-a-workflow). This example combines the Slack API technique ([`users.lookupByEmail`](https://docs.slack.dev/reference/methods/users.lookupByEmail), [`chat.postMessage`](https://docs.slack.dev/reference/methods/chat.postMessage)) with the CLI technique ([`collaborators add`](https://docs.slack.dev/tools/slack-cli/reference/commands/slack_collaborators_add)/[`remove`](https://docs.slack.dev/tools/slack-cli/reference/commands/slack_collaborators_remove)) to look up a user by email, update collaborators, and post a confirmation message. + +**Related files**: + +- [`collaborators.yml`](./collaborators.yml): GitHub Actions workflow. diff --git a/example-workflows/Technique_4_Slack_CLI_Command/collaborators.yml b/example-workflows/Technique_4_Slack_CLI_Command/collaborators.yml new file mode 100644 index 00000000..46ba6ac8 --- /dev/null +++ b/example-workflows/Technique_4_Slack_CLI_Command/collaborators.yml @@ -0,0 +1,59 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json + +name: Manage a collaborator +on: + workflow_dispatch: + inputs: + email: + description: "The collaborator's email address" + required: true + type: string + add: + description: "Checked to add, unchecked to remove" + required: false + default: true + type: boolean +jobs: + collaborator: + name: Add or remove a collaborator + runs-on: ubuntu-latest + steps: + - name: Checkout the repo + uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Look up the Slack user by email + id: email + uses: slackapi/slack-github-action@v4.0.0 + with: + method: users.lookupByEmail # https://docs.slack.dev/reference/methods/users.lookupByEmail/ + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + email: ${{ inputs.email }} + + - name: Gather the display name + if: ${{ steps.email.outputs.ok }} + run: | + SLACK_USER_ID=$(echo '${{ steps.email.outputs.response }}' | jq -r '.user.id') + SLACK_DISPLAY_NAME=$(echo '${{ steps.email.outputs.response }}' | jq -r '(.user.profile.display_name | select(. != "")) // .user.real_name') + echo "SLACK_USER_ID=$SLACK_USER_ID" >> "$GITHUB_ENV" + echo "SLACK_DISPLAY_NAME=$SLACK_DISPLAY_NAME" >> "$GITHUB_ENV" + + - name: Add or remove the collaborator + if: ${{ steps.email.outputs.ok }} + uses: slackapi/slack-github-action/cli@v4.0.0 + with: + command: "collaborators ${{ inputs.add && 'add' || 'remove' }} ${{ inputs.email }} --app ${{ vars.SLACK_APP_ID }}" + token: ${{ secrets.SLACK_SERVICE_TOKEN }} + + - name: Post a confirmation message + if: ${{ steps.email.outputs.ok }} + uses: slackapi/slack-github-action@v4.0.0 + with: + errors: true + method: chat.postMessage # https://docs.slack.dev/reference/methods/chat.postMessage/ + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + channel: ${{ secrets.SLACK_CHANNEL_ID }} + text: "<@${{ env.SLACK_USER_ID }}> (${{ env.SLACK_DISPLAY_NAME }}) was ${{ inputs.add && 'added to' || 'removed from' }} the app collaborators." diff --git a/example-workflows/Technique_4_Slack_CLI_Command/deploy.yml b/example-workflows/Technique_4_Slack_CLI_Command/deploy.yml new file mode 100644 index 00000000..aab6d4a0 --- /dev/null +++ b/example-workflows/Technique_4_Slack_CLI_Command/deploy.yml @@ -0,0 +1,32 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json + +name: Deploy an app +on: + push: + branches: + - main +jobs: + deploy: + name: Deploy to Slack + runs-on: ubuntu-latest + steps: + - name: Checkout the repo + uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Deploy the app + uses: slackapi/slack-github-action/cli@v4.0.0 + with: + command: "deploy --app ${{ vars.SLACK_APP_ID }} --force" + token: ${{ secrets.SLACK_SERVICE_TOKEN }} + + - name: Post a deployment message + uses: slackapi/slack-github-action@v4.0.0 + with: + errors: true + method: chat.postMessage + token: ${{ secrets.SLACK_BOT_TOKEN }} + payload: | + channel: ${{ secrets.SLACK_CHANNEL_ID }} + text: "Deployed to Slack from ${{ github.sha }}" diff --git a/example-workflows/Technique_4_Slack_CLI_Command/manifest.yml b/example-workflows/Technique_4_Slack_CLI_Command/manifest.yml new file mode 100644 index 00000000..16048a8c --- /dev/null +++ b/example-workflows/Technique_4_Slack_CLI_Command/manifest.yml @@ -0,0 +1,20 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json + +name: Validate the app manifest +on: + pull_request: +jobs: + validate: + name: Check the app manifest + runs-on: ubuntu-latest + steps: + - name: Checkout the repo + uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Validate the manifest + uses: slackapi/slack-github-action/cli@v4.0.0 + with: + command: "manifest validate --app ${{ vars.SLACK_APP_ID }}" + token: ${{ secrets.SLACK_SERVICE_TOKEN }} diff --git a/jsconfig.json b/jsconfig.json index 05eec262..8b3bfbe9 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,19 +1,19 @@ { "compilerOptions": { "strict": true, - "baseUrl": ".", "checkJs": true, + "types": ["node"], "esModuleInterop": true, "lib": [ - "ES2023" + "ESNext" ], - "module": "node16", - "moduleResolution": "node", + "module": "nodenext", + "moduleResolution": "nodenext", "noFallthroughCasesInSwitch": true, "noImplicitReturns": true, "noUnusedLocals": true, "noUnusedParameters": true, - "target": "ES2022" + "target": "ESNext" }, "exclude": [ "node_modules" diff --git a/package-lock.json b/package-lock.json index fffce7bc..62f3917b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,108 +1,134 @@ { - "name": "slack-github-action", - "version": "2.0.0", + "name": "@slack/slack-github-action", + "version": "4.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "slack-github-action", - "version": "2.0.0", + "name": "@slack/slack-github-action", + "version": "4.0.0", "license": "MIT", "dependencies": { - "@actions/core": "^1.11.1", - "@actions/github": "^6.0.0", - "@slack/logger": "^4.0.0", - "@slack/web-api": "^7.8.0", - "axios": "^1.8.2", - "axios-retry": "^4.5.0", + "@actions/core": "^3.0.1", + "@actions/github": "^9.1.1", + "@slack/logger": "^5.0.0", + "@slack/web-api": "^8.0.0", + "@slack/webhook": "^8.0.0", "flat": "^6.0.1", - "https-proxy-agent": "^7.0.6", - "js-yaml": "^4.1.0", - "markup-js": "^1.5.21" + "js-yaml": "^5.2.1", + "markup-js": "^1.5.21", + "undici": "^8.7.0" }, "devDependencies": { - "@biomejs/biome": "^1.9.4", - "@types/chai": "^5.0.1", + "@biomejs/biome": "^2.5.4", + "@changesets/cli": "^2.31.0", "@types/flat": "^5.0.5", "@types/js-yaml": "^4.0.9", "@types/markup-js": "^1.5.0", - "@types/mocha": "^10.0.10", - "@types/node": "^22.13.7", - "@types/sinon": "^17.0.4", - "@vercel/ncc": "^0.38.3", - "c8": "^10.1.3", - "chai": "^5.1.2", - "mocha": "^11.1.0", - "mocha-suppress-logs": "^0.5.1", - "sinon": "^19.0.2", - "typescript": "^5.8.2" + "@types/node": "^24.13.2", + "@types/sinon": "^22.0.0", + "@vercel/ncc": "^0.44.1", + "sinon": "^22.0.0", + "typescript": "^7.0.2" }, "engines": { - "node": ">=20.0.0", - "npm": ">=10.2.0" + "node": ">=24.11.0", + "npm": ">=11.6.1" } }, "node_modules/@actions/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", - "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.1.tgz", + "integrity": "sha512-a6d/Nwahm9fliVGRhdhofo40HjHQasUPusmc7vBfyky+7Z+P2A1J68zyFVaNcEclc/Se+eO595oAr5nwEIoIUA==", "license": "MIT", "dependencies": { - "@actions/exec": "^1.1.1", - "@actions/http-client": "^2.0.1" + "@actions/exec": "^3.0.0", + "@actions/http-client": "^4.0.0" } }, "node_modules/@actions/exec": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", - "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-3.0.0.tgz", + "integrity": "sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==", "license": "MIT", "dependencies": { - "@actions/io": "^1.0.1" + "@actions/io": "^3.0.2" } }, "node_modules/@actions/github": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.0.tgz", - "integrity": "sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-9.1.1.tgz", + "integrity": "sha512-tL5JbYOBZHc0ngEnCsaDcryUizIUIlQyIMwy1Wkx93H5HzbBJ7TbiPx2PnFjBwZW0Vh05JmfFZhecE6gglYegA==", + "license": "MIT", + "dependencies": { + "@actions/http-client": "^3.0.2", + "@octokit/core": "^7.0.6", + "@octokit/plugin-paginate-rest": "^14.0.0", + "@octokit/plugin-rest-endpoint-methods": "^17.0.0", + "@octokit/request": "^10.0.7", + "@octokit/request-error": "^7.1.0", + "undici": "^6.23.0" + } + }, + "node_modules/@actions/github/node_modules/@actions/http-client": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.2.tgz", + "integrity": "sha512-JP38FYYpyqvUsz+Igqlc/JG6YO9PaKuvqjM3iGvaLqFnJ7TFmcLyy2IDrY0bI0qCQug8E9K+elv5ZNfw62ZJzA==", "license": "MIT", "dependencies": { - "@actions/http-client": "^2.2.0", - "@octokit/core": "^5.0.1", - "@octokit/plugin-paginate-rest": "^9.0.0", - "@octokit/plugin-rest-endpoint-methods": "^10.0.0" + "tunnel": "^0.0.6", + "undici": "^6.23.0" + } + }, + "node_modules/@actions/github/node_modules/undici": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", + "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", + "license": "MIT", + "engines": { + "node": ">=18.17" } }, "node_modules/@actions/http-client": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.0.tgz", - "integrity": "sha512-q+epW0trjVUUHboliPb4UF9g2msf+w61b32tAkFEwL/IwP0DQWgbCMM0Hbe3e3WXSKz5VcUXbzJQgy8Hkra/Lg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.0.tgz", + "integrity": "sha512-QuwPsgVMsD6qaPD57GLZi9sqzAZCtiJT8kVBCDpLtxhL5MydQ4gS+DrejtZZPdIYyB1e95uCK9Luyds7ybHI3g==", + "license": "MIT", "dependencies": { "tunnel": "^0.0.6", - "undici": "^5.25.4" + "undici": "^6.23.0" + } + }, + "node_modules/@actions/http-client/node_modules/undici": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", + "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", + "license": "MIT", + "engines": { + "node": ">=18.17" } }, "node_modules/@actions/io": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", - "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-3.0.2.tgz", + "integrity": "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==", "license": "MIT" }, - "node_modules/@bcoe/v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.1.tgz", - "integrity": "sha512-W+a0/JpU28AqH4IKtwUPcEUnUyXMDLALcn5/JLczGGT9fHE2sIby/xP/oQnx3nxkForzgzPy201RAKcB4xPAFQ==", + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "dev": true, + "license": "MIT", "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, "node_modules/@biomejs/biome": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz", - "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.4.tgz", + "integrity": "sha512-xy5FNE5kQJKyK5MR1gJy6ztXYx4WBAbYGlK04lMEgmyPRWKybY9NFwiG9yo0XdzOU8Xvhj41u034J1ywfoWfMw==", "dev": true, - "hasInstallScript": true, "license": "MIT OR Apache-2.0", "bin": { "biome": "bin/biome" @@ -115,20 +141,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "1.9.4", - "@biomejs/cli-darwin-x64": "1.9.4", - "@biomejs/cli-linux-arm64": "1.9.4", - "@biomejs/cli-linux-arm64-musl": "1.9.4", - "@biomejs/cli-linux-x64": "1.9.4", - "@biomejs/cli-linux-x64-musl": "1.9.4", - "@biomejs/cli-win32-arm64": "1.9.4", - "@biomejs/cli-win32-x64": "1.9.4" + "@biomejs/cli-darwin-arm64": "2.5.4", + "@biomejs/cli-darwin-x64": "2.5.4", + "@biomejs/cli-linux-arm64": "2.5.4", + "@biomejs/cli-linux-arm64-musl": "2.5.4", + "@biomejs/cli-linux-x64": "2.5.4", + "@biomejs/cli-linux-x64-musl": "2.5.4", + "@biomejs/cli-win32-arm64": "2.5.4", + "@biomejs/cli-win32-x64": "2.5.4" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz", - "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.4.tgz", + "integrity": "sha512-4o3NFRobXHynkgcFVrlZsoDAFtF2ldlEGN8sORSws5ZQqyY4PXnPUIylu4ksfyHuwkfvDREuWh3JK+niRwGq3w==", "cpu": [ "arm64" ], @@ -143,9 +169,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz", - "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.4.tgz", + "integrity": "sha512-D32P5HkU2Y6PySuC/WsVDTOgsDwVFmujzhhhOQjajtATpVWFDXuVd3oRbsWNSEA+aaFzyzZm22szsyydBYlSyQ==", "cpu": [ "x64" ], @@ -160,13 +186,16 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", - "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.4.tgz", + "integrity": "sha512-pSEfW7B8kTsXUjUxC1xVVK+y85Ht3C5XxZ9gclmC7/3Ku9Vqz8jmI7k0p/BNIjQ6t4sFERI2sFeH73ybiZl6YQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -177,13 +206,16 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", - "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.4.tgz", + "integrity": "sha512-Rpm5/AT1m+DlJmUoYvS4/vXc+0tXJPJ2NQz25TGPyHVF5JrWy75PE0GH6kVxsKtQDuCH4OgzquZq0R4kj/wCVg==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -194,13 +226,16 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", - "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.4.tgz", + "integrity": "sha512-FNxojWJkL7EajAuzBgoLe0T2G0y112M4lBrDIFl/DomFTx8yqenYOIdsRLNXvOvBBofE8hJi85LjzLmBDpY7/Q==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -211,13 +246,16 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", - "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.4.tgz", + "integrity": "sha512-aby/PohmmgbShcHqFsZVzG8H6D98+P+A6xRWRrQcLW1pCjabcov5UUlke4UqNQBYTkDQav+jB4zyyDDeKB2GaA==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT OR Apache-2.0", "optional": true, "os": [ @@ -228,9 +266,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", - "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.4.tgz", + "integrity": "sha512-emoXexPZIPAZkz2RKmA95WJUqK3I5MJNYtwEbL5ESciRzhmFMMyekDhNG8hpeOaK+ZGRDxAU4wvGuA5IHQ0h0w==", "cpu": [ "arm64" ], @@ -245,9 +283,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", - "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.4.tgz", + "integrity": "sha512-U1jaluLw1qQc2Tx7/CeSoL9N5XcqIH+GWjpUAy1ouB5nVjSCMNO+NNHdY3RAs8zxNurLWAdj6pehQdCA2zyU+Q==", "cpu": [ "x64" ], @@ -261,330 +299,528 @@ "node": ">=14.21.3" } }, - "node_modules/@fastify/busboy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", - "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", - "engines": { - "node": ">=14" + "node_modules/@changesets/apply-release-plan": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.1.1.tgz", + "integrity": "sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/config": "^3.1.4", + "@changesets/get-version-range-type": "^0.4.0", + "@changesets/git": "^3.0.4", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "detect-indent": "^6.0.0", + "fs-extra": "^7.0.1", + "lodash.startcase": "^4.4.0", + "outdent": "^0.5.0", + "prettier": "^2.7.1", + "resolve-from": "^5.0.0", + "semver": "^7.5.3" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "node_modules/@changesets/assemble-release-plan": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.10.tgz", + "integrity": "sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.1.4", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "semver": "^7.5.3" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "node_modules/@changesets/changelog-git": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-0.2.1.tgz", + "integrity": "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "@changesets/types": "^6.1.0" + } + }, + "node_modules/@changesets/cli": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.31.0.tgz", + "integrity": "sha512-AhI4enNTgHu2IZr6K4WZyf0EPch4XVMn1yOMFmCD9gsfBGqMYaHXls5HyDv6/CL5axVQABz68eG30eCtbr2wFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/apply-release-plan": "^7.1.1", + "@changesets/assemble-release-plan": "^6.0.10", + "@changesets/changelog-git": "^0.2.1", + "@changesets/config": "^3.1.4", + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.1.4", + "@changesets/get-release-plan": "^4.0.16", + "@changesets/git": "^3.0.4", + "@changesets/logger": "^0.1.1", + "@changesets/pre": "^2.0.2", + "@changesets/read": "^0.6.7", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@changesets/write": "^0.4.0", + "@inquirer/external-editor": "^1.0.2", + "@manypkg/get-packages": "^1.1.3", + "ansi-colors": "^4.1.3", + "enquirer": "^2.4.1", + "fs-extra": "^7.0.1", + "mri": "^1.2.0", + "package-manager-detector": "^0.2.0", + "picocolors": "^1.1.0", + "resolve-from": "^5.0.0", + "semver": "^7.5.3", + "spawndamnit": "^3.0.1", + "term-size": "^2.1.0" }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "bin": { + "changeset": "bin.js" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "node_modules/@changesets/config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@changesets/config/-/config-3.1.4.tgz", + "integrity": "sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.1.4", + "@changesets/logger": "^0.1.1", + "@changesets/should-skip-package": "^0.1.2", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "fs-extra": "^7.0.1", + "micromatch": "^4.0.8" + } + }, + "node_modules/@changesets/errors": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@changesets/errors/-/errors-0.2.0.tgz", + "integrity": "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==", + "dev": true, + "license": "MIT", + "dependencies": { + "extendable-error": "^0.1.5" + } + }, + "node_modules/@changesets/get-dependents-graph": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-2.1.4.tgz", + "integrity": "sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "picocolors": "^1.1.0", + "semver": "^7.5.3" + } + }, + "node_modules/@changesets/get-release-plan": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.16.tgz", + "integrity": "sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/assemble-release-plan": "^6.0.10", + "@changesets/config": "^3.1.4", + "@changesets/pre": "^2.0.2", + "@changesets/read": "^0.6.7", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3" + } + }, + "node_modules/@changesets/get-version-range-type": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.4.0.tgz", + "integrity": "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@changesets/git": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@changesets/git/-/git-3.0.4.tgz", + "integrity": "sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^0.2.0", + "@manypkg/get-packages": "^1.1.3", + "is-subdir": "^1.1.1", + "micromatch": "^4.0.8", + "spawndamnit": "^3.0.1" + } + }, + "node_modules/@changesets/logger": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@changesets/logger/-/logger-0.1.1.tgz", + "integrity": "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.0" + } + }, + "node_modules/@changesets/parse": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.4.3.tgz", + "integrity": "sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "js-yaml": "^4.1.1" + } + }, + "node_modules/@changesets/parse/node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@changesets/pre": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-2.0.2.tgz", + "integrity": "sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^0.2.0", + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3", + "fs-extra": "^7.0.1" } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "node_modules/@changesets/read": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.6.7.tgz", + "integrity": "sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/git": "^3.0.4", + "@changesets/logger": "^0.1.1", + "@changesets/parse": "^0.4.3", + "@changesets/types": "^6.1.0", + "fs-extra": "^7.0.1", + "p-filter": "^2.1.0", + "picocolors": "^1.1.0" + } + }, + "node_modules/@changesets/should-skip-package": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@changesets/should-skip-package/-/should-skip-package-0.1.2.tgz", + "integrity": "sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "@manypkg/get-packages": "^1.1.3" + } + }, + "node_modules/@changesets/types": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-6.1.0.tgz", + "integrity": "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==", "dev": true, "license": "MIT" }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/@changesets/write": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@changesets/write/-/write-0.4.0.tgz", + "integrity": "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^6.1.0", + "fs-extra": "^7.0.1", + "human-id": "^4.1.1", + "prettier": "^2.7.1" + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", "dev": true, "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/@manypkg/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.5.5", + "@types/node": "^12.7.1", + "find-up": "^4.1.0", + "fs-extra": "^8.1.0" + } + }, + "node_modules/@manypkg/find-root/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@manypkg/find-root/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@manypkg/get-packages": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-1.1.3.tgz", + "integrity": "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.5.5", + "@changesets/types": "^4.0.1", + "@manypkg/find-root": "^1.1.0", + "fs-extra": "^8.1.0", + "globby": "^11.0.0", + "read-yaml-file": "^1.1.0" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "node_modules/@manypkg/get-packages/node_modules/@changesets/types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-4.1.0.tgz", + "integrity": "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@manypkg/get-packages/node_modules/fs-extra": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=6 <7 || >=8" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, "engines": { - "node": ">=8" + "node": ">= 8" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, + "license": "MIT", "engines": { - "node": ">=6.0.0" + "node": ">= 8" } }, - "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 - }, - "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==", + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" } }, "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==", + "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": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/core": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.1.0.tgz", - "integrity": "sha512-BDa2VAMLSh3otEiaMJ/3Y36GU4qf6GI+VivQ/P41NC6GHcdxpKlqV0ikSZ5gdQsmS3ojXeRx5vasgNTinF0Q4g==", + "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": "^4.0.0", - "@octokit/graphql": "^7.0.0", - "@octokit/request": "^8.0.2", - "@octokit/request-error": "^5.0.0", - "@octokit/types": "^12.0.0", - "before-after-hook": "^2.2.0", - "universal-user-agent": "^6.0.0" + "@octokit/auth-token": "^6.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" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "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==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.2.tgz", + "integrity": "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ==", "license": "MIT", "dependencies": { - "@octokit/types": "^13.1.0", - "universal-user-agent": "^6.0.0" + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.2" }, "engines": { - "node": ">= 18" - } - }, - "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==", - "license": "MIT" - }, - "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==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^23.0.1" + "node": ">= 20" } }, "node_modules/@octokit/graphql": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.2.tgz", - "integrity": "sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==", + "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": "^8.0.1", - "@octokit/types": "^12.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "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==", + "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/plugin-paginate-rest": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz", - "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", + "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/types": "^12.6.0" + "@octokit/types": "^16.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" }, "peerDependencies": { - "@octokit/core": "5" + "@octokit/core": ">=6" } }, "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.2.0.tgz", - "integrity": "sha512-ePbgBMYtGoRNXDyKGvr9cyHjQ163PbwD0y1MkDJCpkO2YH4OeXX40c4wYHKikHGZcpGPbcRLuy0unPUuafco8Q==", + "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": "^12.3.0" + "@octokit/types": "^16.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" }, "peerDependencies": { - "@octokit/core": ">=5" + "@octokit/core": ">=6" } }, "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==", + "version": "10.0.7", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.7.tgz", + "integrity": "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA==", "license": "MIT", "dependencies": { - "@octokit/endpoint": "^9.0.6", - "@octokit/request-error": "^5.1.1", - "@octokit/types": "^13.1.0", - "universal-user-agent": "^6.0.0" + "@octokit/endpoint": "^11.0.2", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "fast-content-type-parse": "^3.0.0", + "universal-user-agent": "^7.0.2" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "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==", + "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/types": "^13.1.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" + "@octokit/types": "^16.0.0" }, "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/request-error/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/request-error/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/request/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/request/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/types": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", - "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "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": "^20.0.0" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" + "@octokit/openapi-types": "^27.0.0" } }, "node_modules/@sinonjs/commons": { @@ -598,9 +834,9 @@ } }, "node_modules/@sinonjs/fake-timers": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.2.tgz", - "integrity": "sha512-4Bb+oqXZTSTZ1q27Izly9lv8B9dlV61CROxPiVtywwzv5SnytJqhvYe6FclHYuXml4cd1VHPo1zd5PmTeJozvA==", + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -608,14 +844,13 @@ } }, "node_modules/@sinonjs/samsam": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", - "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-10.0.2.tgz", + "integrity": "sha512-8lVwD1Df1BmzoaOLhMcGGcz/Jyr5QY2KSB75/YK1QgKzoabTeLdIVyhXNZK9ojfSKSdirbXqdbsXXqP9/Ve8+A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.1", - "lodash.get": "^4.4.2", "type-detect": "^4.1.0" } }, @@ -629,77 +864,66 @@ "node": ">=4" } }, - "node_modules/@sinonjs/text-encoding": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", - "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", - "dev": true, - "license": "(Unlicense OR Apache-2.0)" - }, "node_modules/@slack/logger": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-4.0.0.tgz", - "integrity": "sha512-Wz7QYfPAlG/DR+DfABddUZeNgoeY7d1J39OCR2jR+v7VBsB8ezulDK5szTnDDPDwLH5IWhLvXIHlCFZV7MSKgA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-5.0.0.tgz", + "integrity": "sha512-VGXhmmgsAo9shdQYh4tFDndd+7nsgp0Y5h0UPDaUp8K359pBasI6YdkMqFW3mCOxLQkq09qj7o7cq6f3DuXcJQ==", "license": "MIT", "dependencies": { - "@types/node": ">=18.0.0" + "@types/node": ">=20" }, "engines": { - "node": ">= 18", - "npm": ">= 8.6.0" + "node": ">= 20", + "npm": ">=9.6.4" } }, "node_modules/@slack/types": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/@slack/types/-/types-2.12.0.tgz", - "integrity": "sha512-yFewzUomYZ2BYaGJidPuIgjoYj5wqPDmi7DLSaGIkf+rCi4YZ2Z3DaiYIbz7qb/PL2NmamWjCvB7e9ArI5HkKg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@slack/types/-/types-3.0.0.tgz", + "integrity": "sha512-KNOqpnNAlsFt5Jk9XBclslQ0lobRIg/0tnhpmvZJAglHJx9E8oceN8hC3gaBzkR6UzQ9Wzq4rLsJ98wUcxWPfw==", "license": "MIT", "engines": { - "node": ">= 12.13.0", - "npm": ">= 6.12.0" + "node": ">= 20", + "npm": ">=9.6.4" } }, "node_modules/@slack/web-api": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.8.0.tgz", - "integrity": "sha512-d4SdG+6UmGdzWw38a4sN3lF/nTEzsDxhzU13wm10ejOpPehtmRoqBKnPztQUfFiWbNvSb4czkWYJD4kt+5+Fuw==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-8.0.0.tgz", + "integrity": "sha512-ORx3XQryQPq2Jnxv5giSKXVoQRUeylrrymIR2S9fPzLjPcCts8RayMeBSZMcpfpAqp6fnBRuPW2UB6dUPUTEZA==", "license": "MIT", "dependencies": { - "@slack/logger": "^4.0.0", - "@slack/types": "^2.9.0", - "@types/node": ">=18.0.0", + "@slack/logger": "^5.0.0", + "@slack/types": "^3.0.0", + "@types/node": ">=20", "@types/retry": "0.12.0", - "axios": "^1.7.8", "eventemitter3": "^5.0.1", - "form-data": "^4.0.0", - "is-electron": "2.2.2", - "is-stream": "^2", "p-queue": "^6", "p-retry": "^4", "retry": "^0.13.1" }, "engines": { - "node": ">= 18", - "npm": ">= 8.6.0" + "node": ">= 20", + "npm": ">=9.6.4" } }, - "node_modules/@types/chai": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.0.1.tgz", - "integrity": "sha512-5T8ajsg3M/FOncpLYW7sdOcD6yf4+722sze/tc4KQV0P8Z2rAr3SAuHCIkYmYpt8VbcQlnz8SxlOlPQYefe4cA==", - "dev": true, + "node_modules/@slack/webhook": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@slack/webhook/-/webhook-8.0.0.tgz", + "integrity": "sha512-j3VhO6u1rZYDvY2LXDzymGQGHW4NqWLyKkOgrXeE20CsrI39j0E5gHuss+PgVDqSBx4OTUKccF/Vcm9ICC4S5w==", "license": "MIT", "dependencies": { - "@types/deep-eql": "*" + "@slack/types": "^3.0.0", + "@types/node": ">=20", + "@types/retry": "0.12.0", + "p-retry": "^4", + "retry": "^0.13.1" + }, + "engines": { + "node": ">= 20", + "npm": ">=9.6.4" } }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/flat": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/@types/flat/-/flat-5.0.5.tgz", @@ -707,13 +931,6 @@ "dev": true, "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, - "license": "MIT" - }, "node_modules/@types/js-yaml": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", @@ -728,19 +945,13 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/mocha": { - "version": "10.0.10", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", - "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", - "dev": true - }, "node_modules/@types/node": { - "version": "22.13.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.7.tgz", - "integrity": "sha512-oU2q+BsQldB9lYxHNp/5aZO+/Bs0Usa74Abo9mAKulz4ahQyXRHK6UVKYIN8KSC8HXwhWSi7b49JnX+txuac0w==", + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/retry": { @@ -750,9 +961,9 @@ "license": "MIT" }, "node_modules/@types/sinon": { - "version": "17.0.4", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz", - "integrity": "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-22.0.0.tgz", + "integrity": "sha512-TDbVpbccc2HfiqHR09Argj3mHV1KMW7sCCKj52fsl8lbRLkEn7fB1966EWhOKWUBcqfBueZuPoA7/OK1CKiy3g==", "dev": true, "license": "MIT", "dependencies": { @@ -766,373 +977,428 @@ "dev": true, "license": "MIT" }, - "node_modules/@vercel/ncc": { - "version": "0.38.3", - "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.3.tgz", - "integrity": "sha512-rnK6hJBS6mwc+Bkab+PGPs9OiS0i/3kdTO+CkI8V0/VrW3vmz7O2Pxjw/owOlmo6PKEIxRSeZKv/kuL9itnpYA==", + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "bin": { - "ncc": "dist/ncc/cli.js" - } - }, - "node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">= 14" + "node": ">=16.20.0" } }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=6" + "node": ">=16.20.0" } }, - "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==", + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=8" + "node": ">=16.20.0" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=16.20.0" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">= 8" + "node": ">=16.20.0" } }, - "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==", - "license": "Python-2.0" - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": ">=16.20.0" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/axios": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", - "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" } }, - "node_modules/axios-retry": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", - "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, "license": "Apache-2.0", - "dependencies": { - "is-retry-allowed": "^2.2.0" - }, - "peerDependencies": { - "axios": "0.x || 1.x" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" } }, - "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 + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.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==" + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.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==", + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=16.20.0" } }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": ">=16.20.0" } }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "ISC" + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } }, - "node_modules/c8": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", - "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@bcoe/v8-coverage": "^1.0.1", - "@istanbuljs/schema": "^0.1.3", - "find-up": "^5.0.0", - "foreground-child": "^3.1.1", - "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.1.6", - "test-exclude": "^7.0.1", - "v8-to-istanbul": "^9.0.0", - "yargs": "^17.7.2", - "yargs-parser": "^21.1.1" - }, - "bin": { - "c8": "bin/c8.js" - }, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=18" - }, - "peerDependencies": { - "monocart-coverage-reports": "^2" - }, - "peerDependenciesMeta": { - "monocart-coverage-reports": { - "optional": true - } + "node": ">=16.20.0" } }, - "node_modules/c8/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=16.20.0" } }, - "node_modules/c8/node_modules/test-exclude": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", - "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^9.0.4" - }, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=18" + "node": ">=16.20.0" } }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=16.20.0" } }, - "node_modules/chai": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", - "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12" + "node": ">=16.20.0" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=16.20.0" } }, - "node_modules/check-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "node_modules/@vercel/ncc": { + "version": "0.44.1", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.44.1.tgz", + "integrity": "sha512-cUjIE5P2YY1n+Kt9rFIazMMpGoPn1Fic04rOmTkElMkiDP5oszGfERMpo2shVkFKDL7rVppdM2pqJKC59shQWQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 16" + "bin": { + "ncc": "dist/ncc/cli.js" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "node": ">=6" } }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "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": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=8" } }, - "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "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==", + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.8" + "node": ">=8" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "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==", + "license": "Apache-2.0" + }, + "node_modules/better-path-resolve": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/better-path-resolve/-/better-path-resolve-1.0.0.tgz", + "integrity": "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==", "dev": true, + "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "is-windows": "^1.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=4" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "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==", + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", "dependencies": { - "delayed-stream": "~1.0.0" + "fill-range": "^7.1.1" }, "engines": { - "node": ">= 0.8" + "node": ">=8" } }, - "node_modules/convert-source-map": { - "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==", + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", "dev": true, "license": "MIT" }, @@ -1151,252 +1417,175 @@ "node": ">= 8" } }, - "node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" + "node": ">=8" } }, - "node_modules/deprecation": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" - }, "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/emoji-regex": { - "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 - }, - "node_modules/escalade": { - "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" - } - }, - "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" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "license": "MIT" - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, "license": "MIT", "dependencies": { - "to-regex-range": "^5.0.1" + "path-type": "^4.0.0" }, "engines": { "node": ">=8" } }, - "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==", + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", "dev": true, + "license": "MIT", "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8.6" } }, - "node_modules/flat": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/flat/-/flat-6.0.1.tgz", - "integrity": "sha512-/3FfIa8mbrg3xE7+wAhWeV+bd7L2Mof+xtZb5dRDKZ+wDvYJK4WDYeIOuOhre5Yv5aQObZrlbRmk3RTSiuQBtw==", - "license": "BSD-3-Clause", + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", "bin": { - "flat": "cli.js" + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" }, "engines": { - "node": ">=18" + "node": ">=4" } }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/extendable-error": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.7.tgz", + "integrity": "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==", + "dev": true, + "license": "MIT" + }, + "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", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", "funding": [ { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" } ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } + "license": "MIT" }, - "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, + "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=8.6.0" } }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "node_modules/fastq": { + "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": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" + "reusify": "^1.0.4" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "to-regex-range": "^5.0.1" + }, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=8" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "node_modules/find-up": { + "version": "4.1.0", + "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" + }, "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": ">=8" } }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "license": "MIT", + "node_modules/flat": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/flat/-/flat-6.0.1.tgz", + "integrity": "sha512-/3FfIa8mbrg3xE7+wAhWeV+bd7L2Mof+xtZb5dRDKZ+wDvYJK4WDYeIOuOhre5Yv5aQObZrlbRmk3RTSiuQBtw==", + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + }, "engines": { - "node": "*" + "node": ">=18" } }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, + "license": "MIT", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": ">=6 <7 || >=8" } }, "node_modules/glob-parent": { @@ -1412,77 +1601,71 @@ "node": ">= 6" } }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.1" + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "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==", "dev": true, - "engines": { - "node": ">=8" - } + "license": "ISC" }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "node_modules/human-id": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.1.3.tgz", + "integrity": "sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==", "dev": true, "license": "MIT", "bin": { - "he": "bin/he" + "human-id": "dist/cli.js" } }, - "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 - }, - "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==", + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">= 14" + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "node_modules/ignore": { + "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", - "dependencies": { - "binary-extensions": "^2.0.0" - }, "engines": { - "node": ">=8" + "node": ">= 4" } }, - "node_modules/is-electron": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz", - "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==", - "license": "MIT" - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1493,15 +1676,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "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, - "engines": { - "node": ">=8" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1525,196 +1699,87 @@ "node": ">=0.12.0" } }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "node_modules/is-subdir": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-subdir/-/is-subdir-1.2.0.tgz", + "integrity": "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-retry-allowed": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", - "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", - "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "better-path-resolve": "1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "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, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report/node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", - "dev": true, - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jackspeak": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.2.tgz", - "integrity": "sha512-qH3nOSj8q/8+Eg8LUPOq3C+6HWkpUioIjDsq1+D4zY91oZvpPttw8GwtF1nReRYKXl+1AORyFqtm2f5Q1SB6/Q==", "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "14 >=14.21 || 16 >=16.20 || >=18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } + "license": "ISC" }, "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": "5.2.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.1.tgz", + "integrity": "sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, "bin": { - "js-yaml": "bin/js-yaml.js" + "js-yaml": "bin/js-yaml.mjs" } }, - "node_modules/just-extend": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", - "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", - "dev": true, - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" + "p-locate": "^4.1.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/loupe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", - "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.1" - } + "license": "MIT" }, "node_modules/markup-js": { "version": "1.5.21", @@ -1724,151 +1789,58 @@ "markup-js": "src/markup.js" } }, - "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==", - "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==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mocha": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz", - "integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.3", - "browser-stdout": "^1.3.1", - "chokidar": "^3.5.3", - "debug": "^4.3.5", - "diff": "^5.2.0", - "escape-string-regexp": "^4.0.0", - "find-up": "^5.0.0", - "glob": "^10.4.5", - "he": "^1.2.0", - "js-yaml": "^4.1.0", - "log-symbols": "^4.1.0", - "minimatch": "^5.1.6", - "ms": "^2.1.3", - "serialize-javascript": "^6.0.2", - "strip-json-comments": "^3.1.1", - "supports-color": "^8.1.1", - "workerpool": "^6.5.1", - "yargs": "^17.7.2", - "yargs-parser": "^21.1.1", - "yargs-unparser": "^2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/mocha-suppress-logs": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mocha-suppress-logs/-/mocha-suppress-logs-0.5.1.tgz", - "integrity": "sha512-f4BhMiCABgCt3tlXiOcRydWreNCkfvgXgNL2ZclfXPdLNcY7jfyNy3Oi5wwPuxx++UyuNiIx3F7orvckAfrKzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^2.1.2" - } - }, - "node_modules/mocha/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "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", - "dependencies": { - "has-flag": "^4.0.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "node": ">= 8" } }, - "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==" - }, - "node_modules/nise": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", - "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^13.0.1", - "@sinonjs/text-encoding": "^0.7.3", - "just-extend": "^6.2.0", - "path-to-regexp": "^8.1.0" + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "node_modules/outdent": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz", + "integrity": "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-filter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz", + "integrity": "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==", + "dev": true, + "license": "MIT", "dependencies": { - "wrappy": "1" + "p-map": "^2.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/p-finally": { @@ -1881,33 +1853,42 @@ } }, "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, + "license": "MIT", "dependencies": { - "yocto-queue": "^0.1.0" + "p-try": "^2.0.0" }, "engines": { - "node": ">=10" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "version": "4.1.0", + "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": "^3.0.2" + "p-limit": "^2.2.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" } }, "node_modules/p-queue": { @@ -1957,18 +1938,32 @@ "node": ">=8" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-manager-detector": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz", + "integrity": "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==", "dev": true, - "license": "BlueOak-1.0.0" + "license": "MIT", + "dependencies": { + "quansync": "^0.2.7" + } }, "node_modules/path-exists": { "version": "4.0.0", "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" } @@ -1978,102 +1973,153 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, + "license": "MIT", "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=8" } }, - "node_modules/path-scurry/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==", + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, - "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pathval": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true, "license": "MIT", "engines": { - "node": ">= 14.16" + "node": ">=6" } }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, "engines": { - "node": ">=8.6" + "node": ">=10.13.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/proxy-from-env": { + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/read-yaml-file": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "resolved": "https://registry.npmjs.org/read-yaml-file/-/read-yaml-file-1.1.0.tgz", + "integrity": "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.5", + "js-yaml": "^3.6.1", + "pify": "^4.0.1", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "node_modules/read-yaml-file/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "license": "MIT", "dependencies": { - "safe-buffer": "^5.1.0" + "sprintf-js": "~1.0.2" } }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "node_modules/read-yaml-file/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { - "picomatch": "^2.2.1" + "argparse": "^1.0.7", + "esprima": "^4.0.0" }, - "engines": { - "node": ">=8.10.0" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/retry": { @@ -2085,10 +2131,21 @@ "node": ">= 4" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { @@ -2104,12 +2161,22 @@ "url": "https://feross.org/support" } ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, "license": "MIT" }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -2119,21 +2186,12 @@ "node": ">=10" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, "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" }, @@ -2146,6 +2204,7 @@ "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" } @@ -2155,6 +2214,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, + "license": "ISC", "engines": { "node": ">=14" }, @@ -2163,69 +2223,56 @@ } }, "node_modules/sinon": { - "version": "19.0.2", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz", - "integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-22.0.0.tgz", + "integrity": "sha512-sq/6DpdXOrLyfbKlXLg/Usc7xu8YXPeLkOFZRvA3bNUSA2lhbrZ06yuXbH1fkzBPCbz9O10+7hznzUsjaYNm0Q==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^13.0.2", - "@sinonjs/samsam": "^8.0.1", - "diff": "^7.0.0", - "nise": "^6.1.1", - "supports-color": "^7.2.0" + "@sinonjs/fake-timers": "^15.4.0", + "@sinonjs/samsam": "^10.0.2", + "diff": "^9.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/sinon" } }, - "node_modules/sinon/node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "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": "BSD-3-Clause", + "license": "MIT", "engines": { - "node": ">=0.3.1" + "node": ">=8" } }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/spawndamnit": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spawndamnit/-/spawndamnit-3.0.1.tgz", + "integrity": "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==", "dev": true, + "license": "SEE LICENSE IN LICENSE", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" + "cross-spawn": "^7.0.5", + "signal-exit": "^4.0.1" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } + "license": "BSD-3-Clause" }, "node_modules/strip-ansi": { "version": "6.0.1", "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" }, @@ -2233,24 +2280,20 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "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", - "dependencies": { - "ansi-regex": "^5.0.1" - }, "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/strip-json-comments": { - "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==", + "node_modules/term-size": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", + "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", "dev": true, "license": "MIT", "engines": { @@ -2260,18 +2303,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2289,6 +2320,7 @@ "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "license": "MIT", "engines": { "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } @@ -2304,54 +2336,69 @@ } }, "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", "dev": true, "license": "Apache-2.0", "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "tsc": "bin/tsc" }, "engines": { - "node": ">=14.17" + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" } }, "node_modules/undici": { - "version": "5.28.5", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.5.tgz", - "integrity": "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.7.0.tgz", + "integrity": "sha512-N7iQtfyLhIMOFgQubvmLV26svHpO0bqKnAiWotTQCVKCmWrcGbBotPuW1x+xwYZ2VHdSTVUfPQQnlEt1/LouTQ==", "license": "MIT", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, "engines": { - "node": ">=14.0" + "node": ">=22.19.0" } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.7.0.tgz", + "integrity": "sha512-gbsS+hAjHg9iV+T8XWdFqnOZEk4f5xrrX3eb9Y1GDe+B5u9H68P6SzYXXUw/rkRvJHRgKIdNfAcrcVj/JvayVA==", + "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/v8-to-istanbul": { - "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==", + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, + "license": "MIT", "engines": { - "node": ">=10.12.0" + "node": ">= 4.0.0" } }, "node_modules/which": { @@ -2359,6 +2406,7 @@ "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" }, @@ -2368,131 +2416,6 @@ "engines": { "node": ">= 8" } - }, - "node_modules/workerpool": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", - "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "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", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "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", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "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==" - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "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", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "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" - } - }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-unparser/node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } } } } diff --git a/package.json b/package.json index c141538f..7f52ab59 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,25 @@ { - "name": "slack-github-action", - "version": "2.0.0", - "description": "The official Slack Github Action. Use this to send data into your Slack workspace", + "name": "@slack/slack-github-action", + "version": "4.0.0", + "private": true, + "description": "The official Slack GitHub Action. Use this to send data into your Slack workspace", "main": "dist/index.js", + "files": [ + "action.yml", + "cli/", + "dist/" + ], "type": "module", "scripts": { "build": "ncc build src/index.js --license licenses.txt --source-map", - "check": "tsc --noemit --module es2022 --project ./jsconfig.json", - "dev": "act public --eventpath .github/resources/.actions/event.json --secret-file .github/resources/.env --platform ubuntu-latest=node:20-buster --container-architecture linux/amd64", - "lint:fix": "biome check --write", + "changeset": "npx @changesets/cli", + "check": "tsc --noemit --project ./jsconfig.json", + "dev": "act public --eventpath .github/resources/.actions/event.json --secret-file .github/resources/.env --platform ubuntu-latest=node:24 --container-architecture linux/amd64", + "docs": "TAG=\"v$(npm pkg get version | jq -r)\" && grep -rl 'slackapi/slack-github-action@v' ./docs ./example-workflows | xargs sed -i \"s|slackapi/slack-github-action@v.*|slackapi/slack-github-action@${TAG}|g\" && grep -rl 'slackapi/slack-github-action/cli@v' ./docs ./example-workflows | xargs sed -i \"s|slackapi/slack-github-action/cli@v.*|slackapi/slack-github-action/cli@${TAG}|g\"", "lint": "biome check", - "test": "c8 mocha test/*.spec.js" + "lint:fix": "biome check --write", + "test": "node --test --experimental-test-coverage --test-reporter=spec --test-reporter-destination=stdout --test-reporter=lcov --test-reporter-destination=test/coverage.txt test/*.spec.js", + "version": "npm run changeset version && npm install && npm run docs" }, "repository": { "type": "git", @@ -28,37 +37,34 @@ "url": "https://github.com/slackapi/slack-github-action/issues" }, "engines": { - "node": ">=20.0.0", - "npm": ">=10.2.0" + "node": ">=24.11.0", + "npm": ">=11.6.1" }, - "homepage": "https://github.com/slackapi/slack-github-action#readme", + "homepage": "https://docs.slack.dev/tools/slack-github-action/", "dependencies": { - "@actions/core": "^1.11.1", - "@actions/github": "^6.0.0", - "@slack/logger": "^4.0.0", - "@slack/web-api": "^7.8.0", - "axios": "^1.8.2", - "axios-retry": "^4.5.0", + "@actions/core": "^3.0.1", + "@actions/github": "^9.1.1", + "@slack/logger": "^5.0.0", + "@slack/web-api": "^8.0.0", + "@slack/webhook": "^8.0.0", "flat": "^6.0.1", - "https-proxy-agent": "^7.0.6", - "js-yaml": "^4.1.0", - "markup-js": "^1.5.21" + "js-yaml": "^5.2.1", + "markup-js": "^1.5.21", + "undici": "^8.7.0" + }, + "overrides": { + "undici-types": "$undici" }, "devDependencies": { - "@biomejs/biome": "^1.9.4", - "@types/chai": "^5.0.1", + "@biomejs/biome": "^2.5.4", + "@changesets/cli": "^2.31.0", "@types/flat": "^5.0.5", "@types/js-yaml": "^4.0.9", "@types/markup-js": "^1.5.0", - "@types/mocha": "^10.0.10", - "@types/node": "^22.13.7", - "@types/sinon": "^17.0.4", - "@vercel/ncc": "^0.38.3", - "c8": "^10.1.3", - "chai": "^5.1.2", - "mocha": "^11.1.0", - "mocha-suppress-logs": "^0.5.1", - "sinon": "^19.0.2", - "typescript": "^5.8.2" + "@types/node": "^24.13.2", + "@types/sinon": "^22.0.0", + "@vercel/ncc": "^0.44.1", + "sinon": "^22.0.0", + "typescript": "^7.0.2" } } diff --git a/src/client.js b/src/client.js index e476a885..7d48f2a4 100644 --- a/src/client.js +++ b/src/client.js @@ -1,14 +1,14 @@ import webapi from "@slack/web-api"; -import { HttpsProxyAgent } from "https-proxy-agent"; import Config from "./config.js"; import SlackError from "./errors.js"; +import { fetch } from "./proxies.js"; /** * The Client class creates a WebClient from @slack/web-api for use when calling * various Slack API methods. * - * @see {@link https://tools.slack.dev/node-slack-sdk/web-api/} - * @see {@link https://api.slack.com/methods/} + * @see {@link https://docs.slack.dev/tools/node-slack-sdk/web-api/} + * @see {@link https://docs.slack.dev/reference/methods/} */ export default class Client { /** @@ -23,7 +23,8 @@ export default class Client { throw new SlackError(config.core, "No token was provided to post with"); } const client = new config.webapi.WebClient(config.inputs.token, { - agent: this.proxies(config)?.httpsAgent, + fetch: fetch(config), + allowAbsoluteUrls: false, logger: config.logger, retryConfig: this.retries(config.inputs.retries), slackApiUrl: config.inputs.api || undefined, @@ -71,28 +72,6 @@ export default class Client { } } - /** - * Return configurations for https proxy options if these are set. - * @param {Config} config - * @returns {import("axios").AxiosRequestConfig | undefined} - * @see {@link https://github.com/slackapi/slack-github-action/pull/205} - */ - proxies(config) { - const proxy = config.inputs.proxy; - try { - if (!proxy) { - return undefined; - } - return { - httpsAgent: new HttpsProxyAgent(proxy), - }; - } catch (/** @type {any} */ err) { - throw new SlackError(config.core, "Failed to configure the HTTPS proxy", { - cause: err, - }); - } - } - /** * Return configurations for retry options with different delays. * @param {string} option diff --git a/src/config.js b/src/config.js index 8ba45024..6413079f 100644 --- a/src/config.js +++ b/src/config.js @@ -1,6 +1,6 @@ -import core from "@actions/core"; import webapi from "@slack/web-api"; -import axios from "axios"; +import webhook from "@slack/webhook"; +import packageJson from "../package.json" with { type: "json" }; import Content from "./content.js"; import SlackError from "./errors.js"; import Logger from "./logger.js"; @@ -44,11 +44,11 @@ export default class Config { * @property {boolean} errors - If the job should exit after errors or succeed. * @property {string?} method - The Slack API method to call. * @property {string?} payload - Request contents from the provided input. - * @property {string?} payloadDelimiter - Seperators of nested attributes. + * @property {string?} payloadDelimiter - Separators of nested attributes. * @property {string?} payloadFilePath - Location of a JSON request payload. * @property {boolean} payloadTemplated - If templated values are replaced. * @property {string?} proxy - An optional proxied connection for requests. - * @property {Retries} retries - The retries method to use for failed requests. + * @property {string} retries - The retries method to use for failed requests. * @property {string?} token - The authentication value used with the Slack API. * @property {string?} webhook - A location for posting request payloads. * @property {string?} webhookType - Posting method to use with the webhook. @@ -59,11 +59,6 @@ export default class Config { */ inputs; - /** - * @type {import("axios").AxiosStatic} - The axios client. - */ - axios; - /** * @type {Content} - The parsed payload data to send. */ @@ -71,7 +66,7 @@ export default class Config { /** * Shared utilities specific to the GitHub action workflow. - * @type {import("@actions/core")} + * @type {typeof import("@actions/core")} */ core; @@ -82,10 +77,15 @@ export default class Config { logger; /** - * @type {import("@slack/web-api")} - Slack API client. + * @type {typeof import("@slack/web-api")} - Slack API client. */ webapi; + /** + * @type {typeof import("@slack/webhook")} - Slack webhook client. + */ + webhook; + /** * Gather values from the job inputs and use defaults or error for the missing * ones. @@ -94,13 +94,13 @@ export default class Config { * kept for later use. * * @constructor - * @param {core} core - GitHub Actions core utilities. + * @param {typeof import("@actions/core")} core - GitHub Actions core utilities. */ constructor(core) { - this.axios = axios; this.core = core; this.logger = new Logger(core).logger; this.webapi = webapi; + this.webhook = webhook; this.inputs = { api: core.getInput("api"), errors: core.getBooleanInput("errors"), @@ -120,6 +120,7 @@ export default class Config { core.getInput("webhook") || process.env.SLACK_WEBHOOK_URL || null, webhookType: core.getInput("webhook-type"), }; + this.instrument(); this.mask(); this.validate(core); core.debug(`Gathered action inputs: ${JSON.stringify(this.inputs)}`); @@ -127,23 +128,37 @@ export default class Config { core.debug(`Parsed request content: ${JSON.stringify(this.content)}`); } + /** + * Add user agent metadata for instrumentation. + */ + instrument() { + this.webapi.addAppMetadata({ + name: packageJson.name, + version: packageJson.version, + }); + this.webhook.addAppMetadata({ + name: packageJson.name, + version: packageJson.version, + }); + } + /** * Hide secret values provided in the inputs from appearing. */ mask() { if (this.inputs.token) { - core.debug("Setting the provided token as a secret variable."); - core.setSecret(this.inputs.token); + this.core.debug("Setting the provided token as a secret variable."); + this.core.setSecret(this.inputs.token); } if (this.inputs.webhook) { - core.debug("Setting the provided webhook as a secret variable."); - core.setSecret(this.inputs.webhook); + this.core.debug("Setting the provided webhook as a secret variable."); + this.core.setSecret(this.inputs.webhook); } } /** * Confirm the configurations are correct enough to continue. - * @param {core} core - GitHub Actions core utilities. + * @param {typeof import("@actions/core")} core - GitHub Actions core utilities. */ validate(core) { switch (this.inputs.retries.trim().toUpperCase()) { diff --git a/src/content.js b/src/content.js index 6594a32d..52dd4e81 100644 --- a/src/content.js +++ b/src/content.js @@ -1,8 +1,8 @@ import fs from "node:fs"; import path from "node:path"; -import github from "@actions/github"; +import * as github from "@actions/github"; import { flatten } from "flat"; -import yaml from "js-yaml"; +import { JSON_SCHEMA, load } from "js-yaml"; import markup from "markup-js"; import Config from "./config.js"; import SlackError from "./errors.js"; @@ -12,6 +12,12 @@ import SlackError from "./errors.js"; * of sending a payload. */ export default class Content { + /** + * Provided inputs as values to be sent from content provided to the action. + * @type {any} + */ + values; + /** * Gather content from the provided payload or payload file path with parsings. * @param {Config} config @@ -37,6 +43,9 @@ export default class Content { this.values = github.context; break; } + if (config.inputs.payloadTemplated) { + this.values = this.templatize(this.values); + } if (config.inputs.payloadDelimiter) { this.values = flatten(this.values, { delimiter: config.inputs.payloadDelimiter, @@ -55,6 +64,7 @@ export default class Content { * @returns {Content} - the parsed JSON payload to use in requests. */ getContentPayload(config) { + const errors = []; if (!config.inputs.payload) { throw new SlackError( config.core, @@ -62,17 +72,15 @@ export default class Content { ); } try { - const input = this.templatize(config, config.inputs.payload); const content = /** @type {Content} */ ( - yaml.load(input, { - schema: yaml.JSON_SCHEMA, + load(config.inputs.payload, { + schema: JSON_SCHEMA, }) ); return /** @type {Content} */ (content); } catch (error) { if (error instanceof Error) { - config.core.debug("Failed to parse input payload as YAML"); - config.core.debug(error.message); + errors.push(error); } } try { @@ -90,11 +98,12 @@ export default class Content { } return JSON.parse(trimmed); } catch (/** @type {any} */ error) { + errors.unshift(error); throw new SlackError( config.core, "Invalid input! Failed to parse contents of the provided payload", { - cause: error, + cause: { values: errors }, }, ); } @@ -118,18 +127,17 @@ export default class Content { path.resolve(config.inputs.payloadFilePath), "utf-8", ); - const content = this.templatize(config, input); if ( config.inputs.payloadFilePath.endsWith("yaml") || config.inputs.payloadFilePath.endsWith("yml") ) { - const load = yaml.load(content, { - schema: yaml.JSON_SCHEMA, + const content = load(input, { + schema: JSON_SCHEMA, }); - return /** @type {Content} */ (load); + return /** @type {Content} */ (content); } if (config.inputs.payloadFilePath.endsWith("json")) { - return JSON.parse(content); + return JSON.parse(input); } throw new SlackError( config.core, @@ -140,27 +148,39 @@ export default class Content { config.core, "Invalid input! Failed to parse contents of the provided payload file", { - cause: error, + cause: { values: [error] }, }, ); } } /** - * Replace templated variables in the provided content if requested. - * @param {Config} config - * @param {string} input - The initial value of the content. - * @returns {string} Content with templatized variables replaced. + * Replace templated variables in the provided content as requested. + * @param {unknown} input - The initial value of the content. + * @returns {unknown} Content with templatized variables replaced. */ - templatize(config, input) { - if (!config.inputs.payloadTemplated) { - return input; + templatize(input) { + if (Array.isArray(input)) { + return input.map((v) => this.templatize(v)); + } + if (input && typeof input === "object") { + /** + * @type {Record} + */ + const out = {}; + for (const [k, v] of Object.entries(input)) { + out[k] = this.templatize(v); + } + return out; + } + if (typeof input === "string") { + const template = input.replace(/\$\{\{/g, "{{"); // swap ${{ for {{ + const context = { + env: process.env, + github: github.context, + }; + return markup.up(template, context); } - const template = input.replace(/\$\{\{/g, "{{"); // swap ${{ for {{ - const context = { - env: process.env, - github: github.context, - }; - return markup.up(template, context); + return input; } } diff --git a/src/errors.js b/src/errors.js index b2d4ce0c..ea14f036 100644 --- a/src/errors.js +++ b/src/errors.js @@ -1,4 +1,7 @@ -import core from "@actions/core"; +/** + * @typedef Cause + * @property {Error[]} [values] - Caught exceptions. + */ /** * SlackError is a custom error wrapper for known errors of Slack GitHub Action. @@ -6,11 +9,11 @@ import core from "@actions/core"; export default class SlackError extends Error { /** * @typedef Options - * @property {Error} [cause] - thrown exception of this error. + * @property {Cause} [cause] - Reason for an error. */ /** - * @param {core} _core - GitHub Actions core utilities. + * @param {typeof import("@actions/core")} _core - GitHub Actions core utilities. * @param {any} error - The error message to throw. * @param {Options} options - configurations of erroring. */ diff --git a/src/index.js b/src/index.js index fcdf2277..48cb1968 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ -import core from "@actions/core"; +import * as core from "@actions/core"; import send from "./send.js"; /** @@ -6,13 +6,21 @@ import send from "./send.js"; * from the send.js file for testing purposes. */ try { - send(core); + await send(core); } catch (error) { if (error instanceof Error) { core.error(error.message); - core.debug(`${error.name} cause: ${error.cause}`); - core.debug(`${error.name} stack: ${error.stack}`); + /** @type {import('./errors.js').Cause} */ + const causes = /** @type {any} */ (error.cause); + if (causes?.values) { + for (const cause of causes.values) { + core.info(`${cause.stack}`); + } + } else { + core.info(`${error.stack}`); + } } else { core.error(`${error}`); } + throw error; } diff --git a/src/logger.js b/src/logger.js index 79a26012..faab2c22 100644 --- a/src/logger.js +++ b/src/logger.js @@ -3,7 +3,7 @@ import { LogLevel } from "@slack/logger"; /** * The Logger class creates a Logger to output debug messages and errors. * - * @see {@link https://tools.slack.dev/node-slack-sdk/web-api/#logging} + * @see {@link https://docs.slack.dev/tools/node-slack-sdk/web-api/#logging} */ export default class Logger { /** @@ -14,7 +14,7 @@ export default class Logger { /** * Shared utilities specific to the GitHub action workflow. - * @param {import("@actions/core")} core - GitHub Actions core utilities. + * @param {typeof import("@actions/core")} core - GitHub Actions core utilities. */ constructor(core) { this.logger = { diff --git a/src/proxies.js b/src/proxies.js new file mode 100644 index 00000000..d3b64e0e --- /dev/null +++ b/src/proxies.js @@ -0,0 +1,56 @@ +import { ProxyAgent } from "undici"; +import SlackError from "./errors.js"; + +/** + * Return a fetch function that routes requests through a configured proxy. + * + * @param {import("./config.js").default} config + * @param {string?} [destination] - A provided request destination. + * @returns {typeof globalThis.fetch | undefined} + */ +export function fetch(config, destination) { + const dispatcher = proxies(config, destination); + if (!dispatcher) { + return undefined; + } + return (url, init) => globalThis.fetch(url, { ...init, dispatcher }); +} + +/** + * Return a configured proxy agent dispatcher to the request destination. + * + * @see {@link https://github.com/slackapi/slack-github-action/pull/132} + * @see {@link https://github.com/slackapi/slack-github-action/pull/205} + * + * @param {import("./config.js").default} config + * @param {string?} [destination] - The request destination to proxy towards. + * @returns {ProxyAgent | undefined} + */ +export function proxies(config, destination) { + const proxy = config.inputs.proxy; + if (!proxy) { + return undefined; + } + try { + if (destination && new URL(destination).protocol !== "https:") { + config.core.debug( + "The request destination is not HTTPS so skipping the HTTPS proxy", + ); + return undefined; + } + switch (new URL(proxy).protocol) { + case "https:": + case "http:": + return new ProxyAgent(proxy); + default: + throw new SlackError(config.core, `Unsupported URL protocol: ${proxy}`); + } + } catch (/** @type {any} */ err) { + if (err instanceof SlackError) { + throw err; + } + throw new SlackError(config.core, "Failed to configure the HTTPS proxy", { + cause: err, + }); + } +} diff --git a/src/send.js b/src/send.js index e742991f..4127ce65 100644 --- a/src/send.js +++ b/src/send.js @@ -1,4 +1,3 @@ -import core from "@actions/core"; import Client from "./client.js"; import Config from "./config.js"; import SlackError from "./errors.js"; @@ -6,16 +5,16 @@ import Webhook from "./webhook.js"; /** * Orchestrate the action job happenings from inputs to logic to outputs. - * @param {core} core - GitHub Actions core utilities. + * @param {typeof import("@actions/core")} core - GitHub Actions core utilities. * @throws if an error happens but might not cause the job to fail. */ export default async function send(core) { const config = new Config(core); try { await post(config); - config.core.setOutput("time", Math.floor(new Date().valueOf() / 1000)); + config.core.setOutput("time", Math.floor(Date.now() / 1000)); } catch (/** @type {any} */ error) { - config.core.setOutput("time", Math.floor(new Date().valueOf() / 1000)); + config.core.setOutput("time", Math.floor(Date.now() / 1000)); if (config.inputs.errors) { core.setFailed(error); throw new SlackError(core, error); diff --git a/src/webhook.js b/src/webhook.js index 0d6df7d4..8a351d7e 100644 --- a/src/webhook.js +++ b/src/webhook.js @@ -1,11 +1,13 @@ -import axiosRetry, { exponentialDelay, linearDelay } from "axios-retry"; -import { HttpsProxyAgent } from "https-proxy-agent"; +import webhook from "@slack/webhook"; import Config from "./config.js"; import SlackError from "./errors.js"; +import { fetch } from "./proxies.js"; /** * This Webhook class posts the configured payload to the provided webhook, with * whatever additional settings set. + * + * @see {@link https://docs.slack.dev/tools/node-slack-sdk/webhook/} */ export default class Webhook { /** @@ -15,109 +17,64 @@ export default class Webhook { if (!config.inputs.webhook) { throw new SlackError(config.core, "No webhook was provided to post to"); } - /** - * @type {import("axios-retry").IAxiosRetryConfig} - * @see {@link https://www.npmjs.com/package/axios-retry} - */ - const retries = this.retries(config.inputs.retries); - axiosRetry(config.axios, retries); + const url = config.inputs.webhook; + const options = { + fetch: fetch(config, url), + retryConfig: this.retries(config.inputs.retries), + }; try { - const response = await config.axios.post( - config.inputs.webhook, - config.content.values, - { - ...this.proxies(config), - }, - ); - config.core.setOutput("ok", response.status === 200); - config.core.setOutput("response", JSON.stringify(response.data)); - config.core.debug(JSON.stringify(response.data)); - } catch (/** @type {any} */ err) { - const response = err.toJSON(); - config.core.setOutput("ok", response.status === 200); - config.core.setOutput("response", JSON.stringify(response.message)); - config.core.debug(response); - throw new SlackError(config.core, response.message); - } - } - - /** - * Return configurations for http proxy options if these are set. - * @param {Config} config - * @returns {import("axios").AxiosRequestConfig | undefined} - * @see {@link https://github.com/slackapi/slack-github-action/pull/132} - */ - proxies(config) { - const { webhook, proxy } = config.inputs; - if (!webhook) { - throw new SlackError(config.core, "No webhook was provided to proxy to"); - } - if (!proxy) { - return undefined; - } - try { - if (new URL(webhook).protocol !== "https:") { - config.core.debug( - "The webhook destination is not HTTPS so skipping the HTTPS proxy", - ); - return undefined; - } - switch (new URL(proxy).protocol) { - case "https:": - return { - httpsAgent: new HttpsProxyAgent(proxy), - }; - case "http:": - return { - httpsAgent: new HttpsProxyAgent(proxy), - proxy: false, - }; + switch (config.inputs.webhookType) { + case "incoming-webhook": { + const response = await new config.webhook.IncomingWebhook( + url, + options, + ).send(config.content.values); + config.core.setOutput("ok", true); + config.core.setOutput("response", JSON.stringify(response.text)); + config.core.debug(JSON.stringify(response.text)); + return; + } + case "webhook-trigger": { + const response = await new config.webhook.WebhookTrigger( + url, + options, + ).send(config.content.values); + config.core.setOutput("ok", response.ok); + config.core.setOutput("response", JSON.stringify(response)); + config.core.debug(JSON.stringify(response)); + return; + } default: throw new SlackError( config.core, - `Unsupported URL protocol: ${proxy}`, + `Unknown webhook type: ${config.inputs.webhookType}`, ); } } catch (/** @type {any} */ err) { - throw new SlackError(config.core, "Failed to configure the HTTPS proxy", { - cause: err, - }); + config.core.setOutput("ok", false); + config.core.setOutput("response", JSON.stringify(err.message)); + config.core.debug(err); + throw new SlackError(config.core, err.message); } } /** * Return configurations for retry options with different delays. * @param {string} option - * @returns {import("axios-retry").IAxiosRetryConfig} + * @returns {import("@slack/webhook").RetryOptions} */ retries(option) { switch (option?.trim().toUpperCase()) { case "0": return { retries: 0 }; case "5": - return { - retryCondition: axiosRetry.isRetryableError, - retries: 5, - retryDelay: linearDelay(60 * 1000), // 5 minutes - }; + return webhook.retryPolicies.fiveRetriesInFiveMinutes; case "10": - return { - retryCondition: axiosRetry.isRetryableError, - retries: 10, - retryDelay: (count, err) => exponentialDelay(count, err, 2 * 1000), // 34.12 minutes - }; + return webhook.retryPolicies.tenRetriesInAboutThirtyMinutes; case "RAPID": - return { - retryCondition: axiosRetry.isRetryableError, - retries: 12, - retryDelay: linearDelay(1 * 1000), // 12 seconds - }; + return webhook.retryPolicies.rapidRetryPolicy; default: - return { - retryCondition: axiosRetry.isRetryableError, - retries: 5, - retryDelay: linearDelay(60 * 1000), // 5 minutes - }; + return webhook.retryPolicies.fiveRetriesInFiveMinutes; } } } diff --git a/test/client.spec.js b/test/client.spec.js index 58f2dcdb..b9e69f8d 100644 --- a/test/client.spec.js +++ b/test/client.spec.js @@ -1,7 +1,6 @@ -import core from "@actions/core"; +import assert from "node:assert"; +import { beforeEach, describe, it } from "node:test"; import webapi from "@slack/web-api"; -import errors from "@slack/web-api/dist/errors.js"; -import { assert } from "chai"; import sinon from "sinon"; import Client from "../src/client.js"; import Config from "../src/config.js"; @@ -21,7 +20,7 @@ describe("client", () => { * @type {Config} */ const config = { - core: core, + core: mocks.core, inputs: { token: "xoxb-example", }, @@ -31,9 +30,9 @@ describe("client", () => { assert.fail("Failed to throw for missing input"); } catch (err) { if (err instanceof SlackError) { - assert.include(err.message, "No API method was provided for use"); + assert.ok(err.message.includes("No API method was provided for use")); } else { - assert.fail("Failed to throw a SlackError", err); + assert.fail(err); } } }); @@ -43,7 +42,7 @@ describe("client", () => { * @type {Config} */ const config = { - core: core, + core: mocks.core, inputs: { method: "chat.postMessage", }, @@ -54,42 +53,111 @@ describe("client", () => { assert.fail("Failed to throw for missing input"); } catch (err) { if (err instanceof SlackError) { - assert.include(err.message, "No token was provided to post with"); + assert.ok(err.message.includes("No token was provided to post with")); } else { - assert.fail("Failed to throw a SlackError", err); + assert.fail(err); } } }); + }); - it("uses input arguments when constructing the web api client", async () => { - const spy = sinon.spy(mocks.webapi, "WebClient"); + describe("api", async () => { + it("uses arguments to send to a slack api method", async () => { + const apis = sinon.stub().resolves({ ok: true }); + const constructors = sinon + .stub(mocks.webapi, "WebClient") + .returns({ apiCall: apis }); /** * @type {Config} */ const config = { content: { - values: {}, + values: { + channel: "CHANNELHERE", + timestamp: "1234567890.000000", + }, }, - core: core, - logger: new Logger(core).logger, + core: mocks.core, + logger: new Logger(mocks.core).logger, inputs: { - api: "http://localhost:8080/api", method: "pins.add", - retries: "10", token: "xoxb-example-002", }, webapi: mocks.webapi, }; await new Client().post(config); - assert.isTrue(spy.calledWithNew()); - assert.isTrue( - spy.calledWith("xoxb-example-002", { - agent: undefined, + assert.ok(constructors.calledWithNew()); + assert.ok( + constructors.calledWith("xoxb-example-002", { + fetch: undefined, + allowAbsoluteUrls: false, + logger: config.logger, + retryConfig: webapi.retryPolicies.fiveRetriesInFiveMinutes, + slackApiUrl: undefined, + }), + ); + assert.ok(apis.calledOnce); + assert.ok( + apis.calledWith("pins.add", { + channel: "CHANNELHERE", + timestamp: "1234567890.000000", + }), + ); + assert.ok(config.core.setOutput.calledWith("ok", true)); + }); + + it("uses arguments to send to a custom api method", async () => { + const apis = sinon.stub().resolves({ done: true, response: "Infinite" }); + const constructors = sinon + .stub(mocks.webapi, "WebClient") + .returns({ apiCall: apis }); + /** + * @type {Config} + */ + const config = { + content: { + values: { + model: "llama3.2", + prompt: "How many sides does a circle have?", + stream: false, + }, + }, + core: mocks.core, + logger: new Logger(mocks.core).logger, + inputs: { + api: "http://localhost:11434/api/", + method: "generate", + retries: "10", + token: "ollamapassword", + }, + webapi: mocks.webapi, + }; + await new Client().post(config); + assert.ok(constructors.calledWithNew()); + assert.ok( + constructors.calledWith("ollamapassword", { + fetch: undefined, + allowAbsoluteUrls: false, logger: config.logger, retryConfig: webapi.retryPolicies.tenRetriesInAboutThirtyMinutes, - slackApiUrl: "http://localhost:8080/api", + slackApiUrl: "http://localhost:11434/api/", }), ); + assert.ok(apis.calledOnce); + assert.ok( + apis.calledWith("generate", { + model: "llama3.2", + prompt: "How many sides does a circle have?", + stream: false, + }), + ); + assert.ok(config.core.setOutput.calledWith("ok", undefined)); + assert.ok( + config.core.setOutput.calledWith( + "response", + JSON.stringify({ done: true, response: "Infinite" }), + ), + ); }); }); @@ -137,8 +205,8 @@ describe("client", () => { ); assert.equal(mocks.core.setOutput.getCall(5).firstArg, "time"); assert.equal(mocks.core.setOutput.getCalls().length, 6); - } catch (error) { - console.error(error); + } catch (err) { + console.error(err); assert.fail("Unexpected error when calling the method"); } }); @@ -175,8 +243,8 @@ describe("client", () => { assert.equal(mocks.core.setOutput.getCall(2).lastArg, "C0101010101"); assert.equal(mocks.core.setOutput.getCall(3).firstArg, "time"); assert.equal(mocks.core.setOutput.getCalls().length, 4); - } catch (error) { - console.error(error); + } catch (err) { + console.error(err); assert.fail("Unexpected error when calling the method"); } }); @@ -208,8 +276,8 @@ describe("client", () => { ); assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time"); assert.equal(mocks.core.setOutput.getCalls().length, 3); - } catch (error) { - console.error(error); + } catch (err) { + console.error(err); assert.fail("Unexpected error when calling the method"); } }); @@ -217,27 +285,18 @@ describe("client", () => { describe("failure", () => { it("errors when the request to the api cannot be sent correct", async () => { - /** - * @type {webapi.WebAPICallError} - */ - const response = { - code: "slack_webapi_request_error", - data: { - error: "unexpected_request_failure", - message: "Something bad happened!", - }, - }; + const response = new Error("Something bad happened!"); try { mocks.core.getInput.reset(); mocks.core.getBooleanInput.withArgs("errors").returns(true); mocks.core.getInput.withArgs("method").returns("chat.postMessage"); mocks.core.getInput.withArgs("token").returns("xoxb-example"); mocks.core.getInput.withArgs("payload").returns(`"text": "hello"`); - mocks.calls.rejects(errors.requestErrorWithOriginal(response, true)); + mocks.calls.rejects(new webapi.WebAPIRequestError(response)); await send(mocks.core); assert.fail("Expected an error but none was found"); - } catch (error) { - assert.isTrue(mocks.core.setFailed.called); + } catch (_err) { + assert.ok(mocks.core.setFailed.called); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); @@ -251,36 +310,29 @@ describe("client", () => { }); it("errors when the http portion of the request fails to send", async () => { - /** - * @type {import("axios").AxiosResponse} - */ - const response = { - code: "slack_webapi_http_error", - headers: { + const response = new webapi.WebAPIHTTPError( + 500, + "Internal Server Error", + { authorization: "none", }, - data: { - ok: false, - error: "unknown_http_method", - }, - }; + { ok: false, error: "unknown_http_method" }, + ); try { mocks.core.getInput.withArgs("method").returns("chat.postMessage"); mocks.core.getInput.withArgs("token").returns("xoxb-example"); mocks.core.getInput.withArgs("payload").returns(`"text": "hello"`); - mocks.calls.rejects(errors.httpErrorFromResponse(response)); + mocks.calls.rejects(response); await send(mocks.core); assert.fail("Expected an error but none was found"); - } catch (error) { - assert.isFalse(mocks.core.setFailed.called); + } catch (_err) { + assert.strictEqual(mocks.core.setFailed.called, false); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); - response.body = response.data; - response.data = undefined; - assert.deepEqual( - mocks.core.setOutput.getCall(1).lastArg, - JSON.stringify(response), + assert.equal( + JSON.parse(mocks.core.setOutput.getCall(1).lastArg).statusCode, + 500, ); assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time"); assert.equal(mocks.core.setOutput.getCalls().length, 3); @@ -288,27 +340,18 @@ describe("client", () => { }); it("errors when the payload arguments are invalid for the api", async () => { - /** - * @type {webapi.WebAPICallError} - */ - const response = { - code: "slack_webapi_platform_error", - data: { - ok: false, - error: "missing_channel", - }, - }; + const response = { ok: false, error: "missing_channel" }; try { mocks.core.getInput.reset(); mocks.core.getBooleanInput.withArgs("errors").returns(true); mocks.core.getInput.withArgs("method").returns("chat.postMessage"); mocks.core.getInput.withArgs("token").returns("xoxb-example"); mocks.core.getInput.withArgs("payload").returns(`"text": "hello"`); - mocks.calls.rejects(errors.platformErrorFromResult(response)); + mocks.calls.rejects(new webapi.WebAPIPlatformError(response)); await send(mocks.core); assert.fail("Expected an error but none was found"); - } catch (error) { - assert.isTrue(mocks.core.setFailed.called); + } catch (_err) { + assert.ok(mocks.core.setFailed.called); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); @@ -322,22 +365,16 @@ describe("client", () => { }); it("returns the api error and details without a exit failing", async () => { - const response = { - code: "slack_webapi_platform_error", - data: { - ok: false, - error: "missing_channel", - }, - }; + const response = { ok: false, error: "missing_channel" }; try { mocks.core.getInput.withArgs("method").returns("chat.postMessage"); mocks.core.getInput.withArgs("token").returns("xoxb-example"); mocks.core.getInput.withArgs("payload").returns(`"text": "hello"`); - mocks.calls.rejects(errors.platformErrorFromResult(response)); + mocks.calls.rejects(new webapi.WebAPIPlatformError(response)); await send(mocks.core); assert.fail("Expected an error but none was found"); - } catch (error) { - assert.isFalse(mocks.core.setFailed.called); + } catch (_err) { + assert.strictEqual(mocks.core.setFailed.called, false); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); @@ -351,25 +388,21 @@ describe("client", () => { }); it("errors if rate limit responses are returned after retries", async () => { - const response = { - code: "slack_webapi_rate_limited_error", - retryAfter: 12, - }; try { mocks.core.getInput.withArgs("method").returns("chat.postMessage"); mocks.core.getInput.withArgs("token").returns("xoxb-example"); mocks.core.getInput.withArgs("payload").returns(`"text": "hello"`); - mocks.calls.rejects(errors.rateLimitedErrorWithDelay(12)); + mocks.calls.rejects(new webapi.WebAPIRateLimitedError(12)); await send(mocks.core); assert.fail("Expected an error but none was found"); - } catch (error) { - assert.isFalse(mocks.core.setFailed.called); + } catch (_err) { + assert.strictEqual(mocks.core.setFailed.called, false); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); - assert.deepEqual( - mocks.core.setOutput.getCall(1).lastArg, - JSON.stringify(response), + assert.equal( + JSON.parse(mocks.core.setOutput.getCall(1).lastArg).retryAfter, + 12, ); assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time"); assert.equal(mocks.core.setOutput.getCalls().length, 3); @@ -377,39 +410,6 @@ describe("client", () => { }); }); - describe("proxies", () => { - it("sets up the proxy agent for the provided https proxy", async () => { - const proxy = "https://example.com"; - mocks.core.getInput.withArgs("method").returns("chat.postMessage"); - mocks.core.getInput.withArgs("proxy").returns(proxy); - mocks.core.getInput.withArgs("token").returns("xoxb-example"); - const config = new Config(mocks.core); - const client = new Client(); - const { httpsAgent, proxy: proxying } = client.proxies(config); - assert.deepEqual(httpsAgent.proxy, new URL(proxy)); - assert.isNotFalse(proxying); - }); - - it("fails to configure proxies with an invalid proxied url", async () => { - const proxy = "https://"; - mocks.core.getInput.withArgs("method").returns("chat.postMessage"); - mocks.core.getInput.withArgs("proxy").returns(proxy); - mocks.core.getInput.withArgs("token").returns("xoxb-example"); - try { - const config = new Config(mocks.core); - const client = new Client(); - client.proxies(config); - assert.fail("An invalid proxy URL was not thrown as error!"); - } catch (err) { - if (err instanceof SlackError) { - assert.include(err.message, "Failed to configure the HTTPS proxy"); - } else { - assert.fail("Failed to throw a SlackError", err); - } - } - }); - }); - describe("retries", () => { it("uses a default of five retries in requests", async () => { const client = new Client(); @@ -421,14 +421,14 @@ describe("client", () => { }); it('does not attempt retries when "0" is set', async () => { - const webhook = new Client(); - const result = webhook.retries("0"); + const client = new Client(); + const result = client.retries("0"); assert.equal(result.retries, 0); }); it('attempts a default amount of "5" retries', async () => { - const webhook = new Client(); - const result = webhook.retries("5"); + const client = new Client(); + const result = client.retries("5"); assert.equal( result.retries, webapi.retryPolicies.fiveRetriesInFiveMinutes.retries, @@ -440,8 +440,8 @@ describe("client", () => { }); it('attempts "10" retries in around "30" minutes', async () => { - const webhook = new Client(); - const result = webhook.retries("10"); + const client = new Client(); + const result = client.retries("10"); assert.equal( result.retries, webapi.retryPolicies.tenRetriesInAboutThirtyMinutes.retries, @@ -453,8 +453,8 @@ describe("client", () => { }); it('attempts a "rapid " burst of "12" retries in seconds', async () => { - const webhook = new Client(); - const result = webhook.retries("rapid "); + const client = new Client(); + const result = client.retries("rapid "); assert.equal( result.retries, webapi.retryPolicies.rapidRetryPolicy.retries, @@ -463,8 +463,8 @@ describe("client", () => { }); it('attempts a "RAPID" burst of "12" retries in seconds', async () => { - const webhook = new Client(); - const result = webhook.retries("RAPID"); + const client = new Client(); + const result = client.retries("RAPID"); assert.equal( result.retries, webapi.retryPolicies.rapidRetryPolicy.retries, diff --git a/test/config.spec.js b/test/config.spec.js index 470b35e9..029f7eb7 100644 --- a/test/config.spec.js +++ b/test/config.spec.js @@ -1,4 +1,8 @@ -import { assert } from "chai"; +import assert from "node:assert"; +import { beforeEach, describe, it } from "node:test"; +import webapi from "@slack/web-api"; +import webhook from "@slack/webhook"; +import sinon from "sinon"; import Config from "../src/config.js"; import SlackError from "../src/errors.js"; import send from "../src/send.js"; @@ -34,7 +38,7 @@ describe("config", () => { assert.equal(config.inputs.proxy, "https://example.com"); assert.equal(config.inputs.retries, config.Retries.ZERO); assert.equal(config.inputs.token, "xoxb-example"); - assert.isTrue(mocks.core.setSecret.withArgs("xoxb-example").called); + assert.ok(mocks.core.setSecret.withArgs("xoxb-example").called); }); it("allows token environment variables with a webhook", async () => { @@ -45,10 +49,8 @@ describe("config", () => { assert.equal(config.inputs.token, "xoxb-example"); assert.equal(config.inputs.webhook, "https://example.com"); assert.equal(config.inputs.webhookType, "incoming-webhook"); - assert.isTrue(mocks.core.setSecret.withArgs("xoxb-example").called); - assert.isTrue( - mocks.core.setSecret.withArgs("https://example.com").called, - ); + assert.ok(mocks.core.setSecret.withArgs("xoxb-example").called); + assert.ok(mocks.core.setSecret.withArgs("https://example.com").called); }); it("allows webhook environment variables with a token", async () => { @@ -59,10 +61,8 @@ describe("config", () => { assert.equal(config.inputs.method, "chat.postMessage"); assert.equal(config.inputs.token, "xoxb-example"); assert.equal(config.inputs.webhook, "https://example.com"); - assert.isTrue(mocks.core.setSecret.withArgs("xoxb-example").called); - assert.isTrue( - mocks.core.setSecret.withArgs("https://example.com").called, - ); + assert.ok(mocks.core.setSecret.withArgs("xoxb-example").called); + assert.ok(mocks.core.setSecret.withArgs("https://example.com").called); }); it("errors when both the token and webhook is provided", async () => { @@ -73,16 +73,17 @@ describe("config", () => { assert.fail("Failed to error when invalid inputs are provided"); } catch (err) { if (err instanceof SlackError) { - assert.include( - err.message, - "Invalid input! Either the token or webhook is required - not both.", + assert.ok( + err.message.includes( + "Invalid input! Either the token or webhook is required - not both.", + ), ); - assert.isTrue(mocks.core.setSecret.withArgs("xoxb-example").called); - assert.isTrue( + assert.ok(mocks.core.setSecret.withArgs("xoxb-example").called); + assert.ok( mocks.core.setSecret.withArgs("https://example.com").called, ); } else { - assert.fail("Failed to throw a SlackError", err); + assert.fail(err); } } }); @@ -94,12 +95,13 @@ describe("config", () => { assert.fail("Failed to error when invalid inputs are provided"); } catch (err) { if (err instanceof SlackError) { - assert.include( - err.message, - "Missing input! A token must be provided to use the method decided.", + assert.ok( + err.message.includes( + "Missing input! A token must be provided to use the method decided.", + ), ); } else { - assert.fail("Failed to throw a SlackError", err); + assert.fail(err); } } }); @@ -110,12 +112,13 @@ describe("config", () => { assert.fail("Failed to error when invalid inputs are provided"); } catch (err) { if (err instanceof SlackError) { - assert.include( - err.message, - "Missing input! Either a method or webhook is required to take action.", + assert.ok( + err.message.includes( + "Missing input! Either a method or webhook is required to take action.", + ), ); } else { - assert.fail("Failed to throw a SlackError", err); + assert.fail(err); } } }); @@ -127,12 +130,13 @@ describe("config", () => { assert.fail("Failed to error when invalid inputs are provided"); } catch (err) { if (err instanceof SlackError) { - assert.include( - err.message, - "Missing input! The webhook type must be 'incoming-webhook' or 'webhook-trigger'.", + assert.ok( + err.message.includes( + "Missing input! The webhook type must be 'incoming-webhook' or 'webhook-trigger'.", + ), ); } else { - assert.fail("Failed to throw a SlackError", err); + assert.fail(err); } } }); @@ -145,17 +149,70 @@ describe("config", () => { assert.fail("Failed to error when invalid inputs are provided"); } catch (err) { if (err instanceof SlackError) { - assert.include( - err.message, - "Invalid input! The webhook type must be 'incoming-webhook' or 'webhook-trigger'.", + assert.ok( + err.message.includes( + "Invalid input! The webhook type must be 'incoming-webhook' or 'webhook-trigger'.", + ), ); } else { - assert.fail("Failed to throw a SlackError", err); + assert.fail(err); } } }); }); + describe("instrument", () => { + it("adds metadata to webapi with package name and version", async () => { + const stub = sinon.stub(); + const original = Object.getOwnPropertyDescriptor( + webapi, + "addAppMetadata", + ); + Object.defineProperty(webapi, "addAppMetadata", { + value: stub, + configurable: true, + }); + try { + mocks.core.getInput.withArgs("method").returns("chat.postMessage"); + mocks.core.getInput.withArgs("token").returns("xoxb-example"); + new Config(mocks.core); + assert.ok(stub.calledOnce); + const { name, version } = stub.firstCall.args[0]; + assert.equal(name, "@slack/slack-github-action"); + assert.ok(version); + } finally { + Object.defineProperty(webapi, "addAppMetadata", original); + } + }); + + it("adds metadata to webhook with package name and version", async () => { + const stub = sinon.stub(); + const original = Object.getOwnPropertyDescriptor( + webhook, + "addAppMetadata", + ); + Object.defineProperty(webhook, "addAppMetadata", { + value: stub, + configurable: true, + }); + try { + mocks.core.getInput + .withArgs("webhook") + .returns("https://hooks.slack.com"); + mocks.core.getInput + .withArgs("webhook-type") + .returns("incoming-webhook"); + new Config(mocks.core); + assert.ok(stub.calledOnce); + const { name, version } = stub.firstCall.args[0]; + assert.equal(name, "@slack/slack-github-action"); + assert.ok(version); + } finally { + Object.defineProperty(webhook, "addAppMetadata", original); + } + }); + }); + describe("mask", async () => { it("treats the provided token as a secret", async () => { mocks.core.getInput.withArgs("token").returns("xoxb-example"); @@ -163,7 +220,7 @@ describe("config", () => { await send(mocks.core); assert.fail("Failed to error for incomplete inputs while testing"); } catch { - assert.isTrue(mocks.core.setSecret.withArgs("xoxb-example").called); + assert.ok(mocks.core.setSecret.withArgs("xoxb-example").called); } }); @@ -174,16 +231,14 @@ describe("config", () => { await send(mocks.core); assert.fail("Failed to error for incomplete inputs while testing"); } catch { - assert.isTrue( - mocks.core.setSecret.withArgs("https://slack.com").called, - ); + assert.ok(mocks.core.setSecret.withArgs("https://slack.com").called); } }); }); describe("validate", () => { it('allow the "retries" option with lowercased space', async () => { - mocks.axios.post.returns(Promise.resolve("LGTM")); + mocks.webhook.incoming.resolves({ text: "LGTM" }); mocks.core.getInput.withArgs("retries").returns(" rapid "); mocks.core.getInput .withArgs("webhook") @@ -193,18 +248,19 @@ describe("config", () => { await send(mocks.core); } catch (err) { if (err instanceof SlackError) { - assert.include( - err.message, - 'Invalid input! An unknown "retries" value was used: FOREVER', + assert.ok( + err.message.includes( + 'Invalid input! An unknown "retries" value was used: FOREVER', + ), ); } else { - assert.fail("Failed to throw a SlackError", err); + assert.fail(err); } } }); it("errors if an invalid retries option is provided", async () => { - mocks.axios.post.returns(Promise.resolve("LGTM")); + mocks.webhook.incoming.resolves({ text: "LGTM" }); mocks.core.getInput.withArgs("retries").returns("FOREVER"); mocks.core.getInput .withArgs("webhook") @@ -214,12 +270,13 @@ describe("config", () => { await send(mocks.core); } catch (err) { if (err instanceof SlackError) { - assert.include( - err.message, - 'Invalid input! An unknown "retries" value was used: FOREVER', + assert.ok( + err.message.includes( + 'Invalid input! An unknown "retries" value was used: FOREVER', + ), ); } else { - assert.fail("Failed to throw a SlackError", err); + assert.fail(err); } } }); diff --git a/test/content.spec.js b/test/content.spec.js index be279d44..072f6a52 100644 --- a/test/content.spec.js +++ b/test/content.spec.js @@ -1,6 +1,7 @@ +import assert from "node:assert"; import path from "node:path"; -import core from "@actions/core"; -import { assert } from "chai"; +import { beforeEach, describe, it } from "node:test"; +import { YAMLException } from "js-yaml"; import Config from "../src/config.js"; import Content from "../src/content.js"; import SlackError from "../src/errors.js"; @@ -44,12 +45,13 @@ describe("content", () => { assert.fail("Failed to throw for invalid input"); } catch (err) { if (err instanceof SlackError) { - assert.include( - err.message, - "Invalid input! Just the payload or payload file path is required.", + assert.ok( + err.message.includes( + "Invalid input! Just the payload or payload file path is required.", + ), ); } else { - assert.fail("Failed to throw a SlackError", err); + assert.fail(err); } } }); @@ -69,6 +71,21 @@ describe("content", () => { assert.deepEqual(config.content.values, expected); }); + /** + * @see {@link https://github.com/slackapi/slack-github-action/issues/637} + */ + it("parses multiline YAML from the input payload", async () => { + mocks.core.getInput + .withArgs("payload") + .returns('channel: C0123456789\ntext: "first line\n\n second line"'); + const config = new Config(mocks.core); + const expected = { + channel: "C0123456789", + text: "first line\nsecond line", + }; + assert.deepEqual(config.content.values, expected); + }); + it("parses complete JSON from the input payload", async () => { mocks.core.getInput.withArgs("payload").returns(`{ "message": "this is wrapped", @@ -83,16 +100,122 @@ describe("content", () => { assert.deepEqual(config.content.values, expected); }); + it("templatizes variables requires configuration", async () => { + mocks.core.getInput.withArgs("payload").returns(`{ + "message": "this matches an existing variable: \${{ github.apiUrl }}", + "channel": "C0123456789" + } + `); + const config = new Config(mocks.core); + // biome-ignore-start lint/suspicious/noTemplateCurlyInString: GitHub Action YAML variable syntax + const expected = { + message: "this matches an existing variable: ${{ github.apiUrl }}", + channel: "C0123456789", + }; + // biome-ignore-end lint/suspicious/noTemplateCurlyInString: https://docs.github.com/en/actions/how-tos/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#using-contexts-to-access-variable-values + assert.deepEqual(config.content.values, expected); + }); + it("templatizes variables with matching variables", async () => { - mocks.core.getInput - .withArgs("payload") - .returns("message: Served ${{ env.NUMBER }} from ${{ github.apiUrl }}"); + mocks.core.getInput.withArgs("payload").returns(` + channel: C0123456789 + reply_broadcast: false + message: Served \${{ env.NUMBER }} items + blocks: + - type: section + text: + type: mrkdwn + text: "Served \${{ env.NUMBER }} items on: \${{ env.DETAILS }}" + - type: divider + - type: section + block_id: selector + text: + type: mrkdwn + text: Send feedback + accessory: + action_id: response + type: multi_static_select + placeholder: + type: plain_text + text: Select URL + options: + - text: + type: plain_text + text: "\${{ github.apiUrl }}" + value: api + - text: + type: plain_text + text: "\${{ github.serverUrl }}" + value: server + - text: + type: plain_text + text: "\${{ github.graphqlUrl }}" + value: graphql + `); mocks.core.getBooleanInput.withArgs("payload-templated").returns(true); + process.env.DETAILS = ` +-fri +-sat +-sun`; process.env.NUMBER = 12; const config = new Config(mocks.core); + process.env.DETAILS = undefined; process.env.NUMBER = undefined; const expected = { - message: "Served 12 from https://api.github.com", + channel: "C0123456789", + reply_broadcast: false, + message: "Served 12 items", + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: "Served 12 items on: \n-fri\n-sat\n-sun", + }, + }, + { + type: "divider", + }, + { + type: "section", + block_id: "selector", + text: { + type: "mrkdwn", + text: "Send feedback", + }, + accessory: { + action_id: "response", + type: "multi_static_select", + placeholder: { + type: "plain_text", + text: "Select URL", + }, + options: [ + { + text: { + type: "plain_text", + text: "https://api.github.com", + }, + value: "api", + }, + { + text: { + type: "plain_text", + text: "https://github.com", + }, + value: "server", + }, + { + text: { + type: "plain_text", + text: "https://api.github.com/graphql", + }, + value: "graphql", + }, + ], + }, + }, + ], }; assert.deepEqual(config.content.values, expected); }); @@ -101,9 +224,11 @@ describe("content", () => { * @see {@link https://github.com/slackapi/slack-github-action/issues/203} */ it("templatizes variables with missing variables", async () => { + // biome-ignore-start lint/suspicious/noTemplateCurlyInString: GitHub Action YAML variable syntax mocks.core.getInput .withArgs("payload") .returns("message: What makes ${{ env.TREASURE }} a secret"); + // biome-ignore-end lint/suspicious/noTemplateCurlyInString: https://docs.github.com/en/actions/how-tos/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#using-contexts-to-access-variable-values mocks.core.getBooleanInput.withArgs("payload-templated").returns(true); const config = new Config(mocks.core); const expected = { @@ -159,7 +284,7 @@ describe("content", () => { * @type {Config} */ const config = { - core: core, + core: mocks.core, inputs: { payloadFilePath: "unknown.json", }, @@ -169,12 +294,13 @@ describe("content", () => { assert.fail("Failed to throw for missing payload content"); } catch (err) { if (err instanceof SlackError) { - assert.include( - err.message, - "Invalid input! No payload content was provided", + assert.ok( + err.message.includes( + "Invalid input! No payload content was provided", + ), ); } else { - assert.fail("Failed to throw a SlackError", err); + assert.fail(err); } } }); @@ -186,12 +312,18 @@ describe("content", () => { assert.fail("Failed to throw for invalid JSON"); } catch (err) { if (err instanceof SlackError) { - assert.include( - err.message, - "Invalid input! Failed to parse contents of the provided payload", + assert.ok( + err.message.includes( + "Invalid input! Failed to parse contents of the provided payload", + ), ); + assert.notStrictEqual(err.cause?.values, undefined); + assert.equal(err.cause.values.length, 2); + const [jsonError, yamlError] = err.cause.values; + assert.ok(jsonError instanceof SyntaxError); + assert.ok(yamlError instanceof YAMLException); } else { - assert.fail("Failed to throw a SlackError", err); + assert.fail(err); } } }); @@ -246,19 +378,149 @@ describe("content", () => { assert.deepEqual(config.content.values, expected); }); + it("templatizes variables requires configuration", async () => { + mocks.core.getInput.withArgs("payload-file-path").returns("example.json"); + mocks.fs.readFileSync + .withArgs(path.resolve("example.json"), "utf-8") + .returns(`{ + "message": "this matches an existing variable: \${{ github.apiUrl }}", + "channel": "C0123456789" + } + `); + const config = new Config(mocks.core); + // biome-ignore-start lint/suspicious/noTemplateCurlyInString: GitHub Action YAML variable syntax + const expected = { + message: "this matches an existing variable: ${{ github.apiUrl }}", + channel: "C0123456789", + }; + // biome-ignore-end lint/suspicious/noTemplateCurlyInString: https://docs.github.com/en/actions/how-tos/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#using-contexts-to-access-variable-values + assert.deepEqual(config.content.values, expected); + }); + it("templatizes variables with matching variables", async () => { mocks.core.getInput.withArgs("payload-file-path").returns("example.json"); mocks.fs.readFileSync .withArgs(path.resolve("example.json"), "utf-8") .returns(`{ - "message": "Served $\{\{ env.NUMBER }} from $\{\{ github.apiUrl }}" + "channel": "C0123456789", + "reply_broadcast": false, + "message": "Served \${{ env.NUMBER }} items", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Served \${{ env.NUMBER }} items on: \${{ env.DETAILS }}" + } + }, + { + "type": "divider" + }, + { + "type": "section", + "block_id": "selector", + "text": { + "type": "mrkdwn", + "text": "Send feedback" + }, + "accessory": { + "action_id": "response", + "type": "multi_static_select", + "placeholder": { + "type": "plain_text", + "text": "Select URL" + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "\${{ github.apiUrl }}" + }, + "value": "api" + }, + { + "text": { + "type": "plain_text", + "text": "\${{ github.serverUrl }}" + }, + "value": "server" + }, + { + "text": { + "type": "plain_text", + "text": "\${{ github.graphqlUrl }}" + }, + "value": "graphql" + } + ] + } + } + ] }`); mocks.core.getBooleanInput.withArgs("payload-templated").returns(true); + process.env.DETAILS = ` +-fri +-sat +-sun`; process.env.NUMBER = 12; const config = new Config(mocks.core); + process.env.DETAILS = undefined; process.env.NUMBER = undefined; const expected = { - message: "Served 12 from https://api.github.com", + channel: "C0123456789", + reply_broadcast: false, + message: "Served 12 items", + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: "Served 12 items on: \n-fri\n-sat\n-sun", + }, + }, + { + type: "divider", + }, + { + type: "section", + block_id: "selector", + text: { + type: "mrkdwn", + text: "Send feedback", + }, + accessory: { + action_id: "response", + type: "multi_static_select", + placeholder: { + type: "plain_text", + text: "Select URL", + }, + options: [ + { + text: { + type: "plain_text", + text: "https://api.github.com", + }, + value: "api", + }, + { + text: { + type: "plain_text", + text: "https://github.com", + }, + value: "server", + }, + { + text: { + type: "plain_text", + text: "https://api.github.com/graphql", + }, + value: "graphql", + }, + ], + }, + }, + ], }; assert.deepEqual(config.content.values, expected); }); @@ -271,7 +533,7 @@ describe("content", () => { mocks.fs.readFileSync .withArgs(path.resolve("example.json"), "utf-8") .returns(`{ - "message": "What makes $\{\{ env.TREASURE }} a secret" + "message": "What makes $\{{ env.TREASURE }} a secret" }`); mocks.core.getBooleanInput.withArgs("payload-templated").returns(true); const config = new Config(mocks.core); @@ -286,7 +548,7 @@ describe("content", () => { * @type {Config} */ const config = { - core: core, + core: mocks.core, inputs: { payload: "LGTM", }, @@ -296,12 +558,11 @@ describe("content", () => { assert.fail("Failed to throw for the wrong payload type"); } catch (err) { if (err instanceof SlackError) { - assert.include( - err.message, - "Invalid input! No payload found for content", + assert.ok( + err.message.includes("Invalid input! No payload found for content"), ); } else { - assert.fail("Failed to throw a SlackError", err); + assert.fail(err); } } }); @@ -313,12 +574,13 @@ describe("content", () => { assert.fail("Failed to throw for nonexistent files"); } catch (err) { if (err instanceof SlackError) { - assert.include( - err.message, - "Invalid input! Failed to parse contents of the provided payload file", + assert.ok( + err.message.includes( + "Invalid input! Failed to parse contents of the provided payload file", + ), ); } else { - assert.fail("Failed to throw a SlackError", err); + assert.fail(err); } } }); @@ -330,16 +592,69 @@ describe("content", () => { assert.fail("Failed to throw for an unknown extension"); } catch (err) { if (err instanceof SlackError) { - assert.include( - err.message, - "Invalid input! Failed to parse contents of the provided payload file", + assert.ok( + err.message.includes( + "Invalid input! Failed to parse contents of the provided payload file", + ), + ); + assert.notStrictEqual(err.cause?.values, undefined); + assert.equal(err.cause.values.length, 1); + assert.ok( + err.cause.values[0].message.includes( + "Invalid input! Failed to parse file extension unknown.md", + ), + ); + } else { + assert.fail(err); + } + } + }); + + it("fails if invalid JSON exists in the input payload", async () => { + mocks.core.getInput.withArgs("payload-file-path").returns("example.json"); + mocks.fs.readFileSync + .withArgs(path.resolve("example.json"), "utf-8") + .returns(`{ + "message": "a truncated file without an end`); + try { + await send(mocks.core); + assert.fail("Failed to throw for invalid JSON"); + } catch (err) { + if (err instanceof SlackError) { + assert.ok( + err.message.includes( + "Invalid input! Failed to parse contents of the provided payload file", + ), ); - assert.include( - err.cause.message, - "Invalid input! Failed to parse file extension unknown.md", + assert.notStrictEqual(err.cause?.values, undefined); + assert.equal(err.cause.values.length, 1); + assert.ok(err.cause.values[0] instanceof SyntaxError); + } else { + assert.fail(err); + } + } + }); + + it("fails if invalid YAML exists in the input payload", async () => { + mocks.core.getInput.withArgs("payload-file-path").returns("example.yaml"); + mocks.fs.readFileSync + .withArgs(path.resolve("example.yaml"), "utf-8") + .returns(`- "message": "assigned": "values"`); + try { + await send(mocks.core); + assert.fail("Failed to throw for invalid YAML"); + } catch (err) { + if (err instanceof SlackError) { + assert.ok( + err.message.includes( + "Invalid input! Failed to parse contents of the provided payload file", + ), ); + assert.notStrictEqual(err.cause?.values, undefined); + assert.equal(err.cause.values.length, 1); + assert.ok(err.cause.values[0] instanceof YAMLException); } else { - assert.fail("Failed to throw a SlackError", err); + assert.fail(err); } } }); diff --git a/test/index.spec.js b/test/index.spec.js index 186db60d..55ae357e 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -1,7 +1,6 @@ import fs from "node:fs"; -import core from "@actions/core"; import webapi from "@slack/web-api"; -import axios, { AxiosError } from "axios"; +import webhook from "@slack/webhook"; import sinon from "sinon"; /** @@ -20,21 +19,6 @@ import sinon from "sinon"; * The Mock class sets expected behaviors and test listeners for dependencies. */ export class Mock { - /** - * @typedef Errors - A collection of mocked errors to use in tests. - * @prop {Object.} axios - The mocked axios errors. - */ - - /** - * The mocked errors. - * @type {Errors} - */ - errors = { - axios: { - network_failed: new AxiosError("network_failed"), - }, - }; - /** * Setup stubbed dependencies and configure default input arguments for all * tests. @@ -43,9 +27,20 @@ export class Mock { */ constructor() { this.sandbox = sinon.createSandbox(); - this.axios = this.sandbox.stub(axios); - this.calls = sinon.stub(webapi.WebClient.prototype, "apiCall"); - this.core = this.sandbox.stub(core); + this.calls = this.sandbox.stub(webapi.WebClient.prototype, "apiCall"); + this.core = { + debug: this.sandbox.stub(), + error: this.sandbox.stub(), + getInput: this.sandbox.stub(), + getBooleanInput: this.sandbox.stub(), + info: this.sandbox.stub(), + isDebug: this.sandbox.stub(), + setFailed: this.sandbox.stub(), + setOutput: this.sandbox.stub(), + setSecret: this.sandbox.stub(), + warning: this.sandbox.stub(), + }; + this.fetch = this.sandbox.stub(globalThis, "fetch"); this.fs = this.sandbox.stub(fs); this.webapi = { WebClient: function () { @@ -54,6 +49,10 @@ export class Mock { }); }, }; + this.webhook = { + incoming: this.sandbox.stub(webhook.IncomingWebhook.prototype, "send"), + trigger: this.sandbox.stub(webhook.WebhookTrigger.prototype, "send"), + }; this.core.getInput.withArgs("errors").returns("false"); this.core.getInput.withArgs("retries").returns("5"); } @@ -63,9 +62,27 @@ export class Mock { */ reset() { this.sandbox.reset(); - this.axios.post.resetHistory(); this.calls.resetHistory(); + this.core.debug.reset(); + this.core.error.reset(); this.core.getInput.reset(); + this.core.getBooleanInput.reset(); + this.core.info.reset(); + this.core.isDebug.reset(); + this.core.setFailed.reset(); + this.core.setOutput.reset(); + this.core.setSecret.reset(); + this.core.warning.reset(); + this.fetch.reset(); + this.webapi = { + WebClient: function () { + this.apiCall = () => ({ + ok: true, + }); + }, + }; + this.webhook.incoming.resetHistory(); + this.webhook.trigger.resetHistory(); this.core.getInput.withArgs("errors").returns("false"); this.core.getInput.withArgs("retries").returns("5"); process.env.SLACK_TOKEN = ""; diff --git a/test/logger.spec.js b/test/logger.spec.js index 09fb0deb..a246f4fb 100644 --- a/test/logger.spec.js +++ b/test/logger.spec.js @@ -1,6 +1,6 @@ -import core from "@actions/core"; +import assert from "node:assert"; +import { beforeEach, describe, it } from "node:test"; import { LogLevel } from "@slack/logger"; -import { assert } from "chai"; import Logger from "../src/logger.js"; import { mocks } from "./index.spec.js"; @@ -11,19 +11,19 @@ describe("logger", () => { describe("level", () => { it("debug", () => { - mocks.core.isDebug = () => true; - const { logger } = new Logger(core); + mocks.core.isDebug.returns(true); + const { logger } = new Logger(mocks.core); const actual = logger.getLevel(); const expected = LogLevel.DEBUG; - assert.equal(actual, expected); + assert.strictEqual(actual, expected); }); it("info", () => { - mocks.core.isDebug = () => false; - const { logger } = new Logger(core); + mocks.core.isDebug.returns(false); + const { logger } = new Logger(mocks.core); const actual = logger.getLevel(); const expected = LogLevel.INFO; - assert.equal(actual, expected); + assert.strictEqual(actual, expected); }); }); }); diff --git a/test/proxies.spec.js b/test/proxies.spec.js new file mode 100644 index 00000000..f4ae9227 --- /dev/null +++ b/test/proxies.spec.js @@ -0,0 +1,133 @@ +import assert from "node:assert"; +import { beforeEach, describe, it } from "node:test"; +import Config from "../src/config.js"; +import SlackError from "../src/errors.js"; +import { fetch, proxies } from "../src/proxies.js"; +import { mocks } from "./index.spec.js"; + +describe("proxies", () => { + beforeEach(() => { + mocks.reset(); + }); + + describe("fetch", () => { + it("returns a custom fetch function when a proxy is configured", async () => { + mocks.core.getInput.withArgs("method").returns("chat.postMessage"); + mocks.core.getInput.withArgs("proxy").returns("https://example.com"); + mocks.core.getInput.withArgs("token").returns("xoxb-example"); + const config = new Config(mocks.core); + const fetchFn = fetch(config); + assert.strictEqual(typeof fetchFn, "function"); + }); + + it("returns undefined when no proxy is configured", async () => { + mocks.core.getInput.withArgs("method").returns("chat.postMessage"); + mocks.core.getInput.withArgs("token").returns("xoxb-example"); + const config = new Config(mocks.core); + const fetchFn = fetch(config); + assert.strictEqual(fetchFn, undefined); + }); + + it("fails to configure a fetch with an invalid proxied url", async () => { + mocks.core.getInput.withArgs("method").returns("chat.postMessage"); + mocks.core.getInput.withArgs("proxy").returns("not-a-url"); + mocks.core.getInput.withArgs("token").returns("xoxb-example"); + try { + const config = new Config(mocks.core); + fetch(config); + assert.fail("An invalid proxy URL was not thrown as error!"); + } catch (err) { + if (err instanceof SlackError) { + assert.ok( + err.message.includes("Failed to configure the HTTPS proxy"), + ); + } else { + assert.fail(err); + } + } + }); + }); + + describe("dispatcher", () => { + it("returns no dispatcher when a proxy is not configured", async () => { + mocks.core.getInput + .withArgs("webhook") + .returns("https://hooks.slack.com"); + mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); + const config = new Config(mocks.core); + const dispatcher = proxies(config, config.inputs.webhook); + assert.strictEqual(dispatcher, undefined); + }); + + it("skips proxying a non-https destination altogether", async () => { + mocks.core.getInput.withArgs("webhook").returns("http://hooks.slack.com"); + mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); + mocks.core.getInput.withArgs("proxy").returns("https://example.com"); + const config = new Config(mocks.core); + const dispatcher = proxies(config, config.inputs.webhook); + assert.strictEqual(dispatcher, undefined); + }); + + it("returns a proxy dispatcher for the provided https proxy", async () => { + mocks.core.getInput + .withArgs("webhook") + .returns("https://hooks.slack.com"); + mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); + mocks.core.getInput.withArgs("proxy").returns("https://example.com"); + const config = new Config(mocks.core); + const dispatcher = proxies(config, config.inputs.webhook); + assert.ok(dispatcher); + }); + + it("returns a proxy dispatcher for http proxies", async () => { + mocks.core.getInput + .withArgs("webhook") + .returns("https://hooks.slack.com"); + mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); + mocks.core.getInput.withArgs("proxy").returns("http://example.com"); + const config = new Config(mocks.core); + const dispatcher = proxies(config, config.inputs.webhook); + assert.ok(dispatcher); + }); + + it("fails to configure a dispatcher with an invalid proxied url", async () => { + mocks.core.getInput + .withArgs("webhook") + .returns("https://hooks.slack.com"); + mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); + mocks.core.getInput.withArgs("proxy").returns("https://"); + try { + const config = new Config(mocks.core); + proxies(config, config.inputs.webhook); + assert.fail("An invalid proxy URL was not thrown as error!"); + } catch (err) { + if (err instanceof SlackError) { + assert.ok( + err.message.includes("Failed to configure the HTTPS proxy"), + ); + } else { + assert.fail(err); + } + } + }); + + it("fails to configure a dispatcher with an unknown url protocol", async () => { + mocks.core.getInput + .withArgs("webhook") + .returns("https://hooks.slack.com"); + mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); + mocks.core.getInput.withArgs("proxy").returns("ssh://example.com"); + try { + const config = new Config(mocks.core); + proxies(config, config.inputs.webhook); + assert.fail("An unknown URL protocol was not thrown as error!"); + } catch (err) { + if (err instanceof SlackError) { + assert.ok(err.message.includes("Unsupported URL protocol")); + } else { + assert.fail(err); + } + } + }); + }); +}); diff --git a/test/send.spec.js b/test/send.spec.js index 0d20056c..157c3c96 100644 --- a/test/send.spec.js +++ b/test/send.spec.js @@ -1,4 +1,5 @@ -import { assert } from "chai"; +import assert from "node:assert"; +import { beforeEach, describe, it } from "node:test"; import send from "../src/send.js"; import { mocks } from "./index.spec.js"; @@ -23,9 +24,7 @@ describe("send", () => { .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("webhook-trigger"); mocks.core.getInput.withArgs("payload").returns('"greetings": "hello"'); - mocks.axios.post.returns( - Promise.resolve({ status: 200, data: { ok: true } }), - ); + mocks.webhook.trigger.resolves({ ok: true }); await send(mocks.core); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); @@ -35,7 +34,7 @@ describe("send", () => { JSON.stringify({ ok: true }), ); assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time"); - assert.isAtLeast(mocks.core.setOutput.getCall(2).lastArg, 0); + assert.ok(mocks.core.setOutput.getCall(2).lastArg >= 0); }); it("token", async () => { @@ -53,7 +52,7 @@ describe("send", () => { JSON.stringify({ ok: true }), ); assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time"); - assert.isAtLeast(mocks.core.setOutput.getCall(2).lastArg, 0); + assert.ok(mocks.core.setOutput.getCall(2).lastArg >= 0); }); it("incoming webhook", async () => { @@ -63,7 +62,7 @@ describe("send", () => { .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); mocks.core.getInput.withArgs("payload").returns('"text": "hello"'); - mocks.axios.post.returns(Promise.resolve({ status: 200, data: "ok" })); + mocks.webhook.incoming.resolves({ text: "ok" }); await send(mocks.core); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); @@ -73,7 +72,7 @@ describe("send", () => { JSON.stringify("ok"), ); assert.equal(mocks.core.setOutput.getCall(2).firstArg, "time"); - assert.isAtLeast(mocks.core.setOutput.getCall(2).lastArg, 0); + assert.ok(mocks.core.setOutput.getCall(2).lastArg >= 0); }); }); }); diff --git a/test/webhook.spec.js b/test/webhook.spec.js index f94cf097..91dcc88b 100644 --- a/test/webhook.spec.js +++ b/test/webhook.spec.js @@ -1,6 +1,6 @@ -import core from "@actions/core"; -import { AxiosError } from "axios"; -import { assert } from "chai"; +import assert from "node:assert"; +import { beforeEach, describe, it } from "node:test"; +import webhook from "@slack/webhook"; import Config from "../src/config.js"; import SlackError from "../src/errors.js"; import send from "../src/send.js"; @@ -19,16 +19,13 @@ describe("webhook", () => { .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("webhook-trigger"); mocks.core.getInput.withArgs("payload").returns("drinks: coffee"); - mocks.axios.post.returns( - Promise.resolve({ status: 200, data: { ok: true } }), - ); + mocks.webhook.trigger.resolves({ ok: true }); try { await send(mocks.core); - assert.equal(mocks.axios.post.getCalls().length, 1); - const [url, payload, options] = mocks.axios.post.getCall(0).args; - assert.equal(url, "https://hooks.slack.com"); - assert.deepEqual(payload, { drinks: "coffee" }); - assert.deepEqual(options, {}); + assert.equal(mocks.webhook.trigger.getCalls().length, 1); + assert.deepEqual(mocks.webhook.trigger.getCall(0).firstArg, { + drinks: "coffee", + }); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); @@ -48,14 +45,13 @@ describe("webhook", () => { .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); mocks.core.getInput.withArgs("payload").returns("text: greetings"); - mocks.axios.post.returns(Promise.resolve({ status: 200, data: "ok" })); + mocks.webhook.incoming.resolves({ text: "ok" }); try { await send(mocks.core); - assert.equal(mocks.axios.post.getCalls().length, 1); - const [url, payload, options] = mocks.axios.post.getCall(0).args; - assert.equal(url, "https://hooks.slack.com"); - assert.deepEqual(payload, { text: "greetings" }); - assert.deepEqual(options, {}); + assert.equal(mocks.webhook.incoming.getCalls().length, 1); + assert.deepEqual(mocks.webhook.incoming.getCall(0).firstArg, { + text: "greetings", + }); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, true); assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); @@ -76,7 +72,7 @@ describe("webhook", () => { * @type {Config} */ const config = { - core: core, + core: mocks.core, inputs: {}, }; try { @@ -84,254 +80,159 @@ describe("webhook", () => { assert.fail("Failed to throw for missing input"); } catch (err) { if (err instanceof SlackError) { - assert.include(err.message, "No webhook was provided to post to"); + assert.ok(err.message.includes("No webhook was provided to post to")); } else { - assert.fail("Failed to throw a SlackError", err); + assert.fail(err); + } + } + }); + + it("errors when an unknown webhook type is provided", async () => { + /** + * @type {Config} + */ + const config = { + core: mocks.core, + inputs: { + webhook: "https://hooks.slack.com", + webhookType: "unknown-webhook", + retries: "5", + }, + }; + try { + await new Webhook().post(config); + assert.fail(); + } catch (err) { + if (err instanceof SlackError) { + assert.ok( + err.message.includes("Unknown webhook type: unknown-webhook"), + ); + } else { + assert.fail(err); } } }); it("returns the failures from a webhook trigger", async () => { + mocks.core.getBooleanInput.withArgs("errors").returns(true); mocks.core.getInput .withArgs("webhook") .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("webhook-trigger"); mocks.core.getInput.withArgs("payload").returns("drinks: coffee"); - const response = new AxiosError( - "Request failed with status code 400", - "ERR_BAD_REQUEST", - {}, - {}, - { status: 400 }, + mocks.webhook.trigger.rejects( + new Error("An HTTP protocol error occurred"), ); - mocks.axios.post.resolves(Promise.reject(response)); try { await send(mocks.core); + assert.fail(); } catch (err) { if (err instanceof SlackError) { - assert.include(err.message, "Request failed with status code 400"); + assert.ok(err.message.includes("An HTTP protocol error occurred")); } else { - assert.fail("Failed to throw a SlackError", err); + assert.fail(err); } } - assert.equal(mocks.axios.post.getCalls().length, 1); - const [url, payload, options] = mocks.axios.post.getCall(0).args; - assert.equal(url, "https://hooks.slack.com"); - assert.deepEqual(payload, { drinks: "coffee" }); - assert.deepEqual(options, {}); + assert.equal(mocks.webhook.trigger.getCalls().length, 1); + assert.deepEqual(mocks.webhook.trigger.getCall(0).firstArg, { + drinks: "coffee", + }); + assert.ok(mocks.core.setFailed.called); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); }); it("returns the failures from an incoming webhook", async () => { + mocks.core.getBooleanInput.withArgs("errors").returns(true); mocks.core.getInput .withArgs("webhook") .returns("https://hooks.slack.com"); mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); mocks.core.getInput.withArgs("payload").returns("textt: oops"); - const response = new AxiosError( - "Request failed with status code 400", - "ERR_BAD_REQUEST", - {}, - {}, - { status: 400 }, + mocks.webhook.incoming.rejects( + new Error("An HTTP protocol error occurred"), ); - mocks.axios.post.resolves(Promise.reject(response)); try { await send(mocks.core); + assert.fail(); } catch (err) { if (err instanceof SlackError) { - assert.include(err.message, "Request failed with status code 400"); + assert.ok(err.message.includes("An HTTP protocol error occurred")); } else { - assert.fail("Failed to throw a SlackError", err); + assert.fail(err); } } - assert.equal(mocks.axios.post.getCalls().length, 1); - const [url, payload, options] = mocks.axios.post.getCall(0).args; - assert.equal(url, "https://hooks.slack.com"); - assert.deepEqual(payload, { textt: "oops" }); - assert.deepEqual(options, {}); + assert.equal(mocks.webhook.incoming.getCalls().length, 1); + assert.deepEqual(mocks.webhook.incoming.getCall(0).firstArg, { + textt: "oops", + }); + assert.ok(mocks.core.setFailed.called); assert.equal(mocks.core.setOutput.getCall(0).firstArg, "ok"); assert.equal(mocks.core.setOutput.getCall(0).lastArg, false); assert.equal(mocks.core.setOutput.getCall(1).firstArg, "response"); }); }); - describe("proxies", () => { - it("requires a webhook is included in the inputs", async () => { - /** - * @type {Config} - */ - const config = { - core: core, - inputs: {}, - }; - try { - new Webhook().proxies(config); - assert.fail("Failed to throw for missing input"); - } catch (err) { - if (err instanceof SlackError) { - assert.include(err.message, "No webhook was provided to proxy to"); - } else { - assert.fail("Failed to throw a SlackError", err); - } - } - }); - - it("skips proxying an http webhook url altogether", async () => { - mocks.core.getInput.withArgs("webhook").returns("http://hooks.slack.com"); - mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); - mocks.core.getInput.withArgs("proxy").returns("https://example.com"); - const config = new Config(mocks.core); - const webhook = new Webhook(); - const request = webhook.proxies(config); - assert.isUndefined(request); - }); - - it("sets up the proxy agent for the provided https proxy", async () => { - const proxy = "https://example.com"; - mocks.core.getInput - .withArgs("webhook") - .returns("https://hooks.slack.com"); - mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); - mocks.core.getInput.withArgs("proxy").returns(proxy); - const config = new Config(mocks.core); - const webhook = new Webhook(); - const { httpsAgent, proxy: proxying } = webhook.proxies(config); - assert.deepEqual(httpsAgent.proxy, new URL(proxy)); - assert.isNotFalse(proxying); - }); - - it("sets up the agent without proxy for http proxies", async () => { - const proxy = "http://example.com"; - mocks.core.getInput - .withArgs("webhook") - .returns("https://hooks.slack.com"); - mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); - mocks.core.getInput.withArgs("proxy").returns(proxy); - const config = new Config(mocks.core); - const webhook = new Webhook(); - const { httpsAgent, proxy: proxying } = webhook.proxies(config); - assert.deepEqual(httpsAgent.proxy, new URL(proxy)); - assert.isFalse(proxying); - }); - - it("fails to configure proxies with an invalid proxied url", async () => { - const proxy = "https://"; - mocks.core.getInput - .withArgs("webhook") - .returns("https://hooks.slack.com"); - mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); - mocks.core.getInput.withArgs("proxy").returns(proxy); - try { - const config = new Config(mocks.core); - const webhook = new Webhook(); - webhook.proxies(config); - assert.fail("An invalid proxy URL was not thrown as error!"); - } catch (err) { - if (err instanceof SlackError) { - assert.include(err.message, "Failed to configure the HTTPS proxy"); - } else { - assert.fail("Failed to throw a SlackError", err); - } - } - }); - - it("fails to configure proxies with an unknown url protocol", async () => { - const proxy = "ssh://"; - mocks.core.getInput - .withArgs("webhook") - .returns("https://hooks.slack.com"); - mocks.core.getInput.withArgs("webhook-type").returns("incoming-webhook"); - mocks.core.getInput.withArgs("proxy").returns(proxy); - try { - const config = new Config(mocks.core); - const webhook = new Webhook(); - webhook.proxies(config); - assert.fail("An unknown URL protocol was not thrown as error!"); - } catch (err) { - if (err instanceof SlackError) { - assert.include(err.message, "Failed to configure the HTTPS proxy"); - assert.include(err.cause.message, "Unsupported URL protocol"); - } else { - assert.fail("Failed to throw a SlackError", err); - } - } - }); - }); - describe("retries", () => { it("uses a default of five retries in requests", async () => { - const webhook = new Webhook(); - const result = webhook.retries(); - assert.equal(result.retries, 5); + const result = new Webhook().retries(); + assert.equal( + result.retries, + webhook.retryPolicies.fiveRetriesInFiveMinutes.retries, + ); }); it('does not attempt retries when "0" is set', async () => { - const webhook = new Webhook(); - const result = webhook.retries("0"); + const result = new Webhook().retries("0"); assert.equal(result.retries, 0); }); it('attempts a default amount of "5" retries', async () => { - const webhook = new Webhook(); - const result = webhook.retries("5"); - assert.equal(result.retries, 5); - if (!result.retryDelay) { - assert.fail("No retry delay found!"); - } + const result = new Webhook().retries("5"); assert.equal( - result.retryDelay(5, mocks.errors.axios.network_failed), - 300000, - "5th retry after 5 seconds", + result.retries, + webhook.retryPolicies.fiveRetriesInFiveMinutes.retries, + ); + assert.equal( + result.factor, + webhook.retryPolicies.fiveRetriesInFiveMinutes.factor, ); }); it('attempts "10" retries in around "30" minutes', async () => { - const webhook = new Webhook(); - const result = webhook.retries("10"); - assert.equal(result.retries, 10); - if (!result.retryDelay) { - assert.fail("No retry delay found!"); - } - assert.isAtLeast( - result.retryDelay(10, mocks.errors.axios.network_failed), - 1800000, - "last attempt is around 30 minutes after starting", + const result = new Webhook().retries("10"); + assert.equal( + result.retries, + webhook.retryPolicies.tenRetriesInAboutThirtyMinutes.retries, ); - assert.isAtMost( - result.retryDelay(10, mocks.errors.axios.network_failed), - 3600000, - "last attempt is no more than an hour later", + assert.equal( + result.factor, + webhook.retryPolicies.tenRetriesInAboutThirtyMinutes.factor, ); }); it('attempts a " rapid" burst of "12" retries in seconds', async () => { - const webhook = new Webhook(); - const result = webhook.retries(" rapid"); - assert.equal(result.retries, 12); - if (!result.retryDelay) { - assert.fail("No retry delay found!"); - } + const result = new Webhook().retries(" rapid"); assert.equal( - result.retryDelay(12, mocks.errors.axios.network_failed), - 12000, - "12th retry after 12 seconds", + result.retries, + webhook.retryPolicies.rapidRetryPolicy.retries, + ); + assert.equal( + result.factor, + webhook.retryPolicies.rapidRetryPolicy.factor, ); }); it('attempts a "RAPID" burst of "12" retries in seconds', async () => { - const webhook = new Webhook(); - const result = webhook.retries("RAPID"); - assert.equal(result.retries, 12); - if (!result.retryDelay) { - assert.fail("No retry delay found!"); - } + const result = new Webhook().retries("RAPID"); + assert.equal( + result.retries, + webhook.retryPolicies.rapidRetryPolicy.retries, + ); assert.equal( - result.retryDelay(12, mocks.errors.axios.network_failed), - 12000, - "12th retry after 12 seconds", + result.factor, + webhook.retryPolicies.rapidRetryPolicy.factor, ); }); });